AutoImage replaced LazyImage && general improvements to components with style props
This commit is contained in:
503
package-lock.json
generated
503
package-lock.json
generated
@@ -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",
|
||||
|
||||
10
package.json
10
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",
|
||||
|
||||
2
src/app.d.ts
vendored
2
src/app.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
6
src/lib/api-helper.ts
Normal file
6
src/lib/api-helper.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import ky from 'ky'
|
||||
|
||||
export const apiV1 = ky.create({
|
||||
prefixUrl: '/api/v1/',
|
||||
credentials: 'include',
|
||||
})
|
||||
@@ -1,39 +1,35 @@
|
||||
<script lang="ts">
|
||||
import LazyImage from '$lib/components/media/lazyImage.svelte'
|
||||
import AutoImage from './autoImage.svelte'
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import ArtistList from '$lib/components/media/artistList.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { queue, newestAlert } from '$lib/stores'
|
||||
import { apiV1 } from '$lib/api-helper'
|
||||
import ServiceLogo from '$lib/components/util/serviceLogo.svelte'
|
||||
|
||||
export let album: Album
|
||||
|
||||
async function playAlbum() {
|
||||
const itemsResponse = await fetch(`/api/connections/${album.connection.id}/album/${album.id}/items`, {
|
||||
credentials: 'include',
|
||||
}).catch(() => null)
|
||||
|
||||
if (!itemsResponse || !itemsResponse.ok) {
|
||||
try {
|
||||
const response = await apiV1.get(`connections/${album.connection.id}/album/${album.id}/items`).json<{ items: Song[] }>()
|
||||
$queue.setQueue(response.items)
|
||||
} catch {
|
||||
$newestAlert = ['warning', 'Failed to play album']
|
||||
return
|
||||
}
|
||||
|
||||
const data = (await itemsResponse.json()) as { items: Song[] }
|
||||
$queue.setQueue(data.items)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
<div id="thumbnail-wrapper" class="relative aspect-square w-full overflow-clip rounded">
|
||||
<button id="thumbnail" class="h-full w-full" on:click={() => goto(`/details/album?id=${album.id}&connection=${album.connection.id}`)}>
|
||||
<LazyImage thumbnailUrl={album.thumbnailUrl} alt={`${album.name} jacket`} objectFit={'cover'} />
|
||||
<AutoImage thumbnailUrl={album.thumbnailUrl} alt={`${album.name} jacket`} --object-fit="cover" --border-radius="0.25rem" --height="100%" />
|
||||
</button>
|
||||
<div id="play-button" class="absolute left-1/2 top-1/2 h-1/4 -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity">
|
||||
<IconButton halo={true} on:click={playAlbum}>
|
||||
<i slot="icon" class="fa-solid fa-play text-2xl" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div id="connection-type-icon" class="absolute left-2 top-2 h-9 w-9 opacity-0 transition-opacity">
|
||||
<div id="connection-type-icon" class="absolute left-2 top-2 h-6 w-6 opacity-0 transition-opacity">
|
||||
<ServiceLogo type={album.connection.type} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,9 +48,6 @@
|
||||
#thumbnail-wrapper:hover > #play-button {
|
||||
opacity: 1;
|
||||
}
|
||||
/* #connection-type-icon {
|
||||
filter: grayscale();
|
||||
} */
|
||||
#thumbnail-wrapper:hover > #connection-type-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
57
src/lib/components/media/autoImage.svelte
Normal file
57
src/lib/components/media/autoImage.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<!--
|
||||
@component
|
||||
A component to help render images in a smooth and efficient way. The url passed will be fetched via Lazuli's
|
||||
remoteImage API endpoint with size parameters that are dynamically calculated base off of the image's container's
|
||||
width and height. Images are lazily loaded unless 'eager' loading is specified.
|
||||
|
||||
@param thumbnailUrl A string of a URL that points to the desired image.
|
||||
@param alt Supplementary text in the event the image fails to load.
|
||||
@param loading Optional. Either the string 'lazy' or 'eager', defaults to lazy. The method by which to load the image.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let thumbnailUrl: string
|
||||
export let alt: string
|
||||
export let loading: 'lazy' | 'eager' = 'lazy'
|
||||
|
||||
let currentSlot: number = 1 // 1 | 2
|
||||
|
||||
let imageContainer: HTMLDivElement | undefined
|
||||
let slot1: HTMLImageElement | undefined
|
||||
let slot2: HTMLImageElement | undefined
|
||||
|
||||
const SIZE_TO_PIXEL_FACTOR = 1.5 // Images will be fetched with a pixel density 1.5x the size of its container. This is a good compromise between sharpness and performance
|
||||
|
||||
// ? Maybe implement auto-resizing
|
||||
function updateImage(newThumbnailURL: string) {
|
||||
if (!(slot1 && slot2 && imageContainer)) return
|
||||
|
||||
const maxWidth = imageContainer.clientWidth * SIZE_TO_PIXEL_FACTOR
|
||||
const maxHeight = imageContainer.clientHeight * SIZE_TO_PIXEL_FACTOR
|
||||
|
||||
const imageSrc = `/api/remoteImage?url=${newThumbnailURL}&${maxWidth > maxHeight ? `maxWidth=${maxWidth}` : `maxHeight=${maxHeight}`}`
|
||||
currentSlot === 1 ? (slot2.src = imageSrc) : (slot1.src = imageSrc)
|
||||
}
|
||||
|
||||
onMount(() => updateImage(thumbnailUrl))
|
||||
$: updateImage(thumbnailUrl)
|
||||
</script>
|
||||
|
||||
<div id="image-container" bind:this={imageContainer} class="grid">
|
||||
<img bind:this={slot1} {alt} {loading} class:opacity-0={currentSlot === 2} class:hidden={!slot1 || slot1.src === ''} class="h-full transition-opacity duration-500" on:load={() => (currentSlot = 1)} />
|
||||
<img bind:this={slot2} {alt} {loading} class:opacity-0={currentSlot === 1} class:hidden={!slot1 || slot2.src === ''} class="h-full transition-opacity duration-500" on:load={() => (currentSlot = 2)} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#image-container {
|
||||
height: var(--height);
|
||||
}
|
||||
img {
|
||||
grid-area: 1 / 1;
|
||||
object-fit: var(--object-fit);
|
||||
object-position: var(--object-position);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
</style>
|
||||
@@ -1,70 +0,0 @@
|
||||
<!--
|
||||
@component
|
||||
A component to help render images in a smooth and efficient way. The url passed will be fetched via Lazuli's
|
||||
remoteImage API endpoint with size parameters that are dynamically calculated base off of the image's container's
|
||||
width and height. Images are lazily loaded unless 'eager' loading is specified.
|
||||
|
||||
@param thumbnailUrl A string of a URL that points to the desired image.
|
||||
@param alt Supplementary text in the event the image fails to load.
|
||||
@param loadingMethod Optional. Either the string 'lazy' or 'eager', defaults to lazy. The method by which to load the image.
|
||||
@param objectFit One of the following fill, contain, cover, none, scale-down. Specifies the object-fit styling of the image
|
||||
@param objectPosistion Optional. Specifies the object-position styling of the image. Defaults to 'center'
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let thumbnailUrl: string
|
||||
export let alt: string
|
||||
export let loadingMethod: 'lazy' | 'eager' = 'lazy'
|
||||
export let objectFit: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
|
||||
export let objectPosition: string = 'center'
|
||||
|
||||
let imageContainer: HTMLDivElement
|
||||
|
||||
// TODO: Implement auto-resizing
|
||||
function updateImage(newThumbnailURL: string) {
|
||||
if (!imageContainer) return
|
||||
|
||||
const width = imageContainer.clientWidth * 1.5 // 1.5x is a good compromise between sharpness and performance
|
||||
const height = imageContainer.clientHeight * 1.5
|
||||
|
||||
const newImage = new Image(width, height)
|
||||
imageContainer.appendChild(newImage)
|
||||
|
||||
newImage.loading = loadingMethod
|
||||
newImage.src = `/api/remoteImage?url=${newThumbnailURL}&`.concat(width > height ? `maxWidth=${width}` : `maxHeight=${height}`)
|
||||
newImage.alt = alt
|
||||
|
||||
newImage.style.width = '100%'
|
||||
newImage.style.height = '100%'
|
||||
newImage.style.objectFit = objectFit
|
||||
newImage.style.objectPosition = objectPosition
|
||||
newImage.style.opacity = '0'
|
||||
newImage.style.position = 'absolute'
|
||||
|
||||
function removeOldImage() {
|
||||
if (imageContainer.childElementCount > 1) {
|
||||
const oldImage = imageContainer.firstChild! as HTMLImageElement
|
||||
oldImage.style.opacity = '0'
|
||||
setTimeout(() => imageContainer.removeChild(oldImage), 500)
|
||||
}
|
||||
}
|
||||
|
||||
newImage.onload = () => {
|
||||
removeOldImage()
|
||||
newImage.style.transition = 'opacity 500ms ease'
|
||||
newImage.style.opacity = '1'
|
||||
}
|
||||
|
||||
newImage.onerror = () => {
|
||||
console.error(`Image from url: ${newThumbnailURL} failed to update`)
|
||||
imageContainer.removeChild(newImage)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => updateImage(thumbnailUrl))
|
||||
$: updateImage(thumbnailUrl)
|
||||
</script>
|
||||
|
||||
<div bind:this={imageContainer} class="relative h-full w-full"></div>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import LazyImage from './lazyImage.svelte'
|
||||
import AutoImage from './autoImage.svelte'
|
||||
import ArtistList from './artistList.svelte'
|
||||
|
||||
export let mediaItem: Song | Album | Artist | Playlist
|
||||
@@ -12,7 +12,7 @@
|
||||
<div id="list-item" class="h-16 w-full">
|
||||
<div class="h-full overflow-clip rounded-md">
|
||||
{#if thumbnailUrl}
|
||||
<LazyImage {thumbnailUrl} alt={`${mediaItem.name} thumbnial`} objectFit={'cover'} />
|
||||
<AutoImage {thumbnailUrl} alt="{mediaItem.name} jacket" --object-fit="cover" />
|
||||
{:else}
|
||||
<div id="thumbnail-placeholder" class="grid h-full w-full place-items-center bg-lazuli-primary">
|
||||
<i class="fa-solid {mediaItem.type === 'artist' ? 'fa-user' : 'fa-play'} text-2xl" />
|
||||
|
||||
@@ -4,8 +4,16 @@
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import Slider from '$lib/components/util/slider.svelte'
|
||||
import Loader from '$lib/components/util/loader.svelte'
|
||||
import AutoImage from '$lib/components/media/autoImage.svelte'
|
||||
import PhQueueBold from '~icons/ph/queue-bold'
|
||||
import PhShuffleBold from '~icons/ph/shuffle-bold'
|
||||
import PhRepeatBold from '~icons/ph/repeat-bold'
|
||||
import BxVolumeFull from '~icons/bx/volume-full'
|
||||
import BxVolumeLow from '~icons/bx/volume-low'
|
||||
import BxVolume from '~icons/bx/volume'
|
||||
import MiExpand from '~icons/mi/expand'
|
||||
import { onMount, createEventDispatcher } from 'svelte'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { slide, fade } from 'svelte/transition'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -76,15 +84,10 @@
|
||||
let playerWidth: number
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:clientWidth={playerWidth}
|
||||
transition:slide
|
||||
id="player"
|
||||
class="fixed {playerWidth > 800 ? 'bottom-0 left-0 right-0' : 'bottom-3 left-3 right-3 rounded-lg'} flex h-20 items-center gap-4 overflow-clip bg-neutral-925 px-2 text-neutral-400 transition-all"
|
||||
>
|
||||
<div id="details" class="flex h-full items-center gap-3 overflow-clip py-2" style="backgrounds: linear-gradient(to right, red, blue); flex-grow: 1; flex-basis: 18rem">
|
||||
<div bind:clientWidth={playerWidth} transition:slide id="player" class="flex h-20 items-center gap-4 overflow-clip bg-neutral-925 px-2.5 text-neutral-400">
|
||||
<div id="details" class="flex h-full items-center gap-3 overflow-clip py-2.5" style="backgrounds: linear-gradient(to right, red, blue); flex-grow: 1; flex-basis: 16rem">
|
||||
<div class="relative aspect-square h-full">
|
||||
<img src="/api/remoteImage?url={mediaItem.thumbnailUrl}&maxHeight=96" alt="jacket" class="h-full w-full rounded object-cover" />
|
||||
<AutoImage thumbnailUrl={mediaItem.thumbnailUrl} alt="{mediaItem.name} jacket" loading="eager" --object-fit="cover" --height="100%" --border-radius="0.25rem" />
|
||||
<div id="jacket-play-button" class:hidden={playerWidth > 650} class="absolute bottom-0 left-0 right-0 top-0 backdrop-brightness-50">
|
||||
<IconButton on:click={() => (paused = !paused)}>
|
||||
<i slot="icon" class="fa-solid {paused ? 'fa-play' : 'fa-pause'} text-2xl text-neutral-200" />
|
||||
@@ -93,15 +96,15 @@
|
||||
</div>
|
||||
<div class="flex flex-col justify-center gap-1">
|
||||
<ScrollingText>
|
||||
<span slot="text" class="line-clamp-1 text-sm font-semibold text-neutral-200">{mediaItem.name}</span>
|
||||
<span slot="text" title={mediaItem.name} class="line-clamp-1 text-sm font-medium text-neutral-200">{mediaItem.name}</span>
|
||||
</ScrollingText>
|
||||
<div class="line-clamp-1 text-xs">
|
||||
<ArtistList {mediaItem} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-8">
|
||||
<IconButton color={'#ec4899'} on:click={() => (favorite = !favorite)}>
|
||||
<i slot="icon" class={favorite ? 'fa-solid fa-heart text-pink-500' : 'fa-regular fa-heart'} />
|
||||
<IconButton --color="#ec4899" toggled={favorite} on:click={() => (favorite = !favorite)}>
|
||||
<i slot="icon" class={favorite ? 'fa-solid fa-heart' : 'fa-regular fa-heart'} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<span class:hidden={playerWidth > 700 || playerWidth < 400} class="ml-auto whitespace-nowrap text-xs">{currentTimestamp} / {durationTimestamp}</span>
|
||||
@@ -150,25 +153,36 @@
|
||||
{#if playerWidth > 450}
|
||||
<div id="tools" class="flex h-full justify-end gap-0.5 py-6" style="backgrounds: linear-gradient(to right, purple, orange);">
|
||||
{#if playerWidth > 1100}
|
||||
<IconButton on:click={() => dispatch('toggleShuffle')}>
|
||||
<i slot="icon" class:text-lazuli-primary={shuffled} class="fa-solid fa-shuffle" />
|
||||
<IconButton --hover-color="#e5e5e5" toggled={shuffled} on:click={() => dispatch('toggleShuffle')}>
|
||||
<PhShuffleBold slot="icon" />
|
||||
</IconButton>
|
||||
<IconButton on:click={() => (loop = !loop)}>
|
||||
<i slot="icon" class:text-lazuli-primary={loop} class="fa-solid fa-repeat" />
|
||||
<IconButton --hover-color="#e5e5e5" toggled={loop} on:click={() => (loop = !loop)}>
|
||||
<PhRepeatBold slot="icon" />
|
||||
</IconButton>
|
||||
<IconButton --hover-color="#e5e5e5">
|
||||
<PhQueueBold slot="icon" />
|
||||
</IconButton>
|
||||
<div class="flex h-full items-center gap-1">
|
||||
<IconButton on:click={() => (volume = volume > 0 ? 0 : getStoredVolume())}>
|
||||
<i slot="icon" class="fa-solid {volume > MAX_VOLUME / 2 ? 'fa-volume-high' : volume > 0 ? 'fa-volume-low' : 'fa-volume-xmark'}" />
|
||||
<IconButton --hover-color="#e5e5e5" on:click={() => (volume = volume > 0 ? 0 : getStoredVolume())}>
|
||||
<span slot="icon" class="relative grid place-items-center">
|
||||
{#if volume > MAX_VOLUME / 2}
|
||||
<span class="absolute" in:fade={{ duration: 200 }} out:fade={{ duration: 200, delay: 100 }}><BxVolumeFull /></span>
|
||||
{:else if volume > 0}
|
||||
<span class="absolute" in:fade={{ duration: 200 }} out:fade={{ duration: 200, delay: 100 }}><BxVolumeLow /></span>
|
||||
{:else}
|
||||
<span class="absolute" in:fade={{ duration: 200 }} out:fade={{ duration: 200, delay: 100 }}><BxVolume /></span>
|
||||
{/if}
|
||||
</span>
|
||||
</IconButton>
|
||||
<div class="mr-2 w-20">
|
||||
<Slider bind:value={volume} max={MAX_VOLUME} on:seeked={() => (volume > 0 ? localStorage.setItem('volume', volume.toString()) : null)} />
|
||||
</div>
|
||||
</div>
|
||||
<IconButton>
|
||||
<i slot="icon" class="fa-solid fa-up-right-and-down-left-from-center" />
|
||||
<IconButton --hover-color="#e5e5e5">
|
||||
<MiExpand slot="icon" />
|
||||
</IconButton>
|
||||
{/if}
|
||||
<IconButton>
|
||||
<IconButton --hover-color="#e5e5e5">
|
||||
<i slot="icon" class="fa-solid fa-ellipsis-vertical" />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -189,3 +203,13 @@
|
||||
on:error={() => setTimeout(() => audioElement.load(), 5000)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#player {
|
||||
border-radius: var(--border-radius);
|
||||
transition: border-radius 150ms linear;
|
||||
-webkit-box-shadow: 0px 0px 80px 0px rgba(0, 0, 0, 0.75);
|
||||
-moz-box-shadow: 0px 0px 80px 0px rgba(0, 0, 0, 0.75);
|
||||
box-shadow: 0px 0px 80px 0px rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 @@
|
||||
<main in:fade={{ duration: 75, delay: 500 }} out:fade={{ duration: 75 }} class="flex h-20 w-full gap-10">
|
||||
<section class="flex w-96 min-w-64 gap-2">
|
||||
<div class="relative h-full w-20 min-w-20 overflow-clip rounded-xl p-2">
|
||||
<LazyImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt={`${currentlyPlaying.name} jacket`} objectFit={'cover'} />
|
||||
<AutoImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt="{currentlyPlaying.name} jacket" --object-fit="cover" />
|
||||
</div>
|
||||
<section class="flex flex-grow flex-col justify-center gap-1">
|
||||
<div class="h-6">
|
||||
@@ -185,7 +185,7 @@
|
||||
{:else}
|
||||
<main id="expanded-player" in:fade={{ delay: 500 }} out:fade={{ duration: 75 }} class="relative h-full">
|
||||
<div class="absolute -z-10 h-full w-full blur-2xl brightness-[25%]">
|
||||
<LazyImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt={''} objectFit={'cover'} />
|
||||
<AutoImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt="" --object-fit="cover" />
|
||||
</div>
|
||||
<section class="relative grid h-full grid-rows-[1fr_4fr] gap-4 px-24 py-16">
|
||||
<div class="grid grid-cols-[2fr_1fr]">
|
||||
@@ -202,7 +202,7 @@
|
||||
<strong transition:fade class="ml-2 text-2xl">UP NEXT</strong>
|
||||
<div transition:fly={{ x: 300 }} class="mt-3 flex h-20 w-full items-center gap-3 overflow-clip rounded-lg border border-neutral-300 bg-neutral-900 pr-3">
|
||||
<div class="aspect-square h-full">
|
||||
<LazyImage thumbnailUrl={next.thumbnailUrl} alt={`${next.name} jacket`} objectFit={'cover'} />
|
||||
<AutoImage thumbnailUrl={next.thumbnailUrl} alt="{next.name} jacket" --object-fit="cover" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-0.5 line-clamp-1 font-medium">{next.name}</div>
|
||||
@@ -214,7 +214,7 @@
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
<LazyImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt={`${currentlyPlaying.name} jacket`} objectFit={'contain'} objectPosition={'left'} />
|
||||
<AutoImage thumbnailUrl={currentlyPlaying.thumbnailUrl} alt="{currentlyPlaying.name} jacket" --object-fit="contain" --object-position="left" />
|
||||
</section>
|
||||
<section class="self-center px-16">
|
||||
<div class="mb-7 flex min-w-56 flex-grow items-center justify-items-center gap-3 font-light">
|
||||
|
||||
62
src/lib/components/media/playlistCard.svelte
Normal file
62
src/lib/components/media/playlistCard.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import AutoImage from './autoImage.svelte'
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import ArtistList from '$lib/components/media/artistList.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { queue, newestAlert } from '$lib/stores'
|
||||
import { apiV1 } from '$lib/api-helper'
|
||||
import ServiceLogo from '$lib/components/util/serviceLogo.svelte'
|
||||
|
||||
export let playlist: Playlist
|
||||
|
||||
async function playPlaylist() {
|
||||
try {
|
||||
const initialResponse = await apiV1.get(`connections/${playlist.connection.id}/playlist/${playlist.id}/items?startIndex=0&limit=50`).json<{ items: Song[] }>()
|
||||
$queue.setQueue(initialResponse.items)
|
||||
|
||||
if (initialResponse.items.length < 50) return
|
||||
|
||||
const remainderResponse = await apiV1.get(`connections/${playlist.connection.id}/playlist/${playlist.id}/items?startIndex=50`).json<{ items: Song[] }>()
|
||||
$queue.enqueue(...remainderResponse.items)
|
||||
} catch {
|
||||
$newestAlert = ['warning', 'Failed to play playlist']
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
<div id="thumbnail-wrapper" class="relative aspect-square w-full overflow-clip rounded">
|
||||
<button id="thumbnail" class="h-full w-full" on:click={() => goto(`/details/playlist?id=${playlist.id}&connection=${playlist.connection.id}`)}>
|
||||
<AutoImage thumbnailUrl={playlist.thumbnailUrl} alt="{playlist.name} jacket" --object-fit="cover" --height="100%" --border-radius="0.25rem" />
|
||||
</button>
|
||||
<div id="play-button" class="absolute left-1/2 top-1/2 h-1/4 -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity">
|
||||
<IconButton halo={true} on:click={playPlaylist}>
|
||||
<i slot="icon" class="fa-solid fa-play text-2xl" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div id="connection-type-icon" class="absolute left-2 top-2 h-6 w-6 opacity-0 transition-opacity">
|
||||
<ServiceLogo type={playlist.connection.type} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2 text-center text-sm">
|
||||
<div class="line-clamp-2">{playlist.name}</div>
|
||||
<div class="line-clamp-2 text-neutral-400">
|
||||
<ArtistList mediaItem={playlist} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#thumbnail-wrapper:hover > #thumbnail {
|
||||
filter: brightness(35%);
|
||||
}
|
||||
#thumbnail-wrapper:hover > #play-button {
|
||||
opacity: 1;
|
||||
}
|
||||
#thumbnail-wrapper:hover > #connection-type-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
#thumbnail {
|
||||
transition: filter 150ms ease;
|
||||
}
|
||||
</style>
|
||||
0
src/lib/components/media/songCard.svelte
Normal file
0
src/lib/components/media/songCard.svelte
Normal file
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
export let toggled = false
|
||||
export let disabled = false
|
||||
export let halo = false
|
||||
export let color = 'var(--lazuli-primary)'
|
||||
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
class:toggled
|
||||
class:disabled
|
||||
class:halo
|
||||
class="relative grid aspect-square h-full place-items-center transition-transform duration-75 active:scale-90"
|
||||
style="--button-color: {color}"
|
||||
on:click|preventDefault|stopPropagation={() => dispatch('click')}
|
||||
{disabled}
|
||||
>
|
||||
@@ -27,7 +27,7 @@
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: color-mix(in srgb, var(--button-color) 20%, transparent);
|
||||
background-color: color-mix(in srgb, var(--color, var(--lazuli-primary)) 20%, transparent);
|
||||
border-radius: 100%;
|
||||
transition-property: width height;
|
||||
transition-duration: 200ms;
|
||||
@@ -40,7 +40,10 @@
|
||||
button :global(> :first-child) {
|
||||
transition: color 200ms;
|
||||
}
|
||||
button:not(.disabled):hover :global(> :first-child) {
|
||||
color: var(--button-color);
|
||||
button:not(.disabled).toggled :global(> :first-child) {
|
||||
color: var(--color, var(--lazuli-primary));
|
||||
}
|
||||
button:not(.disabled):not(.toggled):hover :global(> :first-child) {
|
||||
color: var(--hover-color, var(--color, var(--lazuli-primary)));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import IconButton from './iconButton.svelte'
|
||||
import MingcuteMenuLine from '~icons/mingcute/menu-line'
|
||||
import { goto } from '$app/navigation'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let searchBar: HTMLElement, searchInput: HTMLInputElement, searchBarWidth: number
|
||||
let searchInput: HTMLInputElement, searchBarWidth: number
|
||||
|
||||
const HIDE_SEARCHBAR_BREAKPOINT_PX = 300
|
||||
$: showSearchbar = searchBarWidth > HIDE_SEARCHBAR_BREAKPOINT_PX
|
||||
@@ -25,7 +26,7 @@
|
||||
<div class="mr-4 flex h-full items-center">
|
||||
<div class="h-full p-1">
|
||||
<IconButton halo={true} on:click={() => dispatch('opensidebar')}>
|
||||
<i slot="icon" class="fa-solid fa-bars text-xl" />
|
||||
<MingcuteMenuLine slot="icon" class="text-lg" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<!-- --------------This is a placeholder image-------------- -->
|
||||
@@ -35,7 +36,6 @@
|
||||
{#if showSearchbar}
|
||||
<search
|
||||
role="search"
|
||||
bind:this={searchBar}
|
||||
class="relative flex h-full w-full max-w-xl items-center gap-2.5 rounded-lg border border-[rgba(255,255,255,0.1)] px-4 py-2 text-neutral-400"
|
||||
style="background-color: rgba(255,255,255, 0.07);"
|
||||
>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
let slidingTextWidth: number, slidingTextWrapperWidth: number
|
||||
let scrollDirection: 1 | -1 = 1
|
||||
$: scrollDistance = slidingTextWidth - slidingTextWrapperWidth
|
||||
$: if (slidingText && scrollDistance > 0) slidingText.style.animationDuration = `${scrollDistance / 40}s`
|
||||
$: if (slidingText && scrollDistance > 0) slidingText.style.animationDuration = `${scrollDistance / 30}s`
|
||||
</script>
|
||||
|
||||
<div bind:clientWidth={slidingTextWrapperWidth} class="relative h-full w-full overflow-clip">
|
||||
|
||||
@@ -3,16 +3,20 @@
|
||||
import { sineOut } from 'svelte/easing'
|
||||
import { goto } from '$app/navigation'
|
||||
import IconButton from './iconButton.svelte'
|
||||
import PhPlaylistBold from '~icons/ph/playlist-bold'
|
||||
import MaterialSymbolsHome from '~icons/material-symbols/home'
|
||||
import IcSharpVideoLibrary from '~icons/ic/sharp-video-library'
|
||||
|
||||
type NavButton = {
|
||||
name: string
|
||||
path: string
|
||||
icon: string
|
||||
icon: any
|
||||
}
|
||||
|
||||
const navButtons: NavButton[] = [
|
||||
{ name: 'Home', path: '/', icon: 'fa-solid fa-house' },
|
||||
{ name: 'Library', path: '/library', icon: 'fa-solid fa-book' },
|
||||
{ name: 'Home', path: '/', icon: MaterialSymbolsHome },
|
||||
{ name: 'Mixes', path: '/mixes', icon: PhPlaylistBold },
|
||||
{ name: 'Library', path: '/library', icon: IcSharpVideoLibrary },
|
||||
]
|
||||
|
||||
const OPEN_CLOSE_DURATION = 250
|
||||
@@ -41,7 +45,7 @@
|
||||
}}
|
||||
class="flex w-full items-center gap-6 px-10 py-3.5 text-left transition-colors hover:bg-[rgba(255,255,255,0.1)]"
|
||||
>
|
||||
<i class={tab.icon} />
|
||||
<svelte:component this={tab.icon} class="text-lg" />
|
||||
{tab.name}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -6,7 +6,11 @@ export async function userExists(userId: string): Promise<boolean> {
|
||||
return Boolean(await DB.users.where('id', userId).first(DB.db.raw('EXISTS(SELECT 1)')))
|
||||
}
|
||||
|
||||
export async function mixExists(mixId: string): Promise<Boolean> {
|
||||
export async function connectionExists(connectionId: string): Promise<boolean> {
|
||||
return Boolean(await DB.connections.where('id', connectionId).first(DB.db.raw('EXISTS(SELECT 1)')))
|
||||
}
|
||||
|
||||
export async function mixExists(mixId: string): Promise<boolean> {
|
||||
return Boolean(await DB.mixes.where('id', mixId).first(DB.db.raw('EXISTS(SELECT 1)')))
|
||||
}
|
||||
|
||||
|
||||
@@ -570,6 +570,7 @@ export class YouTubeMusic implements Connection {
|
||||
*/
|
||||
public async getSongs(ids: Iterable<string>): Promise<Song[]> {
|
||||
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<InnerTube.Queue.Response>()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -9,25 +9,32 @@
|
||||
|
||||
$: currentlyPlaying = $queue.current
|
||||
$: shuffled = $queue.isShuffled
|
||||
|
||||
let playerWidth: number
|
||||
</script>
|
||||
|
||||
<main id="grid-wrapper" class="h-full">
|
||||
<main id="grid-wrapper" class="relative h-full">
|
||||
<Navbar on:opensidebar={sidebar.open} />
|
||||
<Sidebar bind:this={sidebar} />
|
||||
<section id="content-wrapper" class="overflow-y-scroll no-scrollbar">
|
||||
<div class="my-8">
|
||||
<slot />
|
||||
</section>
|
||||
</div>
|
||||
{#if currentlyPlaying}
|
||||
<div bind:clientWidth={playerWidth} class="sticky {playerWidth > 800 ? 'bottom-0' : 'bottom-3 mx-3'} transition-all">
|
||||
<MediaPlayer
|
||||
mediaItem={currentlyPlaying}
|
||||
{shuffled}
|
||||
mediaSession={'mediaSession' in navigator ? navigator.mediaSession : null}
|
||||
--border-radius={playerWidth > 800 ? '0' : '0.5rem'}
|
||||
on:stop={() => $queue.clear()}
|
||||
on:next={() => $queue.next()}
|
||||
on:previous={() => $queue.previous()}
|
||||
on:toggleShuffle={() => $queue.toggleShuffle()}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@@ -35,4 +42,8 @@
|
||||
display: grid;
|
||||
grid-template-rows: min-content auto;
|
||||
}
|
||||
#content-wrapper {
|
||||
display: grid;
|
||||
grid-template-rows: auto min-content;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types'
|
||||
import Loader from '$lib/components/util/loader.svelte'
|
||||
import MediaCard from '$lib/components/media/mediaCard.svelte'
|
||||
import AlbumCard from '$lib/components/media/albumCard.svelte'
|
||||
import PlaylistCard from '$lib/components/media/playlistCard.svelte'
|
||||
|
||||
export let data: PageData
|
||||
</script>
|
||||
@@ -11,9 +11,13 @@
|
||||
{#await data.recommendations}
|
||||
<Loader />
|
||||
{:then recommendations}
|
||||
<div id="card-wrapper" class="grid w-full gap-4 justify-self-center px-[5%] pt-8">
|
||||
{#each recommendations.filter((item) => item.type === 'album') as album}
|
||||
<AlbumCard {album} />
|
||||
<div id="card-wrapper" class="grid w-full gap-4 justify-self-center px-[5%]">
|
||||
{#each recommendations as mediaItem}
|
||||
{#if mediaItem.type === 'album'}
|
||||
<AlbumCard album={mediaItem} />
|
||||
{:else if mediaItem.type === 'playlist'}
|
||||
<PlaylistCard playlist={mediaItem} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
14
src/routes/(app)/mixes/+page.svelte
Normal file
14
src/routes/(app)/mixes/+page.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import IcBaselinePlus from '~icons/ic/baseline-plus'
|
||||
</script>
|
||||
|
||||
<main class="flex flex-wrap justify-center">
|
||||
<div class="grid aspect-square h-56 place-items-center">
|
||||
<div class="aspect-square h-16">
|
||||
<IconButton halo={true}>
|
||||
<IcBaselinePlus slot="icon" class="text-4xl text-neutral-300" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)}`)
|
||||
|
||||
19
src/routes/api/v1/connections/+server.ts
Normal file
19
src/routes/api/v1/connections/+server.ts
Normal file
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user