diff --git a/components.d.ts b/components.d.ts index 3e65c3cc52..fc7444f62f 100644 --- a/components.d.ts +++ b/components.d.ts @@ -78,6 +78,7 @@ declare module '@vue/runtime-core' { EmojiPicker: typeof import('./src/tools/emoji-picker/emoji-picker.vue')['default'] Encryption: typeof import('./src/tools/encryption/encryption.vue')['default'] EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default'] + FaviconGenerator: typeof import('./src/tools/favicon-generator/favicon-generator.vue')['default'] FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default'] FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default'] GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default'] @@ -129,6 +130,7 @@ declare module '@vue/runtime-core' { MenuLayout: typeof import('./src/components/MenuLayout.vue')['default'] MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] + NAlert: typeof import('naive-ui')['NAlert'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] NCheckbox: typeof import('naive-ui')['NCheckbox'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] diff --git a/package.json b/package.json index 5738b6325a..a614fd90d7 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "ibantools": "^4.3.3", "js-base64": "^3.7.6", "json5": "^2.2.3", + "jszip": "^3.10.1", "jwt-decode": "^3.1.2", "libphonenumber-js": "^1.10.28", "lodash": "^4.17.21", @@ -105,6 +106,11 @@ "xml-js": "^1.6.11", "yaml": "^2.2.1" }, + "pnpm": { + "overrides": { + "@vueuse/shared": "10.3.0" + } + }, "devDependencies": { "@antfu/eslint-config": "^0.41.0", "@iconify-json/mdi": "^1.1.50", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d11389323..305c77d635 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@vueuse/shared': 10.3.0 + importers: .: @@ -49,7 +52,7 @@ importers: version: 10.3.0(vue@3.3.4) '@vueuse/head': specifier: ^1.0.0 - version: 1.0.0(typescript@5.2.2)(vue@3.3.4) + version: 1.0.0(vue@3.3.4) '@vueuse/router': specifier: ^10.0.0 version: 10.0.0(vue-router@4.1.6(vue@3.3.4))(vue@3.3.4) @@ -113,6 +116,9 @@ importers: json5: specifier: ^2.2.3 version: 2.2.3 + jszip: + specifier: ^3.10.1 + version: 3.10.1 jwt-decode: specifier: ^3.1.2 version: 3.1.2 @@ -2303,9 +2309,6 @@ packages: '@vue/compiler-core@3.3.7': resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==} - '@vue/compiler-core@3.5.13': - resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} - '@vue/compiler-dom@3.2.47': resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==} @@ -2315,18 +2318,12 @@ packages: '@vue/compiler-dom@3.3.7': resolution: {integrity: sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==} - '@vue/compiler-dom@3.5.13': - resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} - '@vue/compiler-sfc@3.2.47': resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==} '@vue/compiler-sfc@3.3.4': resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} - '@vue/compiler-sfc@3.5.13': - resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} - '@vue/compiler-ssr@3.2.47': resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==} @@ -2336,9 +2333,6 @@ packages: '@vue/compiler-ssr@3.3.7': resolution: {integrity: sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==} - '@vue/compiler-ssr@3.5.13': - resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} - '@vue/devtools-api@6.5.0': resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==} @@ -2359,21 +2353,12 @@ packages: '@vue/reactivity@3.3.4': resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} - '@vue/reactivity@3.5.13': - resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} - '@vue/runtime-core@3.3.4': resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} - '@vue/runtime-core@3.5.13': - resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} - '@vue/runtime-dom@3.3.4': resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} - '@vue/runtime-dom@3.5.13': - resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} - '@vue/server-renderer@3.3.4': resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} peerDependencies: @@ -2384,11 +2369,6 @@ packages: peerDependencies: vue: 3.3.7 - '@vue/server-renderer@3.5.13': - resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} - peerDependencies: - vue: 3.5.13 - '@vue/shared@3.2.47': resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==} @@ -2398,9 +2378,6 @@ packages: '@vue/shared@3.3.7': resolution: {integrity: sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==} - '@vue/shared@3.5.13': - resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} - '@vue/test-utils@2.3.2': resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==} peerDependencies: @@ -2428,15 +2405,9 @@ packages: peerDependencies: vue-router: '>=4.0.0-rc.1' - '@vueuse/shared@10.0.0': - resolution: {integrity: sha512-Zh3LgJqvUBWVY3SiMvXanTcfAneGbt63QPczBRDNgQ6jd/ehodO9a1lCFzaA6SWJJoI+ugVTjHFYJdoR656DVQ==} - '@vueuse/shared@10.3.0': resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} - '@vueuse/shared@12.0.0': - resolution: {integrity: sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==} - '@zhead/schema@1.0.0-beta.13': resolution: {integrity: sha512-P1A1vRGFBhITco8Iw4/hvnDYoE/SoVrd71dW1pBFdXJb3vP+pBtoOuhbEKy0ROJGOyzQuqvFibcwzyLlWMqNiQ==} @@ -2831,6 +2802,9 @@ packages: core-js-compat@3.39.0: resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + country-code-lookup@0.1.0: resolution: {integrity: sha512-IOI66HEG+8bXfWPy+sTzuN7161vmDZOHg1wgIPFf3WfD73FeLajnn6C+fnxOIa9RL1WRBDMXQQWW/FOaOYaQ3w==} @@ -2896,9 +2870,6 @@ packages: csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - dash-get@1.0.2: resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} @@ -3246,6 +3217,7 @@ packages: eslint-plugin-i@2.28.0-2: resolution: {integrity: sha512-z48kG4qmE4TmiLcxbmvxMT5ycwvPkXaWW0XpU1L768uZaTbiDbxsHMEdV24JHlOR1xDsPpKW39BfP/pRdYIwFA==} engines: {node: '>=12'} + deprecated: Please migrate to the brand new `eslint-plugin-import-x` instead peerDependencies: eslint: ^7.2.0 || ^8 @@ -3649,10 +3621,6 @@ packages: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} - hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} - engines: {node: '>= 0.4'} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -3737,6 +3705,9 @@ packages: engines: {node: '>=0.10.0'} hasBin: true + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -3974,6 +3945,9 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -4096,6 +4070,9 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jwt-decode@3.1.2: resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} @@ -4122,6 +4099,9 @@ packages: libphonenumber-js@1.10.28: resolution: {integrity: sha512-1eAgjLrZA0+2Wgw4hs+4Q/kEBycxQo8ZLYnmOvZ3AlM8ImAVAJgDPlZtISLEzD1vunc2q8s2Pn7XwB7I8U3Kzw==} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -4498,6 +4478,9 @@ packages: package-manager-detector@0.2.7: resolution: {integrity: sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + param-case@2.1.1: resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} @@ -4665,6 +4648,9 @@ packages: resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + prosemirror-changeset@2.2.1: resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} @@ -4788,6 +4774,9 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -4868,10 +4857,6 @@ packages: resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} hasBin: true - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - resolve@1.22.9: resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==} hasBin: true @@ -4919,6 +4904,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -4996,6 +4984,9 @@ packages: resolution: {integrity: sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw==} engines: {node: '>=11.0'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5071,6 +5062,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -5132,6 +5124,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -5701,14 +5696,6 @@ packages: vue@3.3.4: resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} - vue@3.5.13: - resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - vuedraggable@4.1.0: resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==} peerDependencies: @@ -6117,7 +6104,7 @@ snapshots: '@babel/traverse': 7.23.2 '@babel/types': 7.23.0 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -7000,7 +6987,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.10 '@babel/types': 7.22.10 - debug: 4.3.4 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7015,7 +7002,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.5 '@babel/parser': 7.22.5 '@babel/types': 7.22.5 - debug: 4.3.4 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7030,7 +7017,7 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.23.0 '@babel/types': 7.23.0 - debug: 4.3.4 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7228,7 +7215,7 @@ snapshots: '@eslint/eslintrc@2.1.2': dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.4.0 espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 @@ -7244,7 +7231,7 @@ snapshots: '@humanwhocodes/config-array@0.11.10': dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7401,7 +7388,7 @@ snapshots: '@linaria/logger@4.0.0': dependencies: - debug: 4.3.4 + debug: 4.4.0 picocolors: 1.0.0 transitivePeerDependencies: - supports-color @@ -7922,7 +7909,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2) '@typescript-eslint/utils': 6.4.1(eslint@8.47.0)(typescript@5.2.2) - debug: 4.3.4 + debug: 4.4.0 eslint: 8.47.0 ts-api-utils: 1.0.1(typescript@5.2.2) optionalDependencies: @@ -7940,10 +7927,10 @@ snapshots: dependencies: '@typescript-eslint/types': 5.60.0 '@typescript-eslint/visitor-keys': 5.60.0 - debug: 4.3.4 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.2.2) optionalDependencies: typescript: 5.2.2 @@ -7954,10 +7941,10 @@ snapshots: dependencies: '@typescript-eslint/types': 6.4.1 '@typescript-eslint/visitor-keys': 6.4.1 - debug: 4.3.4 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 ts-api-utils: 1.0.1(typescript@5.2.2) optionalDependencies: typescript: 5.2.2 @@ -7968,10 +7955,10 @@ snapshots: dependencies: '@typescript-eslint/types': 6.9.1 '@typescript-eslint/visitor-keys': 6.9.1 - debug: 4.3.4 + debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.4 + semver: 7.6.3 ts-api-utils: 1.0.1(typescript@5.2.2) optionalDependencies: typescript: 5.2.2 @@ -7988,7 +7975,7 @@ snapshots: '@typescript-eslint/typescript-estree': 5.60.0(typescript@5.2.2) eslint: 8.47.0 eslint-scope: 5.1.1 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -8002,7 +7989,7 @@ snapshots: '@typescript-eslint/types': 6.4.1 '@typescript-eslint/typescript-estree': 6.4.1(typescript@5.2.2) eslint: 8.47.0 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -8016,7 +8003,7 @@ snapshots: '@typescript-eslint/types': 6.9.1 '@typescript-eslint/typescript-estree': 6.9.1(typescript@5.2.2) eslint: 8.47.0 - semver: 7.5.4 + semver: 7.6.3 transitivePeerDependencies: - supports-color - typescript @@ -8049,15 +8036,15 @@ snapshots: dependencies: '@unhead/schema': 0.5.1 - '@unhead/vue@0.5.1(typescript@5.2.2)(vue@3.3.4)': + '@unhead/vue@0.5.1(vue@3.3.4)': dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 12.0.0(typescript@5.2.2) + '@vueuse/shared': 10.3.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: - - typescript + - '@vue/composition-api' '@unocss/astro@0.65.1(rollup@2.79.2)(vite@4.4.9(@types/node@18.15.11)(less@4.1.3)(terser@5.37.0))(vue@3.3.4)': dependencies: @@ -8341,14 +8328,6 @@ snapshots: source-map-js: 1.0.2 optional: true - '@vue/compiler-core@3.5.13': - dependencies: - '@babel/parser': 7.26.3 - '@vue/shared': 3.5.13 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - '@vue/compiler-dom@3.2.47': dependencies: '@vue/compiler-core': 3.2.47 @@ -8365,11 +8344,6 @@ snapshots: '@vue/shared': 3.3.7 optional: true - '@vue/compiler-dom@3.5.13': - dependencies: - '@vue/compiler-core': 3.5.13 - '@vue/shared': 3.5.13 - '@vue/compiler-sfc@3.2.47': dependencies: '@babel/parser': 7.21.4 @@ -8396,18 +8370,6 @@ snapshots: postcss: 8.4.28 source-map-js: 1.0.2 - '@vue/compiler-sfc@3.5.13': - dependencies: - '@babel/parser': 7.26.3 - '@vue/compiler-core': 3.5.13 - '@vue/compiler-dom': 3.5.13 - '@vue/compiler-ssr': 3.5.13 - '@vue/shared': 3.5.13 - estree-walker: 2.0.2 - magic-string: 0.30.15 - postcss: 8.4.49 - source-map-js: 1.2.1 - '@vue/compiler-ssr@3.2.47': dependencies: '@vue/compiler-dom': 3.2.47 @@ -8424,11 +8386,6 @@ snapshots: '@vue/shared': 3.3.7 optional: true - '@vue/compiler-ssr@3.5.13': - dependencies: - '@vue/compiler-dom': 3.5.13 - '@vue/shared': 3.5.13 - '@vue/devtools-api@6.5.0': {} '@vue/language-core@1.8.1(typescript@5.2.2)': @@ -8464,33 +8421,17 @@ snapshots: dependencies: '@vue/shared': 3.3.4 - '@vue/reactivity@3.5.13': - dependencies: - '@vue/shared': 3.5.13 - '@vue/runtime-core@3.3.4': dependencies: '@vue/reactivity': 3.3.4 '@vue/shared': 3.3.4 - '@vue/runtime-core@3.5.13': - dependencies: - '@vue/reactivity': 3.5.13 - '@vue/shared': 3.5.13 - '@vue/runtime-dom@3.3.4': dependencies: '@vue/runtime-core': 3.3.4 '@vue/shared': 3.3.4 csstype: 3.1.2 - '@vue/runtime-dom@3.5.13': - dependencies: - '@vue/reactivity': 3.5.13 - '@vue/runtime-core': 3.5.13 - '@vue/shared': 3.5.13 - csstype: 3.1.3 - '@vue/server-renderer@3.3.4(vue@3.3.4)': dependencies: '@vue/compiler-ssr': 3.3.4 @@ -8504,12 +8445,6 @@ snapshots: vue: 3.3.4 optional: true - '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.2.2))': - dependencies: - '@vue/compiler-ssr': 3.5.13 - '@vue/shared': 3.5.13 - vue: 3.5.13(typescript@5.2.2) - '@vue/shared@3.2.47': {} '@vue/shared@3.3.4': {} @@ -8517,8 +8452,6 @@ snapshots: '@vue/shared@3.3.7': optional: true - '@vue/shared@3.5.13': {} - '@vue/test-utils@2.3.2(vue@3.3.4)': dependencies: js-beautify: 1.14.6 @@ -8546,33 +8479,26 @@ snapshots: - '@vue/composition-api' - vue - '@vueuse/head@1.0.0(typescript@5.2.2)(vue@3.3.4)': + '@vueuse/head@1.0.0(vue@3.3.4)': dependencies: '@unhead/schema': 0.5.1 '@unhead/ssr': 0.5.1 - '@unhead/vue': 0.5.1(typescript@5.2.2)(vue@3.3.4) + '@unhead/vue': 0.5.1(vue@3.3.4) vue: 3.3.4 transitivePeerDependencies: - - typescript + - '@vue/composition-api' '@vueuse/metadata@10.3.0': {} '@vueuse/router@10.0.0(vue-router@4.1.6(vue@3.3.4))(vue@3.3.4)': dependencies: - '@vueuse/shared': 10.0.0(vue@3.3.4) + '@vueuse/shared': 10.3.0(vue@3.3.4) vue-demi: 0.14.5(vue@3.3.4) vue-router: 4.1.6(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue - '@vueuse/shared@10.0.0(vue@3.3.4)': - dependencies: - vue-demi: 0.14.5(vue@3.3.4) - transitivePeerDependencies: - - '@vue/composition-api' - - vue - '@vueuse/shared@10.3.0(vue@3.3.4)': dependencies: vue-demi: 0.14.5(vue@3.3.4) @@ -8580,12 +8506,6 @@ snapshots: - '@vue/composition-api' - vue - '@vueuse/shared@12.0.0(typescript@5.2.2)': - dependencies: - vue: 3.5.13(typescript@5.2.2) - transitivePeerDependencies: - - typescript - '@zhead/schema@1.0.0-beta.13': {} abab@2.0.6: {} @@ -8608,7 +8528,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.4 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -9022,6 +8942,8 @@ snapshots: dependencies: browserslist: 4.24.3 + core-util-is@1.0.3: {} + country-code-lookup@0.1.0: {} crelt@1.0.6: {} @@ -9084,8 +9006,6 @@ snapshots: csstype@3.1.2: {} - csstype@3.1.3: {} - dash-get@1.0.2: {} data-uri-to-buffer@4.0.1: {} @@ -9971,10 +9891,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hasown@2.0.0: - dependencies: - function-bind: 1.1.2 - hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -10016,14 +9932,14 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.4.0 transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.4.0 transitivePeerDependencies: - supports-color @@ -10071,6 +9987,8 @@ snapshots: image-size@0.5.5: optional: true + immediate@3.0.6: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -10162,7 +10080,7 @@ snapshots: is-core-module@2.13.1: dependencies: - hasown: 2.0.0 + hasown: 2.0.2 is-core-module@2.16.0: dependencies: @@ -10293,6 +10211,8 @@ snapshots: dependencies: is-docker: 2.2.1 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -10412,6 +10332,13 @@ snapshots: jsonpointer@5.0.1: {} + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + jwt-decode@3.1.2: {} kind-of@6.0.3: {} @@ -10443,6 +10370,10 @@ snapshots: libphonenumber-js@1.10.28: {} + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lines-and-columns@1.2.4: {} linkify-it@4.0.1: @@ -10753,7 +10684,7 @@ snapshots: normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.8 + resolve: 1.22.9 semver: 5.7.2 validate-npm-package-license: 3.0.4 @@ -10857,6 +10788,8 @@ snapshots: package-manager-detector@0.2.7: {} + pako@1.0.11: {} + param-case@2.1.1: dependencies: no-case: 2.3.2 @@ -11017,6 +10950,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.2.0 + process-nextick-args@2.0.1: {} + prosemirror-changeset@2.2.1: dependencies: prosemirror-transform: 1.7.3 @@ -11184,6 +11119,16 @@ snapshots: parse-json: 5.2.0 type-fest: 0.6.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -11271,12 +11216,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.8: - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.9: dependencies: is-core-module: 2.16.0 @@ -11322,6 +11261,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-regex-test@1.1.0: @@ -11401,6 +11342,8 @@ snapshots: is-plain-object: 2.0.4 is-primitive: 3.0.1 + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -11565,6 +11508,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -12018,7 +11965,7 @@ snapshots: vite-node@0.34.0(@types/node@18.15.11)(less@4.1.3)(terser@5.37.0): dependencies: cac: 6.7.14 - debug: 4.3.4 + debug: 4.4.0 mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 @@ -12180,16 +12127,6 @@ snapshots: '@vue/server-renderer': 3.3.4(vue@3.3.4) '@vue/shared': 3.3.4 - vue@3.5.13(typescript@5.2.2): - dependencies: - '@vue/compiler-dom': 3.5.13 - '@vue/compiler-sfc': 3.5.13 - '@vue/runtime-dom': 3.5.13 - '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.2.2)) - '@vue/shared': 3.5.13 - optionalDependencies: - typescript: 5.2.2 - vuedraggable@4.1.0(vue@3.3.4): dependencies: sortablejs: 1.14.0 diff --git a/src/tools/favicon-generator/favicon-generator.e2e.spec.ts b/src/tools/favicon-generator/favicon-generator.e2e.spec.ts new file mode 100644 index 0000000000..848f041211 --- /dev/null +++ b/src/tools/favicon-generator/favicon-generator.e2e.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Tool - Favicon generator', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/favicon-generator'); + }); + + test('Has correct title', async ({ page }) => { + await expect(page).toHaveTitle('Favicon generator - IT Tools'); + }); + + test('', async () => { + + }); +}); diff --git a/src/tools/favicon-generator/favicon-generator.service.test.ts b/src/tools/favicon-generator/favicon-generator.service.test.ts new file mode 100644 index 0000000000..b7bfa8e26c --- /dev/null +++ b/src/tools/favicon-generator/favicon-generator.service.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; +import { FAVICON_SIZES, buildIcoBuffer, generateSiteWebmanifest } from './favicon-generator.service'; + +describe('generateSiteWebmanifest', () => { + it('produces valid JSON', () => { + const result = generateSiteWebmanifest(); + expect(() => JSON.parse(result)).not.toThrow(); + }); + + it('uses the provided app name', () => { + const manifest = JSON.parse(generateSiteWebmanifest('Acme')); + expect(manifest.name).toBe('Acme'); + expect(manifest.short_name).toBe('Acme'); + }); + + it('includes 192x192 and 512x512 icon entries', () => { + const manifest = JSON.parse(generateSiteWebmanifest()); + const sizes = manifest.icons.map((i: { sizes: string }) => i.sizes); + expect(sizes).toContain('192x192'); + expect(sizes).toContain('512x512'); + }); + + it('sets display to standalone', () => { + const manifest = JSON.parse(generateSiteWebmanifest()); + expect(manifest.display).toBe('standalone'); + }); +}); + +function fakeBuffer(size: number): ArrayBuffer { + return new Uint8Array(size).buffer; +} + +describe('buildIcoBuffer', () => { + it('starts with ICO magic bytes (reserved=0, type=1)', () => { + const buf = buildIcoBuffer([fakeBuffer(4), fakeBuffer(4)], [16, 32]); + const view = new DataView(buf); + expect(view.getUint16(0, true)).toBe(0); + expect(view.getUint16(2, true)).toBe(1); + }); + + it('writes correct image count', () => { + const buf = buildIcoBuffer([fakeBuffer(4), fakeBuffer(4), fakeBuffer(4)], [16, 32, 48]); + const view = new DataView(buf); + expect(view.getUint16(4, true)).toBe(3); + }); + + it('embeds png data after directory entries', () => { + const pngData = new Uint8Array([1, 2, 3, 4]); + const buf = buildIcoBuffer([pngData.buffer], [16]); + const view = new DataView(buf); + const dataOffset = view.getUint32(6 + 12, true); + const embedded = new Uint8Array(buf, dataOffset, 4); + expect(Array.from(embedded)).toEqual([1, 2, 3, 4]); + }); + + it('records correct byte size in directory entry', () => { + const buf = buildIcoBuffer([fakeBuffer(100)], [32]); + const view = new DataView(buf); + expect(view.getUint32(6 + 8, true)).toBe(100); + }); + + it('total size equals header + dir entries + all png data', () => { + const sizes = [16, 32, 48]; + const pngBuffers = [fakeBuffer(10), fakeBuffer(20), fakeBuffer(30)]; + const buf = buildIcoBuffer(pngBuffers, sizes); + const expected = 6 + 16 * 3 + 10 + 20 + 30; + expect(buf.byteLength).toBe(expected); + }); +}); + +describe('FAVICON_SIZES', () => { + it('includes all required sizes', () => { + const sizes = FAVICON_SIZES.map(s => s.size); + expect(sizes).toContain(16); + expect(sizes).toContain(32); + expect(sizes).toContain(48); + expect(sizes).toContain(180); + expect(sizes).toContain(192); + expect(sizes).toContain(512); + }); + + it('maps 180 to apple-touch-icon.png', () => { + const entry = FAVICON_SIZES.find(s => s.size === 180); + expect(entry?.filename).toBe('apple-touch-icon.png'); + }); + + it('maps 16 to favicon-16x16.png', () => { + const entry = FAVICON_SIZES.find(s => s.size === 16); + expect(entry?.filename).toBe('favicon-16x16.png'); + }); + + it('maps 32 to favicon-32x32.png', () => { + const entry = FAVICON_SIZES.find(s => s.size === 32); + expect(entry?.filename).toBe('favicon-32x32.png'); + }); +}); diff --git a/src/tools/favicon-generator/favicon-generator.service.ts b/src/tools/favicon-generator/favicon-generator.service.ts new file mode 100644 index 0000000000..59a2262898 --- /dev/null +++ b/src/tools/favicon-generator/favicon-generator.service.ts @@ -0,0 +1,132 @@ +import JSZip from 'jszip'; + +export interface FaviconSize { + filename: string + size: number +} + +export const FAVICON_SIZES: FaviconSize[] = [ + { filename: 'favicon-16x16.png', size: 16 }, + { filename: 'favicon-32x32.png', size: 32 }, + { filename: 'favicon-48x48.png', size: 48 }, + { filename: 'apple-touch-icon.png', size: 180 }, + { filename: 'android-chrome-192x192.png', size: 192 }, + { filename: 'android-chrome-512x512.png', size: 512 }, +]; + +export function generateSiteWebmanifest(appName: string = 'My App'): string { + return JSON.stringify( + { + name: appName, + short_name: appName, + icons: [ + { src: '/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' }, + { src: '/android-chrome-512x512.png', sizes: '512x512', type: 'image/png' }, + ], + theme_color: '#ffffff', + background_color: '#ffffff', + display: 'standalone', + }, + null, + 2, + ); +} + +export async function resizeImageToBlob(img: HTMLImageElement, size: number): Promise { + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Could not get 2D context'); + } + ctx.drawImage(img, 0, 0, size, size); + return new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) { + resolve(blob); + } + else { + reject(new Error(`Failed to create blob for size ${size}`)); + } + }, 'image/png'); + }); +} + +const ICO_SIZES = [16, 32, 48]; + +export function buildIcoBuffer(pngBuffers: ArrayBuffer[], sizes: number[]): ArrayBuffer { + const headerSize = 6; + const dirEntrySize = 16; + const totalHeaderSize = headerSize + dirEntrySize * sizes.length; + + let offset = totalHeaderSize; + const offsets = pngBuffers.map((buf) => { + const o = offset; + offset += buf.byteLength; + return o; + }); + + const buffer = new ArrayBuffer(offset); + const view = new DataView(buffer); + + view.setUint16(0, 0, true); + view.setUint16(2, 1, true); + view.setUint16(4, sizes.length, true); + + sizes.forEach((size, i) => { + const base = headerSize + i * dirEntrySize; + view.setUint8(base, size); + view.setUint8(base + 1, size); + view.setUint8(base + 2, 0); + view.setUint8(base + 3, 0); + view.setUint16(base + 4, 1, true); + view.setUint16(base + 6, 32, true); + view.setUint32(base + 8, pngBuffers[i].byteLength, true); + view.setUint32(base + 12, offsets[i], true); + }); + + const uint8 = new Uint8Array(buffer); + pngBuffers.forEach((pngBuf, i) => uint8.set(new Uint8Array(pngBuf), offsets[i])); + + return buffer; +} + +export async function generateIcoBlob(img: HTMLImageElement): Promise { + const pngBlobs = await Promise.all(ICO_SIZES.map(s => resizeImageToBlob(img, s))); + const pngBuffers = await Promise.all(pngBlobs.map(b => b.arrayBuffer())); + return new Blob([buildIcoBuffer(pngBuffers, ICO_SIZES)], { type: 'image/x-icon' }); +} + +export async function loadImageFromFile(file: File): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + const url = URL.createObjectURL(file); + img.onload = () => { + URL.revokeObjectURL(url); + resolve(img); + }; + img.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error('Failed to load image')); + }; + img.src = url; + }); +} + +export async function generateFaviconZip(file: File): Promise { + const img = await loadImageFromFile(file); + const zip = new JSZip(); + + for (const { filename, size } of FAVICON_SIZES) { + const blob = await resizeImageToBlob(img, size); + zip.file(filename, blob); + } + + const icoBlob = await generateIcoBlob(img); + zip.file('favicon.ico', icoBlob); + + zip.file('site.webmanifest', generateSiteWebmanifest()); + + return zip.generateAsync({ type: 'blob' }); +} diff --git a/src/tools/favicon-generator/favicon-generator.vue b/src/tools/favicon-generator/favicon-generator.vue new file mode 100644 index 0000000000..48602eacf7 --- /dev/null +++ b/src/tools/favicon-generator/favicon-generator.vue @@ -0,0 +1,84 @@ + + + diff --git a/src/tools/favicon-generator/index.ts b/src/tools/favicon-generator/index.ts new file mode 100644 index 0000000000..f3b170622d --- /dev/null +++ b/src/tools/favicon-generator/index.ts @@ -0,0 +1,12 @@ +import { Photo } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Favicon generator', + path: '/favicon-generator', + description: 'Convert any image into a complete favicon package. Upload a logo or square image — get a ZIP with favicon.ico, 16×16, 32×32, 48×48, 180×180, 192×192, and 512×512 PNGs plus site.webmanifest. Everything runs in your browser.', + keywords: ['favicon', 'generator', 'icon', 'apple-touch-icon', 'webmanifest', 'png', 'image', 'convert'], + component: () => import('./favicon-generator.vue'), + icon: Photo, + createdAt: new Date('2026-06-09'), +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index 388cfaf494..cc9cb27865 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; +import { tool as faviconGenerator } from './favicon-generator'; import { tool as emailNormalizer } from './email-normalizer'; import { tool as asciiTextDrawer } from './ascii-text-drawer'; @@ -141,7 +142,7 @@ export const toolsByCategory: ToolCategory[] = [ }, { name: 'Images and videos', - components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], + components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder, faviconGenerator], }, { name: 'Development',