diff --git a/.gitignore b/.gitignore index be8fd61d..cdaf4993 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -dist +/dist/ + node_modules .env .DS_Store .idea/ -.pnpm-store/ \ No newline at end of file +.pnpm-store/ diff --git a/package.json b/package.json index ca5cf495..39ca26d3 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^3.2.0", "prettier": "^3.3.3", + "sharp": "^0.33.0", + "sharp-ico": "^0.1.5", "source-map-support": "^0.5.21", "tsx": "^4.16.2", "type-fest": "^4.23.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e67ad18f..8571f8aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,12 @@ importers: prettier: specifier: ^3.3.3 version: 3.3.3 + sharp: + specifier: ^0.33.0 + version: 0.33.4 + sharp-ico: + specifier: ^0.1.5 + version: 0.1.5 source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -102,6 +108,9 @@ packages: 7zip-bin@5.2.0: resolution: {integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==} + '@canvas/image-data@1.0.0': + resolution: {integrity: sha512-BxOqI5LgsIQP1odU5KMwV9yoijleOPzHL18/YvNqF9KFSGF2K/DLlYAbDQsWqd/1nbaFuSkYD/191dpMtNh4vw==} + '@develar/schema-utils@2.6.5': resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} engines: {node: '>= 8.9.0'} @@ -146,6 +155,9 @@ packages: resolution: {integrity: sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==} engines: {node: '>=16.4'} + '@emnapi/runtime@1.2.0': + resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==} + '@esbuild/aix-ppc64@0.20.2': resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} engines: {node: '>=12'} @@ -459,6 +471,119 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead + '@img/sharp-darwin-arm64@0.33.4': + resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==} + 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] + + '@img/sharp-darwin-x64@0.33.4': + resolution: {integrity: sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==} + 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] + + '@img/sharp-libvips-darwin-arm64@1.0.2': + resolution: {integrity: sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==} + engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.2': + resolution: {integrity: sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==} + engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.2': + resolution: {integrity: sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.2': + resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.2': + resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.2': + resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.2': + resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.2': + resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.4': + resolution: {integrity: sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==} + 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] + + '@img/sharp-linux-arm@0.33.4': + resolution: {integrity: sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==} + 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] + + '@img/sharp-linux-s390x@0.33.4': + resolution: {integrity: sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==} + engines: {glibc: '>=2.31', 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] + + '@img/sharp-linux-x64@0.33.4': + resolution: {integrity: sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==} + 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] + + '@img/sharp-linuxmusl-arm64@0.33.4': + resolution: {integrity: sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==} + 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] + + '@img/sharp-linuxmusl-x64@0.33.4': + resolution: {integrity: sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==} + 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] + + '@img/sharp-wasm32@0.33.4': + resolution: {integrity: sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==} + 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] + + '@img/sharp-win32-ia32@0.33.4': + resolution: {integrity: sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==} + 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] + + '@img/sharp-win32-x64@0.33.4': + resolution: {integrity: sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==} + 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] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -963,10 +1088,17 @@ 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==} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1064,6 +1196,14 @@ packages: supports-color: optional: true + decode-bmp@0.2.1: + resolution: {integrity: sha512-NiOaGe+GN0KJqi2STf24hfMkFitDUaIoUU3eKvP/wAbLe8o6FuW5n/x7MHPR0HKvBokp6MQY/j7w8lewEeVCIA==} + engines: {node: '>=8.6.0'} + + decode-ico@0.4.1: + resolution: {integrity: sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==} + engines: {node: '>=8.6'} + decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} @@ -1660,6 +1800,9 @@ packages: humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + ico-endec@0.1.6: + resolution: {integrity: sha512-ZdLU38ZoED3g1j3iEyzcQj+wAkY2xfWNkymszfJPoxucIUhK7NayQ+/C4Kv0nDFMIsbtbEHldv3V8PU494/ueQ==} + iconv-corefoundation@1.1.7: resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} engines: {node: ^8.11.2 || >=10} @@ -1717,6 +1860,9 @@ packages: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-bigint@1.0.4: resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} @@ -2488,6 +2634,13 @@ packages: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} + sharp-ico@0.1.5: + resolution: {integrity: sha512-a3jODQl82NPp1d5OYb0wY+oFaPk7AvyxipIowCHk7pBsZCWgbe0yAkU2OOXdoH0ENyANhyOQbs9xkAiRHcF02Q==} + + sharp@0.33.4: + resolution: {integrity: sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==} + engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2507,6 +2660,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-update-notifier@2.0.0: resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} engines: {node: '>=10'} @@ -2661,6 +2817,9 @@ packages: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} + to-data-view@1.1.0: + resolution: {integrity: sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==} + to-object-path@0.3.0: resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} engines: {node: '>=0.10.0'} @@ -2875,6 +3034,8 @@ snapshots: 7zip-bin@5.2.0: {} + '@canvas/image-data@1.0.0': {} + '@develar/schema-utils@2.6.5': dependencies: ajv: 6.12.6 @@ -2982,6 +3143,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@emnapi/runtime@1.2.0': + dependencies: + tslib: 2.6.3 + optional: true + '@esbuild/aix-ppc64@0.20.2': optional: true @@ -3159,6 +3325,81 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@img/sharp-darwin-arm64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.2 + optional: true + + '@img/sharp-darwin-x64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.2 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.2': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.2': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.2': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.2': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.2': + optional: true + + '@img/sharp-linux-arm64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.2 + optional: true + + '@img/sharp-linux-arm@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.2 + optional: true + + '@img/sharp-linux-s390x@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.2 + optional: true + + '@img/sharp-linux-x64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.2 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + optional: true + + '@img/sharp-wasm32@0.33.4': + dependencies: + '@emnapi/runtime': 1.2.0 + optional: true + + '@img/sharp-win32-ia32@0.33.4': + optional: true + + '@img/sharp-win32-x64@0.33.4': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3891,8 +4132,18 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + color-support@1.1.3: {} + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -3981,6 +4232,17 @@ snapshots: dependencies: ms: 2.1.2 + decode-bmp@0.2.1: + dependencies: + '@canvas/image-data': 1.0.0 + to-data-view: 1.1.0 + + decode-ico@0.4.1: + dependencies: + '@canvas/image-data': 1.0.0 + decode-bmp: 0.2.1 + to-data-view: 1.1.0 + decode-uri-component@0.2.2: {} decompress-response@6.0.0: @@ -4818,6 +5080,8 @@ snapshots: dependencies: ms: 2.1.3 + ico-endec@0.1.6: {} + iconv-corefoundation@1.1.7: dependencies: cli-truncate: 2.1.0 @@ -4873,6 +5137,8 @@ snapshots: call-bind: 1.0.7 get-intrinsic: 1.2.4 + is-arrayish@0.3.2: {} + is-bigint@1.0.4: dependencies: has-bigints: 1.0.2 @@ -5648,6 +5914,38 @@ snapshots: is-plain-object: 2.0.4 split-string: 3.1.0 + sharp-ico@0.1.5: + dependencies: + decode-ico: 0.4.1 + ico-endec: 0.1.6 + sharp: 0.33.4 + + sharp@0.33.4: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.4 + '@img/sharp-darwin-x64': 0.33.4 + '@img/sharp-libvips-darwin-arm64': 1.0.2 + '@img/sharp-libvips-darwin-x64': 1.0.2 + '@img/sharp-libvips-linux-arm': 1.0.2 + '@img/sharp-libvips-linux-arm64': 1.0.2 + '@img/sharp-libvips-linux-s390x': 1.0.2 + '@img/sharp-libvips-linux-x64': 1.0.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + '@img/sharp-linux-arm': 0.33.4 + '@img/sharp-linux-arm64': 0.33.4 + '@img/sharp-linux-s390x': 0.33.4 + '@img/sharp-linux-x64': 0.33.4 + '@img/sharp-linuxmusl-arm64': 0.33.4 + '@img/sharp-linuxmusl-x64': 0.33.4 + '@img/sharp-wasm32': 0.33.4 + '@img/sharp-win32-ia32': 0.33.4 + '@img/sharp-win32-x64': 0.33.4 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5665,6 +5963,10 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + simple-update-notifier@2.0.0: dependencies: semver: 7.6.3 @@ -5849,6 +6151,8 @@ snapshots: tmp@0.2.3: {} + to-data-view@1.1.0: {} + to-object-path@0.3.0: dependencies: kind-of: 3.2.2 diff --git a/scripts/build/build.mts b/scripts/build/build.mts index 243381ba..8d2c6697 100644 --- a/scripts/build/build.mts +++ b/scripts/build/build.mts @@ -7,6 +7,7 @@ import { BuildContext, BuildOptions, context } from "esbuild"; import { copyFile } from "fs/promises"; +import { composeTrayIcons } from "./composeTrayIcons.mts"; import vencordDep from "./vencordDep.mjs"; const isDev = process.argv.includes("--dev"); @@ -49,8 +50,20 @@ async function copyVenmic() { ]).catch(() => console.warn("Failed to copy venmic. Building without venmic support")); } +async function composeTrayIconsIfSupported() { + if (process.platform === "darwin") return; + + return composeTrayIcons({ + icon: "./static/icon.png", + badgeDir: "./static/badges/", + outDir: "./static/dist/tray_icons", + createEmpty: true + }); +} + await Promise.all([ copyVenmic(), + composeTrayIconsIfSupported(), createContext({ ...NodeCommonOpts, entryPoints: ["src/main/index.ts"], diff --git a/scripts/build/composeTrayIcons.mts b/scripts/build/composeTrayIcons.mts new file mode 100644 index 00000000..021ae0e2 --- /dev/null +++ b/scripts/build/composeTrayIcons.mts @@ -0,0 +1,231 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { readdir, stat } from "node:fs/promises"; +import { format as pathFormat, join, parse as pathParse } from "node:path"; + +import sharp from "sharp"; +import { type ImageData, sharpsFromIco } from "sharp-ico"; + +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, + badgeDir, + outDir, + outExt = ".png", + createEmpty = false, + iconOptions = { width: 64, height: 64 }, + badgeOptions = undefined +}: { + icon: string | Buffer | sharp.Sharp; + badgeDir: string; + outDir: string; + outExt?: string; + createEmpty?: boolean; + iconOptions?: ImageDim; + badgeOptions?: BadgeOptions; +}) { + const badges: string[] = []; + for (const filename of await readdir(badgeDir)) { + const path = join(badgeDir, filename); + if (!(await stat(path)).isDirectory()) { + badges.push(path); + } + } + + 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 + }); + const 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 = 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 46abe1db..ac2659a7 100644 --- a/src/main/appBadge.ts +++ b/src/main/appBadge.ts @@ -6,26 +6,45 @@ 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 { tray, mainWin } 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) { + tray?.setImage(loadTrayIcon(index ?? 0)); + } + + if (!Settings.store.appBadge) return; + switch (process.platform) { case "linux": if (count === -1) count = 0; - app.setBadgeCount(count); + app.setBadgeCount(count); // Only works if libunity is installed break; case "darwin": if (count === 0) { @@ -35,13 +54,10 @@ 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; - // circular import shenanigans - const { mainWin } = require("./mainWindow") as typeof import("./mainWindow"); mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description); break; } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 4fa662c3..b8736c79 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -6,6 +6,7 @@ if (process.platform === "linux") import("./venmic"); +// eslint-disable-next-line simple-import-sort/imports import { execFile } from "child_process"; import { app, BrowserWindow, clipboard, dialog, nativeImage, RelaunchOptions, session, shell } from "electron"; import { mkdirSync, readFileSync, watch } from "fs"; @@ -15,11 +16,11 @@ import { join } from "path"; import { debounce } from "shared/utils/debounce"; import { IpcEvents } from "../shared/IpcEvents"; -import { setBadgeCount } from "./appBadge"; import { autoStart } from "./autoStart"; import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants"; import { mainWin } from "./mainWindow"; import { Settings, State } from "./settings"; +import { setBadgeCount } from "./appBadge"; import { handle, handleSync } from "./utils/ipcWrappers"; import { PopoutWindows } from "./utils/popout"; import { isDeckGameMode, showGamePage } from "./utils/steamOS"; diff --git a/src/main/mainWindow.ts b/src/main/mainWindow.ts index d860b376..2202c13e 100644 --- a/src/main/mainWindow.ts +++ b/src/main/mainWindow.ts @@ -23,7 +23,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 { @@ -43,7 +43,6 @@ import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./u import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader"; let isQuitting = false; -let tray: Tray; applyDeckKeyboardFix(); @@ -52,6 +51,7 @@ app.on("before-quit", () => { }); export let mainWin: BrowserWindow; +export let tray: Tray | null = null; function makeSettingsListenerHelpers(o: SettingsStore) { const listeners = new Map<(data: any) => void, PropertyKey>(); @@ -123,7 +123,7 @@ function initTray(win: BrowserWindow) { } ]); - tray = new Tray(ICON_PATH); + tray = new Tray(TRAY_ICON_PATH); tray.setToolTip("Vesktop"); tray.setContextMenu(trayMenu); tray.on("click", onTrayClick); @@ -330,8 +330,12 @@ function initWindowBoundsListeners(win: BrowserWindow) { function initSettingsListeners(win: BrowserWindow) { addSettingsListener("tray", enable => { - if (enable) initTray(win); - else tray?.destroy(); + if (enable) { + initTray(win); + } else if (tray) { + tray.destroy(); + tray = null; + } }); addSettingsListener("disableMinSize", disable => { if (disable) { diff --git a/src/renderer/appBadge.ts b/src/renderer/appBadge.ts index b55d488b..0cc95b84 100644 --- a/src/renderer/appBadge.ts +++ b/src/renderer/appBadge.ts @@ -13,8 +13,6 @@ let GuildReadStateStore: any; let NotificationSettingsStore: any; export function setBadge() { - if (Settings.store.appBadge === false) return; - try { const mentionCount = GuildReadStateStore.getTotalMentionCount(); const pendingRequests = RelationshipStore.getPendingCount(); @@ -24,7 +22,9 @@ export function setBadge() { let totalCount = mentionCount + pendingRequests; if (!totalCount && hasUnread && !disableUnreadBadge) totalCount = -1; - VesktopNative.app.setBadgeCount(totalCount); + if (Settings.store.appBadge || Settings.store.trayBadge) { + VesktopNative.app.setBadgeCount(totalCount); + } } catch (e) { console.error(e); } diff --git a/src/renderer/components/settings/Settings.tsx b/src/renderer/components/settings/Settings.tsx index d56f0ea4..cf9eed34 100644 --- a/src/renderer/components/settings/Settings.tsx +++ b/src/renderer/components/settings/Settings.tsx @@ -14,6 +14,7 @@ import { isMac, isWindows } from "renderer/utils"; import { AutoStartToggle } from "./AutoStartToggle"; import { DiscordBranchPicker } from "./DiscordBranchPicker"; import { NotificationBadgeToggle } from "./NotificationBadgeToggle"; +import { TrayNotificationBadgeToggle } from "./TrayNotificationBadgeToggle"; import { VencordLocationPicker } from "./VencordLocationPicker"; import { WindowsTransparencyControls } from "./WindowsTransparencyControls"; @@ -102,7 +103,7 @@ const SettingsOptions: Record> defaultValue: false } ], - Notifications: [NotificationBadgeToggle], + Notifications: [NotificationBadgeToggle, TrayNotificationBadgeToggle], Miscelleanous: [ { key: "arRPC", diff --git a/src/renderer/components/settings/TrayNotificationBadgeToggle.tsx b/src/renderer/components/settings/TrayNotificationBadgeToggle.tsx new file mode 100644 index 00000000..6fd81840 --- /dev/null +++ b/src/renderer/components/settings/TrayNotificationBadgeToggle.tsx @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { Switch } from "@vencord/types/webpack/common"; +import { setBadge } from "renderer/appBadge"; + +import { SettingsComponent } from "./Settings"; + +export const TrayNotificationBadgeToggle: SettingsComponent = ({ settings }) => { + return ( + { + settings.trayBadge = v; + if (v) setBadge(); + else VesktopNative.app.setBadgeCount(0); + }} + note="Show mention badge on the tray icon" + > + Tray Notification Badge + + ); +}; diff --git a/src/shared/paths.ts b/src/shared/paths.ts index 483250ac..004b0630 100644 --- a/src/shared/paths.ts +++ b/src/shared/paths.ts @@ -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"); diff --git a/src/shared/settings.d.ts b/src/shared/settings.d.ts index f1000103..fb76849a 100644 --- a/src/shared/settings.d.ts +++ b/src/shared/settings.d.ts @@ -10,6 +10,7 @@ export interface Settings { discordBranch?: "stable" | "canary" | "ptb"; transparencyOption?: "none" | "mica" | "tabbed" | "acrylic"; tray?: boolean; + trayBadge?: boolean; minimizeToTray?: boolean; openLinksWithElectron?: boolean; staticTitle?: boolean; diff --git a/static/dist/.gitignore b/static/dist/.gitignore index c96a04f0..51c9238d 100644 --- a/static/dist/.gitignore +++ b/static/dist/.gitignore @@ -1,2 +1,3 @@ * -!.gitignore \ No newline at end of file +!.gitignore +!tray_icons/ diff --git a/static/dist/tray_icons/.gitignore b/static/dist/tray_icons/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/static/dist/tray_icons/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore