diff --git a/package.json b/package.json index fb23c98..95e5364 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d089f4..d1b72d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/scripts/build/build.mts b/scripts/build/build.mts index 27f45cc..a8504ab 100644 --- a/scripts/build/build.mts +++ b/scripts/build/build.mts @@ -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"], diff --git a/scripts/build/composeTrayIcons.mts b/scripts/build/composeTrayIcons.mts new file mode 100644 index 0000000..8e71e8a --- /dev/null +++ b/scripts/build/composeTrayIcons.mts @@ -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 = { + 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) { + 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 { + 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>(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 +): 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 + } + ]); +} diff --git a/src/main/appBadge.ts b/src/main/appBadge.ts index 46abe1d..63103e6 100644 --- a/src/main/appBadge.ts +++ b/src/main/appBadge.ts @@ -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(); -function loadBadge(index: number) { - const cached = imgCache.get(index); +const imgCache = new Map(); + +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; diff --git a/src/main/mainWindow.ts b/src/main/mainWindow.ts index 6e9a05c..17e1605 100644 --- a/src/main/mainWindow.ts +++ b/src/main/mainWindow.ts @@ -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) { diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts index d72329d..09eda0d 100644 --- a/src/preload/VesktopNative.ts +++ b/src/preload/VesktopNative.ts @@ -23,7 +23,7 @@ export const VesktopNative = { app: { relaunch: () => invoke(IpcEvents.RELAUNCH), getVersion: () => sendSync(IpcEvents.GET_VERSION), - setBadgeCount: (count: number) => invoke(IpcEvents.SET_BADGE_COUNT, count), + setAppBadgeCount: (count: number) => invoke(IpcEvents.SET_BADGE_COUNT, count), supportsWindowsTransparency: () => sendSync(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY) }, autostart: { diff --git a/src/renderer/appBadge.ts b/src/renderer/appBadge.ts index b55d488..0c1b6af 100644 --- a/src/renderer/appBadge.ts +++ b/src/renderer/appBadge.ts @@ -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); } diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx index b90c746..13fea9c 100644 --- a/src/renderer/components/Settings.tsx +++ b/src/renderer/components/Settings.tsx @@ -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 + {Settings.tray && ( + { + Settings.trayBadge = v; + if (v) setBadge(); + else VesktopNative.app.setAppBadgeCount(0); + }} + note="Show mention badge on the tray icon" + > + Tray Notification Badge + + )} + {switches.map(([key, text, note, def, predicate]) => (