Tray icon notifications

This commit is contained in:
Albert Zhang 2023-11-29 20:17:26 -05:00
parent 40b952d8bf
commit 7f1de8e508
No known key found for this signature in database
GPG key ID: D74C859E94CA6DDC
12 changed files with 589 additions and 19 deletions

View file

@ -48,7 +48,10 @@
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^3.0.0",
"fast-glob": "3.3",
"prettier": "^3.1.0",
"sharp": "^0.33.0",
"sharp-ico": "^0.1.5",
"source-map-support": "^0.5.21",
"tsx": "^4.6.0",
"type-fest": "^4.8.2",

View file

@ -69,9 +69,18 @@ devDependencies:
eslint-plugin-unused-imports:
specifier: ^3.0.0
version: 3.0.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.54.0)
fast-glob:
specifier: '3.3'
version: 3.3.1
prettier:
specifier: ^3.1.0
version: 3.1.0
sharp:
specifier: ^0.33.0
version: 0.33.0
sharp-ico:
specifier: ^0.1.5
version: 0.1.5
source-map-support:
specifier: ^0.5.21
version: 0.5.21
@ -99,6 +108,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/@canvas/image-data@1.0.0:
resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==}
dev: true
/@develar/schema-utils@2.6.5:
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
engines: {node: '>= 8.9.0'}
@ -175,6 +188,14 @@ packages:
- supports-color
dev: true
/@emnapi/runtime@0.44.0:
resolution: {integrity: sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==}
requiresBuild: true
dependencies:
tslib: 2.6.2
dev: true
optional: true
/@esbuild/android-arm64@0.18.20:
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
engines: {node: '>=12'}
@ -632,6 +653,194 @@ packages:
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
dev: true
/@img/sharp-darwin-arm64@0.33.0:
resolution: {integrity: sha512-070tEheekI1LJWTGPC9WlQEa5UoKTXzzlORBHMX4TbfUxMiL336YHR8vBEUNsjse0RJCX8dZ4ZXwT595aEF1ug==}
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.0
dev: true
optional: true
/@img/sharp-darwin-x64@0.33.0:
resolution: {integrity: sha512-pu/nvn152F3qbPeUkr+4e9zVvEhD3jhwzF473veQfMPkOYo9aoWXSfdZH/E6F+nYC3qvFjbxbvdDbUtEbghLqw==}
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [darwin]
requiresBuild: true
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.0.0
dev: true
optional: true
/@img/sharp-libvips-darwin-arm64@1.0.0:
resolution: {integrity: sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==}
engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@img/sharp-libvips-darwin-x64@1.0.0:
resolution: {integrity: sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==}
engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@img/sharp-libvips-linux-arm64@1.0.0:
resolution: {integrity: sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==}
engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@img/sharp-libvips-linux-arm@1.0.0:
resolution: {integrity: sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==}
engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@img/sharp-libvips-linux-s390x@1.0.0:
resolution: {integrity: sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==}
engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [s390x]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@img/sharp-libvips-linux-x64@1.0.0:
resolution: {integrity: sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==}
engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@img/sharp-libvips-linuxmusl-arm64@1.0.0:
resolution: {integrity: sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==}
engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@img/sharp-libvips-linuxmusl-x64@1.0.0:
resolution: {integrity: sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==}
engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@img/sharp-linux-arm64@0.33.0:
resolution: {integrity: sha512-dcomVSrtgF70SyOr8RCOCQ8XGVThXwe71A1d8MGA+mXEVRJ/J6/TrCbBEJh9ddcEIIsrnrkolaEvYSHqVhswQw==}
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [linux]
requiresBuild: true
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.0.0
dev: true
optional: true
/@img/sharp-linux-arm@0.33.0:
resolution: {integrity: sha512-4horD3wMFd5a0ddbDY8/dXU9CaOgHjEHALAddXgafoR5oWq5s8X61PDgsSeh4Qupsdo6ycfPPSSNBrfVQnwwrg==}
engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm]
os: [linux]
requiresBuild: true
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.0.0
dev: true
optional: true
/@img/sharp-linux-s390x@0.33.0:
resolution: {integrity: sha512-TiVJbx38J2rNVfA309ffSOB+3/7wOsZYQEOlKqOUdWD/nqkjNGrX+YQGz7nzcf5oy2lC+d37+w183iNXRZNngQ==}
engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [s390x]
os: [linux]
requiresBuild: true
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.0.0
dev: true
optional: true
/@img/sharp-linux-x64@0.33.0:
resolution: {integrity: sha512-PaZM4Zi7/Ek71WgTdvR+KzTZpBqrQOFcPe7/8ZoPRlTYYRe43k6TWsf4GVH6XKRLMYeSp8J89RfAhBrSP4itNA==}
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [linux]
requiresBuild: true
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.0.0
dev: true
optional: true
/@img/sharp-linuxmusl-arm64@0.33.0:
resolution: {integrity: sha512-1QLbbN0zt+32eVrg7bb1lwtvEaZwlhEsY1OrijroMkwAqlHqFj6R33Y47s2XUv7P6Ie1PwCxK/uFnNqMnkd5kg==}
engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [arm64]
os: [linux]
requiresBuild: true
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.0.0
dev: true
optional: true
/@img/sharp-linuxmusl-x64@0.33.0:
resolution: {integrity: sha512-CecqgB/CnkvCWFhmfN9ZhPGMLXaEBXl4o7WtA6U3Ztrlh/s7FUKX4vNxpMSYLIrWuuzjiaYdfU3+Tdqh1xaHfw==}
engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [linux]
requiresBuild: true
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.0.0
dev: true
optional: true
/@img/sharp-wasm32@0.33.0:
resolution: {integrity: sha512-Hn4js32gUX9qkISlemZBUPuMs0k/xNJebUNl/L6djnU07B/HAA2KaxRVb3HvbU5fL242hLOcp0+tR+M8dvJUFw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [wasm32]
requiresBuild: true
dependencies:
'@emnapi/runtime': 0.44.0
dev: true
optional: true
/@img/sharp-win32-ia32@0.33.0:
resolution: {integrity: sha512-5HfcsCZi3l5nPRF2q3bllMVMDXBqEWI3Q8KQONfzl0TferFE5lnsIG0A1YrntMAGqvkzdW6y1Ci1A2uTvxhfzg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@img/sharp-win32-x64@0.33.0:
resolution: {integrity: sha512-i3DtP/2ce1yKFj4OzOnOYltOEL/+dp4dc4dJXJBv6god1AFTcmkaA99H/7SwOmkCOBQkbVvA3lCGm3/5nDtf9Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@malept/cross-spawn-promise@1.1.1:
resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==}
engines: {node: '>= 10'}
@ -1513,6 +1722,13 @@ packages:
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
/color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
dev: true
/color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
@ -1520,6 +1736,14 @@ packages:
dev: false
optional: true
/color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
dev: true
/combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
@ -1622,6 +1846,23 @@ packages:
dependencies:
ms: 2.1.2
/decode-bmp@0.2.1:
resolution: {integrity: sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==}
engines: {node: '>=8.6.0'}
dependencies:
'@canvas/image-data': 1.0.0
to-data-view: 1.1.0
dev: true
/decode-ico@0.4.1:
resolution: {integrity: sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==}
engines: {node: '>=8.6'}
dependencies:
'@canvas/image-data': 1.0.0
decode-bmp: 0.2.1
to-data-view: 1.1.0
dev: true
/decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
@ -1723,6 +1964,11 @@ packages:
dev: false
optional: true
/detect-libc@2.0.2:
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
dev: true
/detect-node@2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
requiresBuild: true
@ -2810,6 +3056,10 @@ packages:
engines: {node: '>=14.18.0'}
dev: true
/ico-endec@0.1.6:
resolution: {integrity: sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==}
dev: true
/iconv-corefoundation@1.1.7:
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
engines: {node: ^8.11.2 || >=10}
@ -2899,6 +3149,10 @@ packages:
is-typed-array: 1.1.12
dev: true
/is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
dev: true
/is-bigint@1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies:
@ -4020,6 +4274,44 @@ packages:
split-string: 3.1.0
dev: true
/sharp-ico@0.1.5:
resolution: {integrity: sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q==}
dependencies:
decode-ico: 0.4.1
ico-endec: 0.1.6
sharp: 0.33.0
dev: true
/sharp@0.33.0:
resolution: {integrity: sha512-99DZKudjm/Rmz+M0/26t4DKpXyywAOJaayGS9boEn7FvgtG0RYBi46uPE2c+obcJRtA3AZa0QwJot63gJQ1F0Q==}
engines: {libvips: '>=8.15.0', node: ^18.17.0 || ^20.3.0 || >=21.0.0}
requiresBuild: true
dependencies:
color: 4.2.3
detect-libc: 2.0.2
semver: 7.5.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.0
'@img/sharp-darwin-x64': 0.33.0
'@img/sharp-libvips-darwin-arm64': 1.0.0
'@img/sharp-libvips-darwin-x64': 1.0.0
'@img/sharp-libvips-linux-arm': 1.0.0
'@img/sharp-libvips-linux-arm64': 1.0.0
'@img/sharp-libvips-linux-s390x': 1.0.0
'@img/sharp-libvips-linux-x64': 1.0.0
'@img/sharp-libvips-linuxmusl-arm64': 1.0.0
'@img/sharp-libvips-linuxmusl-x64': 1.0.0
'@img/sharp-linux-arm': 0.33.0
'@img/sharp-linux-arm64': 0.33.0
'@img/sharp-linux-s390x': 0.33.0
'@img/sharp-linux-x64': 0.33.0
'@img/sharp-linuxmusl-arm64': 0.33.0
'@img/sharp-linuxmusl-x64': 0.33.0
'@img/sharp-wasm32': 0.33.0
'@img/sharp-win32-ia32': 0.33.0
'@img/sharp-win32-x64': 0.33.0
dev: true
/shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@ -4043,6 +4335,12 @@ packages:
/signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
/simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
dependencies:
is-arrayish: 0.3.2
dev: true
/simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
@ -4297,6 +4595,10 @@ packages:
rimraf: 3.0.2
dev: true
/to-data-view@1.1.0:
resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==}
dev: true
/to-object-path@0.3.0:
resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==}
engines: {node: '>=0.10.0'}

