diff --git a/package-lock.json b/package-lock.json index 9302e9d..fd830d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,15 @@ "zod": "^3.23.8" }, "devDependencies": { + "@iconify-json/bx": "^1.1.10", + "@iconify-json/heroicons": "^1.1.22", + "@iconify-json/ic": "^1.1.17", + "@iconify-json/material-symbols": "^1.1.85", + "@iconify-json/mi": "^1.1.8", + "@iconify-json/mingcute": "^1.1.18", + "@iconify-json/ph": "^1.1.13", + "@iconify-json/solar": "^1.1.9", + "@iconify-json/teenyicons": "^1.1.9", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", @@ -34,6 +43,7 @@ "tailwindcss": "^3.4.1", "tslib": "^2.4.1", "typescript": "^5.0.0", + "unplugin-icons": "^0.19.0", "vite": "^5.0.3" } }, @@ -62,6 +72,27 @@ "node": ">=6.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.3.3.tgz", + "integrity": "sha512-nHHsk3NXQ6xkCfiRRC8Nfrg8pU5kkr3P3Y9s9dKqiuRmBD0Yap7fymNDjGFKeWhZQHqqbCS5CfeMy9wtExM24w==", + "dev": true, + "dependencies": { + "@jsdevtools/ez-spawn": "^3.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -439,6 +470,121 @@ "node": ">=6" } }, + "node_modules/@iconify-json/bx": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@iconify-json/bx/-/bx-1.1.10.tgz", + "integrity": "sha512-4JzMDYhs/hkU9mO8nNMNKZwHn706oQaD46URyUuD4fP/XAWGf01vg6wKITmQseEHsHDrtKAXjwcumfnTUDi3OQ==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/heroicons": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/@iconify-json/heroicons/-/heroicons-1.1.22.tgz", + "integrity": "sha512-UNfSBdD/JBYBvFFhce6e3FsmeqshGz8u964R36npJvIzuORhIHUXrFerulBWFmbG0V1xvMqLZVZc4bPyjP5p7A==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/ic": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@iconify-json/ic/-/ic-1.1.17.tgz", + "integrity": "sha512-EvAjZzVESmN36zlyefylePUNaU2BQ3eRKVZ6KQSQ2bG01ppoZaiFZRri74VTyvp5Mlv2yn68ux1fgCoT+etGmA==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/material-symbols": { + "version": "1.1.85", + "resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.1.85.tgz", + "integrity": "sha512-GJXTScAIdaxxMPcp6GCd4qbntvHpG9UrF/2V03PMUuD7+1fMU5vHG5w0IGDdvqOnI9HpEcUFa7CFDVQHOpBeDA==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/mi": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@iconify-json/mi/-/mi-1.1.8.tgz", + "integrity": "sha512-DGoHbf4nkyrHNOAbbNncK1hiiYo/hk/IpdEGZ+eZJpOfWV2VypeaTaQt69pldDgQU+QDyc/GgmO4c0yIwQRfnQ==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/mingcute": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@iconify-json/mingcute/-/mingcute-1.1.18.tgz", + "integrity": "sha512-P5szgBqUMv8XVjZpbVT5eG1XtqpRPgfvZWxOR/uSqBwmVroHpxN5+nbLTHj9HJxL16msMsNbA294b69MNEQzaw==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/ph": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@iconify-json/ph/-/ph-1.1.13.tgz", + "integrity": "sha512-xtM4JJ63HCKj09WRqrBswXiHrpliBlqboWSZH8odcmqYXbvIFceU9/Til4V+MQr6+MoUC+KB72cxhky2+A6r/g==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/solar": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@iconify-json/solar/-/solar-1.1.9.tgz", + "integrity": "sha512-BcWzZqA02BiQduYizqU/J4v4RNs0MkjZUGpMbejpozH8YQSt3+S/LfV6zfVRonx/2DhXTVSqiLa1abDRAZtojQ==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/teenyicons": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@iconify-json/teenyicons/-/teenyicons-1.1.9.tgz", + "integrity": "sha512-8QsYNkeaKkUQensRE2OyTpjBmUTcHerwwTdo82Lsxh0t1g0tZVfqjuoevF5KwDFCbfObK49gdfiuyzGcmuliVw==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, + "node_modules/@iconify/utils": { + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.25.tgz", + "integrity": "sha512-Y+iGko8uv/Fz5bQLLJyNSZGOdMW0G7cnlEX1CiNcKsRXX9cq/y/vwxrIAtLCZhKHr3m0VJmsjVPsvnM4uX8YLg==", + "dev": true, + "dependencies": { + "@antfu/install-pkg": "^0.1.1", + "@antfu/utils": "^0.7.7", + "@iconify/types": "^2.0.0", + "debug": "^4.3.4", + "kolorist": "^1.8.0", + "local-pkg": "^0.5.0", + "mlly": "^1.6.1" + } + }, + "node_modules/@iconify/utils/node_modules/@antfu/install-pkg": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.1.1.tgz", + "integrity": "sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==", + "dev": true, + "dependencies": { + "execa": "^5.1.1", + "find-up": "^5.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -504,6 +650,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ez-spawn": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@jsdevtools/ez-spawn/-/ez-spawn-3.0.4.tgz", + "integrity": "sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==", + "dev": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "cross-spawn": "^7.0.3", + "string-argv": "^0.3.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1205,6 +1366,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1313,6 +1480,12 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "dev": true + }, "node_modules/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", @@ -1596,6 +1769,35 @@ "@types/estree": "^1.0.0" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -1659,6 +1861,22 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -1774,6 +1992,18 @@ "node": ">=8.0.0" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/getopts": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", @@ -2008,6 +2238,15 @@ "node": ">= 14" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2307,6 +2546,12 @@ } } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true + }, "node_modules/ky": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ky/-/ky-1.4.0.tgz", @@ -2333,12 +2578,43 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "dev": true }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2403,6 +2679,12 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2425,6 +2707,15 @@ "node": ">=8.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -2491,6 +2782,18 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, + "node_modules/mlly": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -2602,6 +2905,18 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2639,12 +2954,66 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", "dev": true }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2684,6 +3053,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, "node_modules/periscopic": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", @@ -2736,6 +3111,17 @@ "node": ">= 6" } }, + "node_modules/pkg-types": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", + "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", + "dev": true, + "dependencies": { + "confbox": "^0.1.7", + "mlly": "^1.7.1", + "pathe": "^1.1.2" + } + }, "node_modules/postcss": { "version": "8.4.39", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", @@ -3431,6 +3817,15 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3527,6 +3922,15 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -3926,6 +4330,15 @@ "node": "*" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/typescript": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", @@ -3939,11 +4352,74 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/unplugin": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.11.0.tgz", + "integrity": "sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "chokidar": "^3.6.0", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.6.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-icons": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.19.0.tgz", + "integrity": "sha512-u5g/gIZPZEj1wUGEQxe9nzftOSqmblhusc+sL3cawIRoIt/xWpE6XYcPOfAeFTYNjSbRrX/3QiX89PFiazgU1w==", + "dev": true, + "dependencies": { + "@antfu/install-pkg": "^0.3.3", + "@antfu/utils": "^0.7.7", + "@iconify/utils": "^2.1.23", + "debug": "^4.3.4", + "kolorist": "^1.8.0", + "local-pkg": "^0.5.0", + "unplugin": "^1.10.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@svgr/core": ">=7.0.0", + "@svgx/core": "^1.0.1", + "@vue/compiler-sfc": "^3.0.2 || ^2.7.0", + "vue-template-compiler": "^2.6.12", + "vue-template-es2015-compiler": "^1.9.0" + }, + "peerDependenciesMeta": { + "@svgr/core": { + "optional": true + }, + "@svgx/core": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + }, + "vue-template-es2015-compiler": { + "optional": true + } + } + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -4070,6 +4546,21 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -4202,6 +4693,18 @@ "node": ">= 14" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", diff --git a/package.json b/package.json index 6ea026d..c15a2be 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,15 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, "devDependencies": { + "@iconify-json/bx": "^1.1.10", + "@iconify-json/heroicons": "^1.1.22", + "@iconify-json/ic": "^1.1.17", + "@iconify-json/material-symbols": "^1.1.85", + "@iconify-json/mi": "^1.1.8", + "@iconify-json/mingcute": "^1.1.18", + "@iconify-json/ph": "^1.1.13", + "@iconify-json/solar": "^1.1.9", + "@iconify-json/teenyicons": "^1.1.9", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", @@ -23,6 +32,7 @@ "tailwindcss": "^3.4.1", "tslib": "^2.4.1", "typescript": "^5.0.0", + "unplugin-icons": "^0.19.0", "vite": "^5.0.3" }, "type": "module", diff --git a/src/app.d.ts b/src/app.d.ts index a4a5b77..da02173 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,3 +1,5 @@ +import 'unplugin-icons/types/svelte4.d.ts' + // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 3d0e76e..63d13e2 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,6 +1,6 @@ import { redirect, type Handle, type RequestEvent } from '@sveltejs/kit' import { SECRET_INTERNAL_API_KEY, SECRET_JWT_KEY } from '$env/static/private' -import { userExists, mixExists } from '$lib/server/api-helper' +import { userExists, connectionExists, mixExists } from '$lib/server/api-helper' import jwt from 'jsonwebtoken' function verifyAuthToken(event: RequestEvent) { @@ -23,6 +23,9 @@ const handleAPIRequest: Handle = async ({ event, resolve }) => { const userId = event.params.userId if (userId && !(await userExists(userId))) return new Response(`User ${userId} not found`, { status: 404 }) + const connectionId = event.params.connectionId + if (connectionId && !(await connectionExists(connectionId))) return new Response(`Connection ${connectionId} not found`, { status: 404 }) + const mixId = event.params.mixId if (mixId && !(await mixExists(mixId))) return new Response(`Mix ${mixId} not found`, { status: 404 }) diff --git a/src/lib/api-helper.ts b/src/lib/api-helper.ts new file mode 100644 index 0000000..8e1d48c --- /dev/null +++ b/src/lib/api-helper.ts @@ -0,0 +1,6 @@ +import ky from 'ky' + +export const apiV1 = ky.create({ + prefixUrl: '/api/v1/', + credentials: 'include', +}) diff --git a/src/lib/components/media/albumCard.svelte b/src/lib/components/media/albumCard.svelte index ac28e42..40d904d 100644 --- a/src/lib/components/media/albumCard.svelte +++ b/src/lib/components/media/albumCard.svelte @@ -1,39 +1,35 @@
-
+
@@ -52,9 +48,6 @@ #thumbnail-wrapper:hover > #play-button { opacity: 1; } - /* #connection-type-icon { - filter: grayscale(); - } */ #thumbnail-wrapper:hover > #connection-type-icon { opacity: 1; } diff --git a/src/lib/components/media/autoImage.svelte b/src/lib/components/media/autoImage.svelte new file mode 100644 index 0000000..b256f33 --- /dev/null +++ b/src/lib/components/media/autoImage.svelte @@ -0,0 +1,57 @@ + + + + +
+ (currentSlot = 1)} /> + (currentSlot = 2)} /> +
+ + diff --git a/src/lib/components/media/lazyImage.svelte b/src/lib/components/media/lazyImage.svelte deleted file mode 100644 index 6279e8f..0000000 --- a/src/lib/components/media/lazyImage.svelte +++ /dev/null @@ -1,70 +0,0 @@ - - - - -
diff --git a/src/lib/components/media/listItem.svelte b/src/lib/components/media/listItem.svelte index 79dbe6e..78cb904 100644 --- a/src/lib/components/media/listItem.svelte +++ b/src/lib/components/media/listItem.svelte @@ -1,5 +1,5 @@ -
-
+
+
- jacket +
650} class="absolute bottom-0 left-0 right-0 top-0 backdrop-brightness-50"> (paused = !paused)}> @@ -93,15 +96,15 @@
- {mediaItem.name} + {mediaItem.name}
- (favorite = !favorite)}> - + (favorite = !favorite)}> +
700 || playerWidth < 400} class="ml-auto whitespace-nowrap text-xs">{currentTimestamp} / {durationTimestamp} @@ -150,25 +153,36 @@ {#if playerWidth > 450}
{#if playerWidth > 1100} - dispatch('toggleShuffle')}> - + dispatch('toggleShuffle')}> + - (loop = !loop)}> - + (loop = !loop)}> + + + +
- (volume = volume > 0 ? 0 : getStoredVolume())}> - + (volume = volume > 0 ? 0 : getStoredVolume())}> + + {#if volume > MAX_VOLUME / 2} + + {:else if volume > 0} + + {:else} + + {/if} +
(volume > 0 ? localStorage.setItem('volume', volume.toString()) : null)} />
- - + + {/if} - +
@@ -189,3 +203,13 @@ on:error={() => setTimeout(() => audioElement.load(), 5000)} />
+ + diff --git a/src/lib/components/media/mediaPlayerOLD.svelte b/src/lib/components/media/mediaPlayerOLD.svelte index 588f7cf..1b6ab7c 100644 --- a/src/lib/components/media/mediaPlayerOLD.svelte +++ b/src/lib/components/media/mediaPlayerOLD.svelte @@ -4,9 +4,9 @@ import { queue } from '$lib/stores' import Services from '$lib/services.json' // import { FastAverageColor } from 'fast-average-color' + import AutoImage from './autoImage.svelte' import Slider from '$lib/components/util/slider.svelte' import Loader from '$lib/components/util/loader.svelte' - import LazyImage from './lazyImage.svelte' import IconButton from '$lib/components/util/iconButton.svelte' import ScrollingText from '$lib/components/util/scrollingText.svelte' import ArtistList from './artistList.svelte' @@ -109,7 +109,7 @@
- +
@@ -185,7 +185,7 @@ {:else}
- +
@@ -202,7 +202,7 @@ UP NEXT
- +
{next.name}
@@ -214,7 +214,7 @@ {/if}
- +
diff --git a/src/lib/components/media/playlistCard.svelte b/src/lib/components/media/playlistCard.svelte new file mode 100644 index 0000000..ee3b6b6 --- /dev/null +++ b/src/lib/components/media/playlistCard.svelte @@ -0,0 +1,62 @@ + + +
+
+ +
+ + + +
+
+ +
+
+
+
{playlist.name}
+
+ +
+
+
+ + diff --git a/src/lib/components/media/songCard.svelte b/src/lib/components/media/songCard.svelte new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/components/util/iconButton.svelte b/src/lib/components/util/iconButton.svelte index 11b7382..2ff35a1 100644 --- a/src/lib/components/util/iconButton.svelte +++ b/src/lib/components/util/iconButton.svelte @@ -1,7 +1,7 @@ {/each} diff --git a/src/lib/server/api-helper.ts b/src/lib/server/api-helper.ts index 4a94128..77be81e 100644 --- a/src/lib/server/api-helper.ts +++ b/src/lib/server/api-helper.ts @@ -6,7 +6,11 @@ export async function userExists(userId: string): Promise { return Boolean(await DB.users.where('id', userId).first(DB.db.raw('EXISTS(SELECT 1)'))) } -export async function mixExists(mixId: string): Promise { +export async function connectionExists(connectionId: string): Promise { + return Boolean(await DB.connections.where('id', connectionId).first(DB.db.raw('EXISTS(SELECT 1)'))) +} + +export async function mixExists(mixId: string): Promise { return Boolean(await DB.mixes.where('id', mixId).first(DB.db.raw('EXISTS(SELECT 1)'))) } diff --git a/src/lib/server/youtube-music.ts b/src/lib/server/youtube-music.ts index abd2f7b..3f34b23 100644 --- a/src/lib/server/youtube-music.ts +++ b/src/lib/server/youtube-music.ts @@ -570,6 +570,7 @@ export class YouTubeMusic implements Connection { */ public async getSongs(ids: Iterable): Promise { const uniqueIds = new Set(ids) + if (uniqueIds.size === 0) return [] const response = await this.api.v1.WEB_REMIX('music/get_queue', { json: { videoIds: Array.from(uniqueIds) } }).json() diff --git a/src/lib/utils.ts b/src/lib/utils.ts deleted file mode 100644 index 088a529..0000000 --- a/src/lib/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const generateUUID = (): string => { - return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c: any) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)) -} - -export const getDeviceUUID = (): string => { - const existingUUID = localStorage.getItem('deviceUUID') - if (existingUUID) return existingUUID - - const newUUID = generateUUID() - localStorage.setItem('deviceUUID', newUUID) - return newUUID -} diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index b9d3338..1f4248b 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -9,25 +9,32 @@ $: currentlyPlaying = $queue.current $: shuffled = $queue.isShuffled + + let playerWidth: number -
+
- +
+ +
+ {#if currentlyPlaying} +
+ 800 ? '0' : '0.5rem'} + on:stop={() => $queue.clear()} + on:next={() => $queue.next()} + on:previous={() => $queue.previous()} + on:toggleShuffle={() => $queue.toggleShuffle()} + /> +
+ {/if}
- {#if currentlyPlaying} - $queue.clear()} - on:next={() => $queue.next()} - on:previous={() => $queue.previous()} - on:toggleShuffle={() => $queue.toggleShuffle()} - /> - {/if}
diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 1a738fa..0d32c61 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -1,8 +1,8 @@ @@ -11,9 +11,13 @@ {#await data.recommendations} {:then recommendations} -
- {#each recommendations.filter((item) => item.type === 'album') as album} - +
+ {#each recommendations as mediaItem} + {#if mediaItem.type === 'album'} + + {:else if mediaItem.type === 'playlist'} + + {/if} {/each}
{/await} diff --git a/src/routes/(app)/mixes/+page.svelte b/src/routes/(app)/mixes/+page.svelte new file mode 100644 index 0000000..55539c0 --- /dev/null +++ b/src/routes/(app)/mixes/+page.svelte @@ -0,0 +1,14 @@ + + +
+
+
+ + + +
+
+
diff --git a/src/routes/(app)/user/+page.svelte b/src/routes/(app)/user/+page.svelte index 0d0e462..bf42875 100644 --- a/src/routes/(app)/user/+page.svelte +++ b/src/routes/(app)/user/+page.svelte @@ -7,7 +7,6 @@ import { newestAlert } from '$lib/stores.js' import type { PageServerData } from './$types.js' import type { SubmitFunction } from '@sveltejs/kit' - import { getDeviceUUID } from '$lib/utils' import { SvelteComponent, type ComponentType } from 'svelte' import ConnectionProfile from './connectionProfile.svelte' import { enhance } from '$app/forms' @@ -21,6 +20,15 @@ data.connections.then((userConnections) => ('error' in userConnections ? (errorMessage = userConnections.error) : (connections = userConnections))) + function getDeviceUUID(): string { + const existingUUID = localStorage.getItem('deviceUUID') + if (existingUUID) return existingUUID + + const newUUID = '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c: any) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)) + localStorage.setItem('deviceUUID', newUUID) + return newUUID + } + const authenticateJellyfin: SubmitFunction = ({ formData, cancel }) => { const { serverUrl, username, password } = Object.fromEntries(formData) diff --git a/src/routes/api/remoteImage/+server.ts b/src/routes/api/remoteImage/+server.ts index 7dc9193..2f89b02 100644 --- a/src/routes/api/remoteImage/+server.ts +++ b/src/routes/api/remoteImage/+server.ts @@ -64,12 +64,12 @@ function modifyImageURL(imageURL: URL, options?: { maxWidth?: number; maxHeight? switch (imageURL.origin) { case 'https://i.ytimg.com': case 'https://www.gstatic.com': - // These two origins correspond to images that can't have their size modified with search params, so we just return them at the default res + case 'https://music.youtube.com': + // These origins correspond to images that can't have their size modified with search params, so we just return them at the default res return baseURL case 'https://lh3.googleusercontent.com': case 'https://yt3.googleusercontent.com': case 'https://yt3.ggpht.com': - case 'https://music.youtube.com': const fakeQueryParams = [] if (maxWidth) fakeQueryParams.push(`w${Math.min(maxWidth, MAX_YOUTUBE_THUMBNAIL_SCALAR_SIZE)}`) if (maxHeight) fakeQueryParams.push(`h${Math.min(maxHeight, MAX_YOUTUBE_THUMBNAIL_SCALAR_SIZE)}`) diff --git a/src/routes/api/v1/connections/+server.ts b/src/routes/api/v1/connections/+server.ts new file mode 100644 index 0000000..3d4beb9 --- /dev/null +++ b/src/routes/api/v1/connections/+server.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from '@sveltejs/kit' +import { ConnectionFactory } from '$lib/server/api-helper' + +export const GET: RequestHandler = async ({ url }) => { + const ids = url.searchParams.get('id')?.replace(/\s/g, '').split(',') + if (!ids) return new Response('Missing id query parameter', { status: 400 }) + + const connections = (await Promise.all(ids.map((id) => ConnectionFactory.getConnection(id).catch(() => null)))).filter((result): result is Connection => result !== null) + + const getConnectionInfo = (connection: Connection) => + connection.getConnectionInfo().catch((reason) => { + console.error(`Failed to fetch connection info: ${reason}`) + return null + }) + + const connectionInfo = (await Promise.all(connections.map(getConnectionInfo))).filter((connectionInfo): connectionInfo is ConnectionInfo => connectionInfo !== null) + + return Response.json({ connections: connectionInfo }) +} diff --git a/src/routes/api/v1/connections/[connectionId]/album/+server.ts b/src/routes/api/v1/connections/[connectionId]/album/+server.ts new file mode 100644 index 0000000..cc3d077 --- /dev/null +++ b/src/routes/api/v1/connections/[connectionId]/album/+server.ts @@ -0,0 +1,14 @@ +import type { RequestHandler } from '@sveltejs/kit' +import { ConnectionFactory } from '$lib/server/api-helper' + +export const GET: RequestHandler = async ({ params, url }) => { + const connection = await ConnectionFactory.getConnection(params.connectionId!) + + const albumId = url.searchParams.get('id') + if (!albumId) return new Response(`Missing id search parameter`, { status: 400 }) + + const album = await connection.getAlbum(albumId).catch(() => undefined) + if (!album) return new Response(`Failed to fetch album with id: ${albumId}`, { status: 400 }) + + return Response.json({ album }) +} diff --git a/src/routes/api/v1/connections/[connectionId]/album/[albumId]/items/+server.ts b/src/routes/api/v1/connections/[connectionId]/album/[albumId]/items/+server.ts new file mode 100644 index 0000000..b4e9c13 --- /dev/null +++ b/src/routes/api/v1/connections/[connectionId]/album/[albumId]/items/+server.ts @@ -0,0 +1,11 @@ +import type { RequestHandler } from '@sveltejs/kit' +import { ConnectionFactory } from '$lib/server/api-helper' + +export const GET: RequestHandler = async ({ params }) => { + const connection = await ConnectionFactory.getConnection(params.connectionId!) + + const items = await connection.getAlbumItems(params.albumId!).catch(() => null) + if (!items) return new Response(`Failed to fetch album with id: ${params.albumId!}`, { status: 400 }) + + return Response.json({ items }) +} diff --git a/src/routes/api/v1/connections/[connectionId]/artist/+server.ts b/src/routes/api/v1/connections/[connectionId]/artist/+server.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/routes/api/v1/connections/[connectionId]/playlist/+server.ts b/src/routes/api/v1/connections/[connectionId]/playlist/+server.ts new file mode 100644 index 0000000..521a608 --- /dev/null +++ b/src/routes/api/v1/connections/[connectionId]/playlist/+server.ts @@ -0,0 +1,19 @@ +import type { RequestHandler } from '@sveltejs/kit' +import { ConnectionFactory } from '$lib/server/api-helper' + +export const GET: RequestHandler = async ({ params, url }) => { + const connection = await ConnectionFactory.getConnection(params.connectionId!) + + const playlistId = url.searchParams.get('id') + if (!playlistId) return new Response(`Missing id search parameter`, { status: 400 }) + + const response = await connection + .getPlaylistItems(playlistId) + .then((playlist) => Response.json({ playlist })) + .catch((error: TypeError | Error) => { + if (error instanceof TypeError) return new Response('Bad Request', { status: 400 }) + return new Response('Failed to fetch playlist items', { status: 502 }) + }) + + return response +} diff --git a/src/routes/api/v1/connections/[connectionId]/playlist/[playlistId]/items/+server.ts b/src/routes/api/v1/connections/[connectionId]/playlist/[playlistId]/items/+server.ts new file mode 100644 index 0000000..129a16a --- /dev/null +++ b/src/routes/api/v1/connections/[connectionId]/playlist/[playlistId]/items/+server.ts @@ -0,0 +1,25 @@ +import type { RequestHandler } from '@sveltejs/kit' +import { ConnectionFactory } from '$lib/server/api-helper' + +export const GET: RequestHandler = async ({ params, url }) => { + const connection = await ConnectionFactory.getConnection(params.connectionId!) + + const startIndexString = url.searchParams.get('startIndex') + const limitString = url.searchParams.get('limit') + + const numberStartIndex = Number(startIndexString) + const numberLimit = Number(limitString) + + const startIndex = Number.isInteger(numberStartIndex) && numberStartIndex > 0 ? numberStartIndex : undefined + const limit = Number.isInteger(numberLimit) && numberLimit > 0 ? numberLimit : undefined + + const response = await connection + .getPlaylistItems(params.playlistId!, { startIndex, limit }) + .then((items) => Response.json({ items })) + .catch((error: TypeError | Error) => { + if (error instanceof TypeError) return new Response('Bad Request', { status: 400 }) + return new Response('Failed to fetch playlist items', { status: 502 }) + }) + + return response +} diff --git a/vite.config.ts b/vite.config.ts index 53c1903..cd37977 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,13 @@ import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' +import Icons from 'unplugin-icons/vite' export default defineConfig({ - plugins: [sveltekit()], + plugins: [ + sveltekit(), + Icons({ + compiler: 'svelte', + autoInstall: true, + }), + ], })