View file

@ -8,6 +8,7 @@ import { BuildContext, BuildOptions, context } from "esbuild";
import { copyFile } from "fs/promises";
import vencordDep from "./vencordDep.mjs";
import { composeTrayIcons } from "./composeTrayIcons.mts";
const isDev = process.argv.includes("--dev");
@ -51,6 +52,12 @@ async function copyVenmic() {
await Promise.all([
copyVenmic(),
composeTrayIcons({
icon: "./static/icon.png",
badges: "./static/badges/*",
outDir: "./static/dist/tray_icons",
createEmpty: true
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/main/index.ts"],

View file

@ -0,0 +1,221 @@
import sharp, { OutputInfo } from "sharp";
import fastGlob from "fast-glob";
import type { ImageData } from "sharp-ico";
import { parse as pathParse, format as pathFormat } from "node:path";
interface BadgePosition {
left?: number;
top?: number;
anchorX?: "left" | "right" | "center";
anchorY?: "top" | "bottom" | "center";
}
interface BadgeOptions extends BadgePosition {
width?: number;
height?: number;
resizeOptions?: sharp.ResizeOptions;
}
const DEFAULT_BADGE_OPTIONS: Required<BadgeOptions> = {
width: 0.5,
height: 0.5,
left: 0.8,
top: 0.8,
anchorX: "center",
anchorY: "center",
resizeOptions: {
kernel: sharp.kernel.cubic
}
};
export async function composeTrayIcons({
icon: iconPath,
badges: badgeGlob,
outDir,
outExt = ".png",
createEmpty = false,
iconOptions = { width: 64, height: 64 },
badgeOptions = undefined
}: {
icon: string | Buffer | sharp.Sharp;
badges: string;
outDir: string;
outExt?: string;
createEmpty?: boolean;
iconOptions?: ImageDim;
badgeOptions?: BadgeOptions;
}) {
const badges = await fastGlob.glob(badgeGlob);
if (!badges.length) {
throw new Error(`No badges matching glob '${badgeGlob}' found!`);
}
const badgeOptionsFilled = { ...DEFAULT_BADGE_OPTIONS, ...badgeOptions };
const { data: iconData, info: iconInfo } = await resolveImageOrIco(iconPath, iconOptions);
const iconName = typeof iconPath === "string" ? pathParse(iconPath).name : "tray_icon";
const resizedBadgeDim = {
height: Math.round(badgeOptionsFilled.height * iconInfo.height),
width: Math.round(badgeOptionsFilled.width * iconInfo.width)
};
async function doCompose(badgePath: string | sharp.Sharp, ensureSize?: ImageDim | false) {
const { data: badgeData, info: badgeInfo } = await resolveImageOrIco(badgePath, resizedBadgeDim);
if (ensureSize && (badgeInfo.height !== ensureSize.height || badgeInfo.width !== ensureSize.width)) {
throw new Error(
`Badge loaded from ${badgePath} has size ${badgeInfo.height}x${badgeInfo.height} != ${ensureSize.height}x${ensureSize.height}`
);
}
const savePath = pathFormat({
name: iconName + (typeof badgePath === "string" ? "_" + pathParse(badgePath).name : ""),
dir: outDir,
ext: outExt,
base: undefined
});
let out = composeTrayIcon(iconData, iconInfo, badgeData, badgeInfo, badgeOptionsFilled);
const outputInfo = await out.toFile(savePath);
return {
iconInfo,
badgeInfo,
outputInfo
};
}
if (createEmpty) {
const firstComposition = await doCompose(badges[0]);
return await Promise.all([
firstComposition,
...badges.map(badge => doCompose(badge, firstComposition.badgeInfo)),
doCompose(emptyImage(firstComposition.badgeInfo).png())
]);
} else {
return await Promise.all(badges.map(badge => doCompose(badge)));
}
}
type SharpInput = string | Buffer;
interface ImageDim {
width: number;
height: number;
}
async function resolveImageOrIco(...args: Parameters<typeof loadFromImageOrIco>) {
const image = await loadFromImageOrIco(...args);
const { data, info } = await image.toBuffer({ resolveWithObject: true });
return {
data,
info: validDim(info)
};
}
async function loadFromImageOrIco(
path: string | Buffer | sharp.Sharp,
sizeOptions?: ImageDim & { resizeICO?: boolean }
): Promise<sharp.Sharp> {
if (typeof path === "string" && path.endsWith(".ico")) {
const icos = (await import("sharp-ico")).sharpsFromIco(path, undefined, true) as unknown as ImageData[];
let icoInfo;
if (sizeOptions == null) {
icoInfo = icos[icos.length - 1];
} else {
icoInfo = icos.reduce((best, ico) =>
Math.abs(ico.width - sizeOptions.width) < Math.abs(ico.width - best.width) ? ico : best
);
}
if (icoInfo.image == null) {
throw new Error("Bug: sharps-ico found no image in ICO");
}
const icoImage = icoInfo.image.png();
if (sizeOptions?.resizeICO) {
return icoImage.resize(sizeOptions);
} else {
return icoImage;
}
} else {
let image = typeof path !== "string" && "toBuffer" in path ? path : sharp(path);
if (sizeOptions) {
image = image.resize(sizeOptions);
}
return image;
}
}
function validDim<T extends Partial<ImageDim>>(meta: T): T & ImageDim {
if (meta?.width == null || meta?.height == null) {
throw new Error("Failed getting icon dimensions");
}
return meta as T & ImageDim;
}
function emptyImage(dim: ImageDim) {
return sharp({
create: {
width: dim.width,
height: dim.height,
channels: 4,
background: { r: 0, b: 0, g: 0, alpha: 0 }
}
});
}
function composeTrayIcon(
icon: SharpInput,
iconDim: ImageDim,
badge: SharpInput,
badgeDim: ImageDim,
badgeOptions: Required<BadgeOptions>
): sharp.Sharp {
let badgeLeft = badgeOptions.left * iconDim.width;
switch (badgeOptions.anchorX) {
case "left":
break;
case "right":
badgeLeft -= badgeDim.width;
break;
case "center":
badgeLeft -= badgeDim.width / 2;
break;
}
let badgeTop = badgeOptions.top * iconDim.height;
switch (badgeOptions.anchorY) {
case "top":
break;
case "bottom":
badgeTop -= badgeDim.height / 2;
break;
case "center":
badgeTop -= badgeDim.height / 2;
break;
}
badgeTop = Math.round(badgeTop);
badgeLeft = Math.round(badgeLeft);
const padding = Math.max(
0,
-badgeLeft,
badgeLeft + badgeDim.width - iconDim.width,
-badgeTop,
badgeTop + badgeDim.height - iconDim.height
);
return emptyImage({
width: iconDim.width + 2 * padding,
height: iconDim.height + 2 * padding
}).composite([
{
input: icon,
left: padding,
top: padding
},
{
input: badge,
left: badgeLeft + padding,
top: badgeTop + padding
}
]);
}

View file

@ -6,27 +6,43 @@
import { app, NativeImage, nativeImage } from "electron";
import { join } from "path";
import { BADGE_DIR } from "shared/paths";
import { BADGE_DIR, TRAY_ICON_DIR, TRAY_ICON_PATH } from "shared/paths";
import { trayContainer } from "./mainWindow";
import { Settings } from "./settings";
const imgCache = new Map<number, NativeImage>();
function loadBadge(index: number) {
const cached = imgCache.get(index);
const imgCache = new Map<string, NativeImage>();
function loadImg(path: string) {
const cached = imgCache.get(path);
if (cached) return cached;
const img = nativeImage.createFromPath(join(BADGE_DIR, `${index}.ico`));
imgCache.set(index, img);
const img = nativeImage.createFromPath(path);
imgCache.set(path, img);
return img;
}
function loadBadge(index: number) {
return loadImg(join(BADGE_DIR, `${index}.ico`));
}
function loadTrayIcon(index: number) {
return loadImg(index === 0 ? TRAY_ICON_PATH : join(TRAY_ICON_DIR, `icon_${index}.png`));
}
let lastIndex: null | number = -1;
export function setBadgeCount(count: number) {
const [index, description] = getBadgeIndexAndDescription(count);
if (Settings?.store.trayBadge) {
trayContainer.tray?.setImage(loadTrayIcon(index ?? 0));
}
switch (process.platform) {
case "linux":
if (count === -1) count = 0;
app.setBadgeCount(count);
break;
app.setBadgeCount(count); // Only works if libunity is installed
case "darwin":
if (count === 0) {
app.dock.setBadge("");
@ -35,7 +51,6 @@ export function setBadgeCount(count: number) {
app.dock.setBadge(count === -1 ? "•" : count.toString());
break;
case "win32":
const [index, description] = getBadgeIndexAndDescription(count);
if (lastIndex === index) break;
lastIndex = index;

View file

@ -21,7 +21,7 @@ import { isTruthy } from "shared/utils/guards";
import { once } from "shared/utils/once";
import type { SettingsStore } from "shared/utils/SettingsStore";
import { ICON_PATH } from "../shared/paths";
import { ICON_PATH, TRAY_ICON_PATH } from "../shared/paths";
import { createAboutWindow } from "./about";
import { initArRPC } from "./arrpc";
import {
@ -41,7 +41,9 @@ import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./u
import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
let isQuitting = false;
let tray: Tray;
export const trayContainer: { tray: Tray | null } = {
tray: null
};
applyDeckKeyboardFix();
@ -118,7 +120,7 @@ function initTray(win: BrowserWindow) {
}
]);
tray = new Tray(ICON_PATH);
const tray = (trayContainer.tray = new Tray(TRAY_ICON_PATH));
tray.setToolTip("Vesktop");
tray.setContextMenu(trayMenu);
tray.on("click", () => win.show());
@ -332,7 +334,7 @@ function initWindowBoundsListeners(win: BrowserWindow) {
function initSettingsListeners(win: BrowserWindow) {
addSettingsListener("tray", enable => {
if (enable) initTray(win);
else tray?.destroy();
else trayContainer.tray?.destroy();
});
addSettingsListener("disableMinSize", disable => {
if (disable) {

View file

@ -23,7 +23,7 @@ export const VesktopNative = {
app: {
relaunch: () => invoke<void>(IpcEvents.RELAUNCH),
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION),
setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
setAppBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY)
},
autostart: {

View file

@ -13,7 +13,9 @@ let GuildReadStateStore: any;
let NotificationSettingsStore: any;
export function setBadge() {
if (Settings.store.appBadge === false) return;
const { appBadge, trayBadge } = Settings.store;
if (appBadge === false && trayBadge === false) return;
try {
const mentionCount = GuildReadStateStore.getTotalMentionCount();
@ -24,7 +26,7 @@ export function setBadge() {
let totalCount = mentionCount + pendingRequests;
if (!totalCount && hasUnread && !disableUnreadBadge) totalCount = -1;
VesktopNative.app.setBadgeCount(totalCount);
if (appBadge || trayBadge) VesktopNative.app.setAppBadgeCount(totalCount);
} catch (e) {
console.error(e);
}

View file

@ -93,13 +93,27 @@ export default function SettingsUi() {
onChange={v => {
Settings.appBadge = v;
if (v) setBadge();
else VesktopNative.app.setBadgeCount(0);
else VesktopNative.app.setAppBadgeCount(0);
}}
note="Show mention badge on the app icon"
note="Show mention badge on the app (taskbar/panel) icon"
>
Notification Badge
</Switch>
{Settings.tray && (
<Switch
value={Settings.trayBadge ?? true}
onChange={v => {
Settings.trayBadge = v;
if (v) setBadge();
else VesktopNative.app.setAppBadgeCount(0);
}}
note="Show mention badge on the tray icon"
>
Tray Notification Badge
</Switch>
)}
{switches.map(([key, text, note, def, predicate]) => (
<Switch
value={(Settings[key as any] ?? def ?? false) && predicate?.() !== false}

View file

@ -10,3 +10,5 @@ export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static");
export const VIEW_DIR = /* @__PURE__ */ join(STATIC_DIR, "views");
export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges");
export const ICON_PATH = /* @__PURE__ */ join(STATIC_DIR, "icon.png");
export const TRAY_ICON_DIR = /* @__PURE__ */ join(STATIC_DIR, "dist", "tray_icons");
export const TRAY_ICON_PATH = /* @__PURE__ */ join(TRAY_ICON_DIR, "icon.png");

View file

@ -11,6 +11,7 @@ export interface Settings {
vencordDir?: string;
transparencyOption?: "none" | "mica" | "tabbed" | "acrylic";
tray?: boolean;
trayBadge?: boolean;
minimizeToTray?: boolean;
openLinksWithElectron?: boolean;
staticTitle?: boolean;

View file

@ -1,2 +1,3 @@
*
!.gitignore
!tray_icons/.gitkeep