Compare commits

24 Commits
Author SHA1 Message Date
apt-get 3aae8a0130 yay accessibility!
aria tags + better touchscreen support
2026-09-25 21:50:00 +02:00
apt-get 83cc73acff add varying intensity to stream tags
plus improving the tag select dropdown in general
2026-09-25 21:26:31 +02:00
apt-get c49fb9bb54 upgrade svelte-select to v6 2026-09-25 14:25:00 +02:00
apt-get b8f1a1aa73 update frontpage msg 2026-09-06 02:32:28 +02:00
apt-get 3c42834d47 display cache usage and warn if not enough disk space available 2026-09-06 02:23:06 +02:00
apt-get 343447c37c delete done parts during assembly to prevent needing double the stream size as available disk space 2026-09-05 03:27:39 +02:00
apt-get 0ed1a094a2 don't hold 2x stream size while reassembling 2026-09-02 20:51:58 +02:00
apt-get 828c0a1d7b add some logging... 2026-09-02 20:43:40 +02:00
apt-get 771e81f50b insert into db script fixes 2026-09-02 20:33:23 +02:00
apt-get 65fc04a4d8 more linter fixes 2026-09-02 20:21:24 +02:00
apt-get 39593d470d decrease chunk size to 4MB, fix more potential breakages 2026-09-02 20:18:42 +02:00
apt-get c038316461 fix linter nitpicks 2026-09-02 19:04:43 +02:00
apt-get e98293fa7c guard around potential crashes 2026-09-02 18:38:38 +02:00
apt-get 9c3fd4c1a2 formatting pass 2026-09-02 18:23:22 +02:00
apt-get d7999785c8 partial download support for stream caching, better error handling 2026-09-02 18:23:16 +02:00
apt-get 62af3683a4 Put stream in downloading map on click rather than on dl start
prevents network issue race conditions
2026-09-02 14:25:16 +02:00
apt-get 7a052c4e68 formatting fix 2026-03-17 01:27:13 +01:00
apt-get 1c7736f832 cleanup svelte legacy APIs and stuff 2026-03-17 01:27:07 +01:00
apt-get 64681befad replace carta with bun's own api 2026-03-17 01:26:52 +01:00
apt-get 090a145211 add previous/next track button handling 2026-03-17 00:05:44 +01:00
apt-get 1dc3f4505b fix seeking problems due to progress bar width inconsistency 2026-03-16 23:21:00 +01:00
apt-get 7febb497e9 update homepage message 2026-03-16 20:38:35 +01:00
apt-get 6c12cfd49b Merge branch 'offline-mode' 2026-03-16 20:33:03 +01:00
apt-get f777873432 work around opus bug 2026-02-20 16:21:22 +01:00
24 changed files with 1052 additions and 645 deletions
+2 -284
View File
@@ -10,19 +10,17 @@
"@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"carta-md": "^4.11.1",
"dotenv": "^16.6.1",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.15.2",
"fontkit": "^2.0.4",
"isomorphic-dompurify": "^3.3.0",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"sqids": "0.3.0",
"svelte": "^5.53.12",
"svelte-check": "^4.4.5",
"svelte-select": "^5.8.3",
"svelte-select": "^6",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"vite": "^8.0.0",
@@ -39,28 +37,6 @@
"svelte-adapter-bun",
],
"packages": {
"@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="],
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.6" } }, "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw=="],
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.8.1", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.6" } }, "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ=="],
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
"@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
"@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="],
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="],
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.1", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w=="],
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
"@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="],
@@ -81,8 +57,6 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="],
"@exodus/bytes": ["@exodus/bytes@1.15.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="],
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
@@ -205,20 +179,6 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="],
"@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="],
"@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="],
"@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.9", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA=="],
@@ -237,28 +197,18 @@
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="],
@@ -279,96 +229,56 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="],
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
"carta-md": ["carta-md@4.11.1", "", { "dependencies": { "diff": "^5.2.0", "esm-env": "^1.0.0", "rehype-stringify": "^10.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", "shiki": "^3.4.0", "unified": "^11.0.5" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-sLUJTWOtbUOgaB7gtZiORKehsJ+Ju3/XAK6mQ4EmM8rq5Aahvk59VvZtztX7sMPg6zWZD9WGR21FAzzYYcKSUQ=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"cssstyle": ["cssstyle@6.2.0", "", { "dependencies": { "@asamuzakjp/css-color": "^5.0.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.28", "css-tree": "^3.1.0", "lru-cache": "^11.2.6" } }, "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig=="],
"data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
"dedent-js": ["dedent-js@1.0.1", "", {}, "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="],
"diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="],
"dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.0.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.1.1", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ=="],
@@ -397,8 +307,6 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
@@ -427,18 +335,6 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
@@ -451,18 +347,10 @@
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"isomorphic-dompurify": ["isomorphic-dompurify@3.3.0", "", { "dependencies": { "dompurify": "^3.3.3", "jsdom": "^28.1.0" } }, "sha512-Xx3GlNZNAXIISX49OMpfNgxrRU49KVQ5hqnK2zbkqkPucdWnCZXquTMK/COnb8pmIOJlfiaNYav9qyHX3gRDMA=="],
"jsdom": ["jsdom@28.1.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", "@bramus/specificity": "^2.4.2", "@exodus/bytes": "^1.11.0", "cssstyle": "^6.0.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
@@ -507,96 +395,8 @@
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
@@ -611,10 +411,6 @@
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.5", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -623,8 +419,6 @@
"pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
"parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -651,30 +445,10 @@
"prettier-plugin-svelte": ["prettier-plugin-svelte@3.5.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
@@ -685,8 +459,6 @@
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
@@ -697,18 +469,12 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"sqids": ["sqids@0.3.0", "", {}, "sha512-lOQK1ucVg+W6n3FhRwwSeUijxe93b51Bfz5PMRMihVf1iVkl82ePQG7V5vwrhzB11v0NtsR25PSZRGiSomJaJw=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"svelte": ["svelte@5.53.12", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-4x/uk4rQe/d7RhfvS8wemTfNjQ0bJbKvamIzRBfTe2eHHjzBZ7PZicUQrC2ryj83xxEacfA1zHKd1ephD1tAxA=="],
@@ -721,30 +487,16 @@
"svelte-floating-ui": ["svelte-floating-ui@1.5.8", "", { "dependencies": { "@floating-ui/core": "^1.5.0", "@floating-ui/dom": "^1.5.3" } }, "sha512-dVvJhZ2bT+kQDHlE4Lep8t+sgEc0XD96fXLzAi2DDI2bsaegBbClxXVNMma0C2WsG+n9GJSYx292dTvA8CYRtw=="],
"svelte-select": ["svelte-select@5.8.3", "", { "dependencies": { "svelte-floating-ui": "1.5.8" } }, "sha512-nQsvflWmTCOZjssdrNptzfD1Ok45hHVMTL5IHay5DINk7dfu5Er+8KsVJnZMJdSircqtR0YlT4YkCFlxOUhVPA=="],
"svelte-select": ["svelte-select@6.0.0", "", { "dependencies": { "svelte-floating-ui": "1.5.8" }, "peerDependencies": { "svelte": ">=5" } }, "sha512-WJ1UDw0a1Z3I7oVFgaCS7G9D2rCSmYAPpufNEj/DNuk5Oyb2a5dju6F36o0urBVw9bTE3WkM9cZkI0x+Sfo8Bg=="],
"svelte2tsx": ["svelte2tsx@0.7.52", "", { "dependencies": { "dedent-js": "^1.0.1", "scule": "^1.3.0" }, "peerDependencies": { "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "typescript": "^4.9.4 || ^5.0.0" } }, "sha512-svdT1FTrCLpvlU62evO5YdJt/kQ7nxgQxII/9BpQUvKr+GJRVdAXNVw8UWOt0fhoe5uWKyU0WsUTMRVAtRbMQg=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tldts": ["tldts@7.0.25", "", { "dependencies": { "tldts-core": "^7.0.25" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w=="],
"tldts-core": ["tldts-core@7.0.25", "", {}, "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw=="],
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
"tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
"tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -755,70 +507,36 @@
"typescript-svelte-plugin": ["typescript-svelte-plugin@0.3.50", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "svelte2tsx": "~0.7.35" } }, "sha512-CD6jMNAYJwqCyQ5zZBDRuveeJvAgIogLwXMf5eXAl4K36wD8W+Npw49h6j5fXnpd7SKcG3uptGpeCGETED6WSA=="],
"undici": ["undici@7.24.3", "", {}, "sha512-eJdUmK/Wrx2d+mnWWmwwLRyA7OQCkLap60sk3dOK4ViZR7DKwwptwuIvFBg2HaiP9ESaEdhtpSymQPvytpmkCA=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="],
"unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["vite@8.0.0", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="],
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
"whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"svelte-eslint-parser/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"svelte-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+49 -36
View File
@@ -22,49 +22,62 @@ const lookup = {
tracks: 'tracks'
};
// Retrieve existing IDs from Stream
const existingIds = db.prepare('SELECT id FROM Stream').all().map(row => row.id);
// Create a set of existing IDs for efficient lookup
const idSet = new Set(existingIds);
const existingRow = db.prepare('SELECT * FROM Stream WHERE id = ?');
// Check if --overwrite flag is set
// Overwrite differing streams without asking
const shouldOverwrite = process.argv.includes('--overwrite');
// Process JSON files and insert data into Stream
fs.readdir(jsonFolder, (err, files) => {
if (err) throw err;
console.log(`Found ${files.length} JSON file(s) in ${jsonFolder}.`);
const files = fs.readdirSync(jsonFolder);
console.log(`Found ${files.length} JSON file(s) in ${jsonFolder}.`);
files.forEach((file) => {
if (!file.endsWith('.json')) return;
if (file.endsWith('.sideload.json')) return;
for (const file of files) {
if (!file.endsWith('.json')) continue;
if (file.endsWith('.sideload.json')) continue;
const jsonString = fs.readFileSync(path.join(jsonFolder, file), 'utf8');
let jsonData = JSON.parse(jsonString);
const jsonString = fs.readFileSync(path.join(jsonFolder, file), 'utf8');
let jsonData = JSON.parse(jsonString);
const sideloadPath = path.join(jsonFolder, file.slice(0, -5) + '.sideload.json');
if (fs.existsSync(sideloadPath, 'utf8')) {
const sideloadData = JSON.parse(fs.readFileSync(sideloadPath));
jsonData = { ...jsonData, ...sideloadData };
}
// Skip if ID already exists in Stream
if (idSet.has(jsonData.id)) {
if (!shouldOverwrite) {
console.log(`Skipped data for ID ${jsonData.id} (already exists).`);
return;
}
console.log(`Overwriting data for ID ${jsonData.id}.`);
const sideloadPath = path.join(jsonFolder, file.slice(0, -5) + '.sideload.json');
if (fs.existsSync(sideloadPath)) {
const sideloadData = JSON.parse(fs.readFileSync(sideloadPath));
jsonData = { ...jsonData, ...sideloadData };
}
// A key with no column would otherwise reach the query as `undefined`
const unknown = Object.keys(jsonData).filter((key) => !(key in lookup));
if (unknown.length > 0)
console.log(`Ignored unknown key(s) in ${file}: ${unknown.join(', ')}.`);
// Prepare attributes for insertion
const fields = Object.entries(jsonData)
.filter(([key]) => key in lookup)
// Serialize value if array or object
.map(([key, value]) => [
lookup[key],
typeof value === 'object' && value !== null ? JSON.stringify(value) : value
]);
const row = existingRow.get(jsonData.id);
if (row) {
const changed = fields
.filter(([column, value]) => String(row[column] ?? '') !== String(value ?? ''))
.map(([column]) => column);
if (changed.length === 0) {
console.log(`Unchanged data for ID ${jsonData.id}.`);
continue;
}
// Prepare attributes for insertion
const values = Object.keys(jsonData).map(
// Serialize value if array
(key) => (Array.isArray(jsonData[key]) ? JSON.stringify(jsonData[key]) : jsonData[key])
);
const columns = Object.keys(jsonData).map((key) => lookup[key]);
console.log(`ID ${jsonData.id} differs from the database in: ${changed.join(', ')}.`);
if (!shouldOverwrite && !confirm(`Overwrite ID ${jsonData.id}?`)) {
console.log(`Kept database version of ID ${jsonData.id}.`);
continue;
}
}
const sql = `INSERT OR REPLACE INTO Stream (${columns.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
db.prepare(sql).run(...values);
console.log(`Inserted data for ID ${jsonData.id}.`);
});
});
const columns = fields.map(([column]) => column);
const sql = `INSERT OR REPLACE INTO Stream (${columns.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
db.prepare(sql).run(...fields.map(([, value]) => value));
console.log(`Inserted data for ID ${jsonData.id}.`);
}
+1 -3
View File
@@ -20,19 +20,17 @@
"@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"carta-md": "^4.11.1",
"dotenv": "^16.6.1",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.15.2",
"fontkit": "^2.0.4",
"isomorphic-dompurify": "^3.3.0",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.5.1",
"sqids": "0.3.0",
"svelte": "^5.53.12",
"svelte-check": "^4.4.5",
"svelte-select": "^5.8.3",
"svelte-select": "^6",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"vite": "^8.0.0"
+1
View File
@@ -1,3 +1,4 @@
/// <reference lib="dom.asynciterable" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
+44
View File
@@ -0,0 +1,44 @@
<script lang="ts">
import { TAG_WEIGHT_NAMES } from '$lib/stores.svelte.ts';
import type { TagWeights } from '$lib/types';
let { tags, highlighted = {} }: { tags: TagWeights; highlighted?: Record<string, number> } =
$props();
let sorted = $derived(Object.entries(tags).sort((a, b) => b[1] - a[1]));
</script>
{#each sorted as [tag, weight], i}{i ? ', ' : ''}<span
class="weight-{weight}"
class:highlighted={tag in highlighted}
>{tag}<span class="screen-reader-only"> ({TAG_WEIGHT_NAMES[weight]})</span></span
>{/each}
<style>
.weight-1 {
font-weight: normal;
}
.weight-2 {
font-weight: bold;
}
.weight-3 {
font-weight: bold;
text-decoration: underline;
}
/* translucent to take on the hover/selected bg colour tint */
.highlighted {
background: rgba(255, 220, 0, 0.18);
}
.screen-reader-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
</style>
+4 -3
View File
@@ -9,7 +9,8 @@ const db = new Database(dbName);
export function getStreams(): StreamSummary[] {
const indexData = db
.prepare(
'SELECT id, stream_date, filename, title, tags, length_seconds ' + 'FROM Stream ORDER BY id DESC'
'SELECT id, stream_date, filename, title, tags, length_seconds ' +
'FROM Stream ORDER BY id DESC'
)
.all() as Record<string, unknown>[];
return indexData.map((stream) => ({
@@ -17,7 +18,7 @@ export function getStreams(): StreamSummary[] {
stream_date: Date.parse(stream.stream_date as string),
filename: stream.filename as string,
title: stream.title as string | null,
tags: JSON.parse(stream.tags as string) as string[],
tags: stream.tags ? JSON.parse(stream.tags as string) : {},
length_seconds: stream.length_seconds as number
}));
}
@@ -39,7 +40,7 @@ export function getStreamInfo(streamId: string): Stream | null {
format: streamData.format as string,
title: streamData.title as string | null,
description: streamData.description as string | null,
tags: JSON.parse(streamData.tags as string) as string[],
tags: streamData.tags ? JSON.parse(streamData.tags as string) : {},
length_seconds: streamData.length_seconds as number,
tracks: JSON.parse(streamData.tracks as string)
};
+6 -5
View File
@@ -1,15 +1,16 @@
import { browser } from '$app/environment';
import { SvelteSet } from 'svelte/reactivity';
import { readStored, writeStored } from './utils.ts';
export const favoritedStreams = new SvelteSet<string>(
JSON.parse((browser && localStorage.getItem('favoritedStreams')) || '[]')
);
export const favoritedStreams = new SvelteSet<string>(readStored('favoritedStreams', []));
$effect.root(() => {
$effect(() => {
localStorage.setItem('favoritedStreams', JSON.stringify(Array.from(favoritedStreams)));
writeStored('favoritedStreams', Array.from(favoritedStreams));
});
});
export const MAX_TAG_WEIGHT = 3;
export const TAG_WEIGHT_NAMES = ['off', 'light', 'medium', 'heavy'];
export const tagList = [
'acoustic',
'electronic',
+233 -46
View File
@@ -1,66 +1,253 @@
import { browser } from '$app/environment';
import { SvelteSet, SvelteMap } from 'svelte/reactivity';
import type { StreamSummary } from './types.ts';
import { readStored, writeStored } from './utils.ts';
import type { StreamSummary, PartialDownload } from './types.ts';
const STORAGE_KEY = 'cachedStreams';
const PARTIALS_KEY = 'partialStreams';
const PARTS_DIR = 'parts';
export const cached = new SvelteSet<string>(
browser ? JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') : []
);
// MAX_ATTEMPTS is consecutive attempts that acquired zero chunks, so the exponential
// backoff below tolerates roughly 8 minutes of outage before giving up
const MAX_ATTEMPTS = 8;
const CHUNK_SIZE = 4 * 1024 * 1024;
// 60s as the limit for downloading one chunk is a reasonable guess
// as the intent is to catch socket breakage during cellular network changes for example
const CHUNK_TIMEOUT = 60_000;
class QuotaError extends Error {
constructor(readonly missing: number) {
super();
}
}
// total bytes used by our OPFS origin
export const usage = $state<{ bytes?: number }>({});
export const cached = new SvelteSet<string>(readStored<string[]>(STORAGE_KEY, []));
export const downloading = new SvelteMap<string, number>();
// bytes already written to OPFS for each incomplete download
export const partials = new SvelteMap<string, PartialDownload>(
Object.entries(readStored<Record<string, PartialDownload>>(PARTIALS_KEY, {}))
);
if (browser) {
$effect.root(() => {
$effect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify([...cached]));
});
});
refreshUsage();
$effect.root(() => {
$effect(() => {
writeStored(STORAGE_KEY, [...cached]);
});
$effect(() => {
writeStored(PARTIALS_KEY, Object.fromEntries(partials));
});
});
}
export async function download(stream: StreamSummary) {
const res = await fetch(`/media/tracks/${stream.filename}`);
if (!res.ok || !res.body) throw new Error('Download failed');
if (downloading.has(stream.id) || cached.has(stream.id)) return;
const total = Number(res.headers.get('content-length') || 0);
const reader = res.body.getReader();
const chunks: Uint8Array[] = [];
let received = 0;
const resumed = partials.get(stream.id);
downloading.set(stream.id, percent(resumed?.received ?? 0, resumed?.total ?? 0));
downloading.set(stream.id, 0);
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
if (total) downloading.set(stream.id, Math.round((received / total) * 100));
}
const root = await navigator.storage.getDirectory();
const handle = await root.getFileHandle(stream.id, { create: true });
const writable = await handle.createWritable();
await writable.write(new Blob(chunks));
await writable.close();
downloading.delete(stream.id);
cached.add(stream.id);
try {
let attempt = 0;
while (true) {
const before = partials.get(stream.id)?.received ?? 0;
try {
await attemptDownload(stream);
partials.delete(stream.id);
cached.add(stream.id);
return;
} catch (err) {
if (err instanceof QuotaError) {
alert(
`not enough disk space ;_;, need ${Math.ceil(err.missing / 1_000_000)} MB more.`
);
return;
}
console.error(`[streamCache] ${stream.id} attempt failed`, err);
// reset attempts if download of at least one chunk successful
attempt = (partials.get(stream.id)?.received ?? 0) > before ? 0 : attempt + 1;
if (attempt === MAX_ATTEMPTS) return;
}
await waitForRetry(attempt);
}
} finally {
downloading.delete(stream.id);
}
}
export async function remove(streamId: string) {
const root = await navigator.storage.getDirectory();
await root.removeEntry(streamId);
cached.delete(streamId);
cached.delete(streamId);
await discard(streamId);
}
export async function getUrl(streamId: string): Promise<string | null> {
if (!cached.has(streamId)) return null;
try {
const root = await navigator.storage.getDirectory();
const handle = await root.getFileHandle(streamId);
const file = await handle.getFile();
return URL.createObjectURL(file);
} catch {
cached.delete(streamId);
return null;
}
if (!cached.has(streamId)) return null;
try {
const root = await navigator.storage.getDirectory();
const handle = await root.getFileHandle(streamId);
const file = await handle.getFile();
return URL.createObjectURL(file);
} catch {
cached.delete(streamId);
return null;
}
}
async function attemptDownload(stream: StreamSummary) {
let state = partials.get(stream.id);
let checkedSpace = false;
while (!state || state.received < state.total) {
state = await fetchChunk(stream, state);
partials.set(stream.id, state);
downloading.set(stream.id, percent(state.received, state.total));
// first chunk's HTTP response carries the total stream size
if (!checkedSpace) {
await checkSpace(stream.id, state);
checkedSpace = true;
}
}
console.log(`[streamCache] ${stream.id} assembling ${state.total} bytes`);
await assemble(stream.id, state.total);
console.log(`[streamCache] ${stream.id} assembled`);
}
/** check available space below what's needed to download the stream */
async function checkSpace(streamId: string, state: PartialDownload) {
const { usage: used, quota } = await navigator.storage.estimate();
if (used === undefined || quota === undefined) return;
// extra CHUNK_SIZE is due to the delta in `assemble()` merging one chunk in (+1) then deleting it
const peak = used + (state.total - state.received) + CHUNK_SIZE;
if (peak > quota) throw new QuotaError(peak - quota);
}
/** fetch chunk starting from the last valid one, store in OPFS, and return updated state */
async function fetchChunk(stream: StreamSummary, state?: PartialDownload) {
const start = state?.received ?? 0;
const headers = new Headers({ Range: `bytes=${start}-${start + CHUNK_SIZE - 1}` });
// attach existing ETag to the request
if (state?.etag) headers.set('If-Range', state.etag);
const res = await fetch(`/media/tracks/${stream.filename}`, {
headers,
signal: AbortSignal.timeout(CHUNK_TIMEOUT)
});
// 200 means the range was ignored or the file changed (different ETag)
// 416 that our start offset is past its end
// anything else can be transient, so the chunks already on disk are kept
if (res.status !== 206) {
if (res.status === 200 || res.status === 416) await discard(stream.id);
throw new Error(`Download failed: ${res.status}`);
}
const total = Number(res.headers.get('content-range')?.split('/')[1]);
if (!total) throw new Error('Malformed Content-Range');
const etag = res.headers.get('etag') ?? res.headers.get('last-modified');
if (state && state.total !== total) {
await discard(stream.id);
throw new Error('File changed while downloading');
}
const bytes = await res.arrayBuffer();
const dir = await partsDir(stream.id, true);
const handle = await dir.getFileHandle(String(start), { create: true });
const writable = await handle.createWritable();
await writable.write(bytes);
await writable.close();
return { received: start + bytes.byteLength, total, etag };
}
/** concatenate stream file once all the parts are downloaded */
async function assemble(streamId: string, total: number) {
const root = await navigator.storage.getDirectory();
const dir = await partsDir(streamId, true);
const handle = await root.getFileHandle(streamId, { create: true });
// track size from previous assembling attempt if there was one
const done = (await handle.getFile()).size;
const writable = await handle.createWritable({ keepExistingData: done > 0 });
await writable.seek(done);
// check at each step the chunk names (ranges) are coherent
let written = done;
while (written < total) {
const name = String(written);
const part = await dir.getFileHandle(name).catch(() => null);
if (!part) break;
const file = await part.getFile();
if (!file.size) break;
await writable.write(file);
written += file.size;
await dir.removeEntry(name).catch(() => {});
}
await writable.close();
partials.delete(streamId);
await sweep(streamId);
// chunks went missing, shit's fucked...
if (written !== total) throw new Error(`Assembled ${written} of ${total} bytes`);
}
/** returns directory handle dedicated to partial downloads of given stream id */
async function partsDir(streamId: string, create = false) {
const root = await navigator.storage.getDirectory();
const parts = await root.getDirectoryHandle(PARTS_DIR, { create });
return parts.getDirectoryHandle(streamId, { create });
}
/** discard everything on disk for a stream that is not fully downloaded */
async function discard(streamId: string) {
partials.delete(streamId);
const root = await navigator.storage.getDirectory();
await root.removeEntry(streamId).catch(() => {});
await sweep(streamId);
}
/** remove unreferenced chunk directories (that aren't still downloading)
* `force` is there for `discard()` to use as an escape hatch, otherwise it'd be caught in
* the guard condition due to still being in `downloading` by then
*/
async function sweep(force?: string) {
const root = await navigator.storage.getDirectory();
const parts = await root.getDirectoryHandle(PARTS_DIR, { create: true });
const orphans: string[] = [];
for await (const name of parts.keys()) {
if (partials.has(name)) continue;
if (downloading.has(name) && name !== force) continue;
orphans.push(name);
}
for (const name of orphans) await parts.removeEntry(name, { recursive: true }).catch(() => {});
await refreshUsage();
}
async function refreshUsage() {
usage.bytes = (await navigator.storage.estimate()).usage;
}
/** exponential backoff, cut short if the connection comes back before it elapses */
function waitForRetry(attempt: number) {
return new Promise<void>((resolve) => {
const done = () => {
removeEventListener('online', done);
clearTimeout(timer);
resolve();
};
const timer = setTimeout(done, 2 ** attempt * 1000);
addEventListener('online', done);
});
}
function percent(received: number, total: number) {
return total ? Math.round((received / total) * 100) : 0;
}
+35 -34
View File
@@ -2,46 +2,47 @@ import { createContext } from 'svelte';
import type { Stream, StreamSummary } from './types.ts';
export class StreamContext {
streams: StreamSummary[];
current = $state<Stream | null>(null);
songIndex = $state<number | null>(null);
#timestamps: number[] = [];
streams: StreamSummary[];
current = $state<Stream | null>(null);
songIndex = $state<number | null>(null);
#timestamps: number[] = [];
constructor(streams: StreamSummary[]) {
this.streams = streams;
}
constructor(streams: StreamSummary[]) {
this.streams = streams;
}
setCurrent(stream: Stream) {
this.current = stream;
this.#timestamps = [-Infinity, ...stream.tracks.map((t) => t[0]), Infinity];
this.songIndex = 0;
}
setCurrent(stream: Stream) {
this.current = stream;
this.#timestamps = [-Infinity, ...stream.tracks.map((t) => t[0]), Infinity];
this.songIndex = 0;
}
clearCurrent() {
this.current = null;
this.songIndex = null;
}
clearCurrent() {
this.current = null;
this.songIndex = null;
}
getSongAtTime(time: number): number {
return this.#locationOf(time, this.#timestamps) - 1;
}
getSongAtTime(time: number): number {
return Math.max(this.#locationOf(time, this.#timestamps) - 1, 0);
}
updateCurrentSong(currentTime: number, songIndex: number) {
const ts = this.#timestamps;
const recurse = (idx: number): number => {
if (currentTime >= ts[idx + 2]) return recurse(idx + 1);
if (currentTime < ts[idx + 1]) return recurse(idx - 1);
return idx;
};
this.songIndex = recurse(songIndex);
}
updateCurrentSong(currentTime: number, songIndex: number) {
const ts = this.#timestamps;
// the first track need not start at 0, so descending past index 0 is possible
const recurse = (idx: number): number => {
if (currentTime >= ts[idx + 2]) return recurse(idx + 1);
if (idx > 0 && currentTime < ts[idx + 1]) return recurse(idx - 1);
return idx;
};
this.songIndex = recurse(songIndex);
}
#locationOf(element: number, array: number[], start = 0, end = array.length): number {
const pivot = Math.floor(start + (end - start) / 2);
if (end - start <= 1 || array[pivot] === element) return pivot;
if (array[pivot] < element) return this.#locationOf(element, array, pivot, end);
return this.#locationOf(element, array, start, pivot);
}
#locationOf(element: number, array: number[], start = 0, end = array.length): number {
const pivot = Math.floor(start + (end - start) / 2);
if (end - start <= 1 || array[pivot] === element) return pivot;
if (array[pivot] < element) return this.#locationOf(element, array, pivot, end);
return this.#locationOf(element, array, start, pivot);
}
}
export const [getStreamContext, setStreamContext] = createContext<StreamContext>();
+21 -9
View File
@@ -1,19 +1,31 @@
/** A track entry: [timestamp_seconds, artist, title] */
export type Track = [number, string, string];
// tag -> weight, 1 (light) to 3 (heavy)
export type TagWeights = Record<string, number>;
/** The summary shape used in sidebar listings. */
export interface StreamSummary {
id: string;
stream_date: number;
filename: string;
title: string | null;
tags: string[];
length_seconds: number;
id: string;
stream_date: number;
filename: string;
title: string | null;
tags: TagWeights;
length_seconds: number;
}
/** The full stream shape used on the detail/player page. */
export interface Stream extends StreamSummary {
format: string;
description: string | null;
tracks: Track[];
format: string;
description: string | null;
// rendered at page load, so absent straight out of the database
descriptionHtml?: string;
tracks: Track[];
}
/** Partial stream downloads, as well as associated etag for cache validation. */
export interface PartialDownload {
received: number;
total: number;
etag: string | null;
}
+19
View File
@@ -1,7 +1,26 @@
import { browser } from '$app/environment';
import Sqids from 'sqids';
const sqids = new Sqids({ minLength: 6, alphabet: 'abcdefghijklmnopqrstuvwxyz0123456789' });
// browser blocks site data => localStorage throws
// so we're wrapping our calls to ensure things running at module init
// don't bring down the site render entirely
export function readStored<T>(key: string, fallback: T): T {
if (!browser) return fallback;
try {
return (JSON.parse(localStorage.getItem(key) ?? '') as T) ?? fallback;
} catch {
return fallback;
}
}
export function writeStored(key: string, value: unknown) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {}
}
export function hashcode(str: string): number {
let h = 9;
for (let i = 0; i < str.length; ) h = Math.imul(h ^ str.charCodeAt(i++), 9 ** 9);
+4
View File
@@ -8,6 +8,10 @@
<svelte:head>
<style>
:root {
--accent: #add8e6;
}
body,
html {
margin: 0;
+2 -1
View File
@@ -1,12 +1,13 @@
<script lang="ts">
import Sidebar from './Sidebar.svelte';
import Footer from './Footer.svelte';
import { untrack } from 'svelte';
import { setStreamContext, StreamContext } from '$lib/streamContext.svelte.ts';
// streams are grabbed from the server here, then accessed throughout the rest
// of the components through the svelte context api
const { data, children } = $props();
setStreamContext(new StreamContext(data.streams));
setStreamContext(new StreamContext(untrack(() => data.streams)));
</script>
<div id="mainContainer">
-1
View File
@@ -38,7 +38,6 @@
a,
a:visited {
outline: none;
color: black;
}
</style>
+168 -35
View File
@@ -1,46 +1,88 @@
<script lang="ts">
import { untrack } from 'svelte';
import { goto } from '$app/navigation';
import { favoritedStreams } from '$lib/stores.svelte.ts';
import { favoritedStreams, tagList, MAX_TAG_WEIGHT } from '$lib/stores.svelte.ts';
import * as streamCache from '$lib/streamCache.svelte.ts';
import { getStreamContext } from '$lib/streamContext.svelte.ts';
import type { StreamSummary } from '$lib/types';
import { hashColor, shorthandCode, formatSecondsToHms, formatDate } from '$lib/utils.ts';
import { shorthandCode, formatSecondsToHms, formatDate } from '$lib/utils.ts';
import TagSelect from './TagSelect.svelte';
import WeightedTags from '$lib/components/WeightedTags.svelte';
const ctx = getStreamContext();
let filteredTags = $state([]);
// map of tag -> minimum weight
// a stream shows if it has every tag with at least the minimum weight
let filteredTags: Record<string, number> = $state({});
let favoritesOnly = $state(false);
let cachedOnly = $state(false);
let listOpen = $state();
let listOpen = $state(false);
let displayedStreams = $derived.by(streamsToDisplay);
let remainingTags = $derived.by(getRemainingTags);
// how many displayed streams have each unselected tag, at each weight?
// used in the tag select dropdown
let dropdownCounts = $derived.by(getDropdownCounts);
// how many displayed streams would be shown
// if filtered per each of this tag's weights,
// all other filters unchanged?
// used for the selected tag pills
let pillCounts = $derived.by(getPillCounts);
// which tags can still alter the displayed streams if selected?
// we eliminate either those which aren't present in the displayed streams
// or those present (at the same weight) in every displayed stream
// decides which tags are visible in the selection dropdown
let remainingTags = $derived(
Object.keys(dropdownCounts).filter((tag) =>
dropdownCounts[tag].some((count) => count > 0 && count < displayedStreams.size)
)
);
let cacheUsage = $derived(streamCache.usage.bytes);
$effect(() => {
if (displayedStreams.length == 1) {
listOpen = untrack(() => !listOpen);
if (displayedStreams.size == 1) {
listOpen = false;
}
});
function matchesFilter(stream: StreamSummary, filters: Record<string, number>) {
return Object.entries(filters).every(([tag, min]) => (stream.tags[tag] ?? 0) >= min);
}
function streamsToDisplay() {
return ctx.streams.filter(
(stream) =>
!('tags' in stream) || filteredTags.every((tag) => stream.tags.includes(tag))
return new Set(ctx.streams.filter((stream) => matchesFilter(stream, filteredTags)));
}
// how many of these streams have the tag at weight 1+, 2+ and 3+
// e.g. dark at weights [1, 3, 3, None] on four streams gives [3, 2, 2]
function weightCounts(streams: Iterable<StreamSummary>, tag: string) {
const counts = new Array(MAX_TAG_WEIGHT).fill(0);
for (const stream of streams) {
const weight = stream.tags[tag] ?? 0;
for (let i = 0; i < weight; i++) counts[i]++;
}
return counts;
}
function getDropdownCounts() {
const unselected = tagList.filter((tag) => !(tag in filteredTags));
return Object.fromEntries(
unselected.map((tag) => [tag, weightCounts(displayedStreams, tag)])
);
}
function getRemainingTags() {
let tagCounts = displayedStreams.reduce((tags: Record<string, number>, stream) => {
stream.tags.forEach((tag) => (tags[tag] ? (tags[tag] += 1) : (tags[tag] = 1)));
return tags;
}, {});
return Object.entries(tagCounts)
.filter((el) => el[1] != displayedStreams.length)
.map((el) => el[0]);
}
function getPillCounts() {
return Object.fromEntries(
Object.keys(filteredTags).map((tag) => {
const otherFilters = { ...filteredTags };
delete otherFilters[tag];
function getDisplayedTagsInList(streamTags: string[], remainingTags: string[]) {
return streamTags.filter((tag) => remainingTags.includes(tag));
const candidates = ctx.streams.filter((stream) =>
matchesFilter(stream, otherFilters)
);
return [tag, weightCounts(candidates, tag)];
})
);
}
function updateFavorites(stream: StreamSummary) {
@@ -51,13 +93,25 @@
</script>
<div class="tag-select">
<TagSelect bind:listOpen bind:checked={filteredTags} {remainingTags} />
<TagSelect
bind:listOpen
bind:checked={filteredTags}
{remainingTags}
{dropdownCounts}
{pillCounts}
/>
<button
onclick={() => (cachedOnly = !cachedOnly)}
class="material-icons filter-download {cachedOnly ? 'select-faves-star' : 'unselect-faves-star'}">file_download</button
aria-label="Downloaded only"
aria-pressed={cachedOnly}
class="material-icons filter-download {cachedOnly
? 'select-faves-star'
: 'unselect-faves-star'}">file_download</button
>
<button
onclick={() => (favoritesOnly = !favoritesOnly)}
aria-label="Favorites only"
aria-pressed={favoritesOnly}
class="material-icons {favoritesOnly ? '' : 'un'}select-faves-star">star</button
>
</div>
@@ -66,13 +120,20 @@
{#each ctx.streams as stream}
{@const favorited = favoritedStreams.has(stream.id)}
{@const isCached = streamCache.cached.has(stream.id)}
{@const isPartial = streamCache.partials.has(stream.id)}
{@const current = ctx.current?.id === stream.id}
<li
hidden={!displayedStreams.includes(stream) || (favoritesOnly && !favorited) || (cachedOnly && !isCached)}
hidden={!displayedStreams.has(stream) ||
(favoritesOnly && !favorited) ||
(cachedOnly && !(isCached || isPartial))}
class="stream-item {current ? 'current-stream' : ''}"
id="stream-{stream.id}"
>
<button class="stream-item-button" onclick={() => goto('/streams/' + stream.id)}>
<button
class="stream-item-button"
aria-current={current ? 'page' : undefined}
onclick={() => goto('/streams/' + stream.id)}
>
<span class="stream-item-id">
ID:
{shorthandCode(stream.id)}</span
@@ -82,12 +143,12 @@
<span class="stream-item-length">{formatSecondsToHms(stream.length_seconds)}</span>
<p class="stream-item-tags" hidden={!remainingTags.length}>
Tags: <span hidden={!filteredTags.length}>[...] </span>{getDisplayedTagsInList(
stream.tags,
remainingTags
).join(', ')}
</p>
<span class="stream-item-tags" hidden={!Object.keys(stream.tags).length}>
<span class="stream-item-tags-label">Tags:</span>
<span class="stream-item-tags-list"
><WeightedTags tags={stream.tags} highlighted={filteredTags} /></span
>
</span>
</button>
<button
@@ -95,6 +156,8 @@
e.stopPropagation();
updateFavorites(stream);
}}
aria-label="Favorite"
aria-pressed={favorited}
class="material-icons stream-item-star {favorited ? 'stream-item-star-faved' : ''}"
>{favorited ? 'star' : 'star_border'}</button
>
@@ -102,19 +165,45 @@
<span class="stream-item-cached">{streamCache.downloading.get(stream.id)}%</span>
{:else if isCached}
<button
onclick={(e) => { e.stopPropagation(); streamCache.remove(stream.id); }}
onclick={(e) => {
e.stopPropagation();
streamCache.remove(stream.id);
}}
aria-label="Delete download"
class="material-icons stream-item-cached stream-item-cached-btn">delete</button
>
{:else}
{@const partial = streamCache.partials.get(stream.id)}
<button
onclick={(e) => { e.stopPropagation(); streamCache.download(stream); }}
class="material-icons filter-download stream-item-cached stream-item-cached-btn">file_download</button
onclick={(e) => {
e.stopPropagation();
streamCache.download(stream);
}}
aria-label={partial ? 'Resume download' : 'Download'}
title={partial
? `Partially downloaded (${Math.round((partial.received / partial.total) * 100)}%). Click to resume.`
: undefined}
class="material-icons filter-download stream-item-cached stream-item-cached-btn {partial
? 'stream-item-partial'
: ''}">file_download</button
>
{/if}
</li>
{/each}
</ul>
{#if cachedOnly && (streamCache.cached.size || streamCache.partials.size)}
<p class="cache-usage">
{#if cacheUsage !== undefined}
{cacheUsage > 1_000_000_000
? (cacheUsage / 1_000_000_000).toFixed(1) + ' GB'
: Math.round(cacheUsage / 1_000_000) + ' MB'} of cached streams
{:else}
Calculating…
{/if}
</p>
{/if}
<style>
.stream-list {
list-style-type: none;
@@ -126,11 +215,27 @@
}
.stream-item-tags {
display: flex;
clear: both;
gap: 0.3em;
margin: 0;
font-family: Tahoma;
font-size: smaller;
}
.stream-item-tags[hidden] {
display: none;
}
.stream-item-tags-label {
flex-shrink: 0;
}
.stream-item-tags-list {
flex-grow: 1;
text-align: center;
}
.stream-item-id {
float: left;
font-family: monospace;
@@ -176,6 +281,13 @@
margin-bottom: 1px;
}
.cache-usage {
margin: 0;
padding: 4px 5px;
font-family: Tahoma;
font-size: smaller;
}
.material-icons {
margin-bottom: 0px;
color: black;
@@ -241,10 +353,31 @@
opacity: 1;
}
.stream-item-cached.stream-item-partial {
color: #c8a415;
opacity: 0.75;
}
.material-icons::-moz-focus-inner {
border: 0;
}
.stream-item .stream-item-star:focus-visible,
.stream-item .stream-item-cached-btn:focus-visible {
opacity: 1;
}
/* touch screens can't hover, so always display */
@media (hover: none) {
.stream-item-star {
opacity: 0.5;
}
.stream-item-cached {
opacity: 0.4;
}
}
@media (max-width: 575px) {
.stream-item-star {
opacity: 0.5;
+140 -32
View File
@@ -1,42 +1,68 @@
<script lang="ts">
import Select from 'svelte-select';
import { tagList } from '$lib/stores.svelte.ts';
import { tagList, MAX_TAG_WEIGHT, TAG_WEIGHT_NAMES } from '$lib/stores.svelte.ts';
let { checked = $bindable([]), remainingTags = [], listOpen = $bindable() } = $props();
// type not exported in the lib itself
type SelectItem = Record<string, unknown>;
type Props = {
// tag -> minimum weight
checked: Record<string, number>;
// unselected tags that can still narrow the list
remainingTags: string[];
// unselected tag -> [displayed streams with it at weight 1+, 2+, 3+]
dropdownCounts: Record<string, number[]>;
// selected tag -> [streams shown if its minimum weight were 1, 2, 3]
pillCounts: Record<string, number[]>;
listOpen: boolean;
};
let {
checked = $bindable({}),
remainingTags = [],
dropdownCounts = {},
pillCounts = {},
listOpen = $bindable()
}: Props = $props();
let items = $state(tagList.map((x) => ({ value: x, label: x })));
let isChecked = $derived.by(computeIsChecked);
let value = $derived.by(computeValue);
function computeIsChecked() {
return checked.reduce((acc, c) => {
acc[c] = true;
return acc;
}, {});
}
function computeValue() {
return checked.map((c) => items.find((i) => i.value === c));
return Object.keys(checked).map((c) => items.find((i) => i.value === c));
}
function handleSelectable() {
items = items.map((item) => {
return { ...item, selectable: !checked.includes(item.value) };
return { ...item, selectable: !(item.value in checked) };
});
}
function handleChange(e) {
if (e.type === 'clear' && Array.isArray(e.detail)) checked = [];
else
checked.includes(e.detail.value)
? (checked = checked.filter((i) => i != e.detail.value))
: (checked = [...checked, e.detail.value]);
// item selected or deleted => passes every selected item as a list
// removing last pill or clearing all => passes undefined
function handleInput(selected: unknown) {
const tags = ((selected ?? []) as SelectItem[]).map((item) => item.value as string);
checked = Object.fromEntries(tags.map((tag) => [tag, checked[tag] ?? 1]));
handleSelectable();
}
let itemFilter = function (label, filterText, option) {
// maximum weight that matches at least one stream for this tag with the current filters
function ceilingOf(tag: string) {
return pillCounts[tag].filter((count) => count > 0).length;
}
// increment the weight threshold for this tag
// and cycles back to 1 once we hit ceilingOf
function cycle(tag: string, back: boolean) {
const min = checked[tag];
const ceiling = ceilingOf(tag);
const next = back ? (min > 1 ? min - 1 : ceiling) : min < ceiling ? min + 1 : 1;
checked = { ...checked, [tag]: next };
}
let itemFilter = function (label: unknown, filterText: string, option: SelectItem) {
return (
remainingTags.includes(option['value']) &&
label.toLowerCase().includes(filterText.toLowerCase())
remainingTags.includes(option.value as string) &&
String(label).toLowerCase().includes(filterText.toLowerCase())
);
};
</script>
@@ -49,28 +75,110 @@
placeholder="Tag Filter..."
filterSelectedItems={true}
closeListOnChange={false}
on:select={handleChange}
on:clear={handleChange}
oninput={handleInput}
{itemFilter}
--max-height="42px"
--background="rgba(255, 255, 255, 0.1)"
--border="1px solid rgba(255, 255, 255, 0.3)"
--border-hover="1px solid rgba(255, 255, 255, 0.3)"
--border-focused="1px solid var(--accent)"
--item-color="white"
--placeholder-color="rgba(255, 255, 255, 0.5)"
--icons-color="rgba(255, 255, 255, 0.6)"
--list-background="#3a3a3a"
--list-border="1px solid rgba(255, 255, 255, 0.3)"
--list-shadow="none"
--item-hover-bg="rgba(255, 255, 255, 0.15)"
--item-hover-color="white"
--item-transition="none"
--item-height="30px"
--item-line-height="30px"
--item-active-background="rgba(173, 216, 230, 0.3)"
--multi-item-bg="rgba(255, 255, 255, 0.15)"
--multi-item-outline="none"
--multi-item-clear-icon-color="white"
listOffset={0}
>
{#snippet item({ item })}
<div class="item">
<label for={item.value}>
<input type="checkbox" id={item.value} checked={isChecked[item.value]} />
{item.label}
</label>
</div>
{#snippet item({ item }: { item: SelectItem })}
{item.label}<span class="count">{dropdownCounts[item.value as string][0]}</span>
{/snippet}
{#snippet empty()}
<div class="empty">(´・ω・`)</div>
{/snippet}
{#snippet selection({ selection }: { selection: SelectItem })}
{@const tag = selection.value as string}
{#if tag in checked}
{@const min = checked[tag]}
{@const ceiling = ceilingOf(tag)}
<button
type="button"
class="weight"
aria-label="{selection.label}, at least {TAG_WEIGHT_NAMES[min]}, press to change"
onpointerup={(e) => e.stopPropagation()}
onclick={(e) => cycle(tag, e.shiftKey)}
>
{selection.label}<span class="dots"
>{#each { length: MAX_TAG_WEIGHT }, i}<span
class:headroom={i >= min}
class:spacer={i >= ceiling}>·</span
>{/each}</span
>
</button>
{:else}
{selection.label}
{/if}
{/snippet}
</Select>
<style>
.item {
pointer-events: none;
.count {
float: right;
opacity: 0.4;
}
.empty {
text-align: center;
padding: 6px 0;
color: rgba(255, 255, 255, 0.5);
}
.weight {
background: none;
border: none;
padding: 0;
font: inherit;
color: inherit;
cursor: pointer;
}
.weight:hover {
color: var(--accent);
}
.dots {
margin-left: 3px;
color: var(--accent);
font-weight: bold;
letter-spacing: 1px;
}
/* to avoid pills' size shifting as tags are selected */
.headroom {
opacity: 0.3;
}
.spacer {
visibility: hidden;
}
:global(.svelte-select:hover) {
max-height: unset !important;
}
:global(.svelte-select-list) {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
}
</style>
+16 -1
View File
@@ -11,7 +11,22 @@
</div>
<div class="description-bubble">
<p>Hello, there, welcome to V1.5.</p>
<p>Hello, there, welcome to V1.7.</p>
<p>
<u>2026-09-06 update:</u> caching behavior has been greatly improved when trying to download
streams in poor network conditions.<br />
You can now also resume interrupted downloads and see how much disk space is being used when the
downloads-only filter is enabled.<br />
I've done a bit of refactoring to the codebase, so don't hesitate to message me if you're having
any weird issues!<br /><br />
<u>2026-03-16 update:</u> you can now cache streams offline for playback in low-connectivity
environments (and to prevent buffering)!<br />
Simply click on the download icon in the stream list (and click again to remove them from cache).
You can also filter by cached streams at the top. This all goes into your browser storage, and
a stream is generally 100~250MB depending on its length.
</p>
<hr />
<p>
Still in construction. The design is responsive now, so phones should work well enough!
Needs touch events though.
+19 -10
View File
@@ -1,12 +1,17 @@
import fs from 'fs';
import path from 'path';
import fs from 'node:fs';
import path from 'node:path';
import { error } from '@sveltejs/kit';
import { getStreamInfo } from '$lib/database.ts';
import { dev } from '$app/environment';
import { STREAM_JSON_LOCATION } from '$env/static/private';
import type { Actions } from './$types';
import type { TagWeights } from '$lib/types';
let getOriginalJson, writeSideloadJson;
export let actions;
type SideloadJson = { title: unknown; description: unknown; tags: unknown };
let getOriginalJson: (streamId: string) => string;
let writeSideloadJson: (streamId: string, newJson: SideloadJson) => void;
export let actions: Actions | undefined;
// utilities for manipulating original stream JSONs
if (dev) {
@@ -18,7 +23,7 @@ if (dev) {
const jsonString = fs.readFileSync(filePath, 'utf8');
return jsonString;
} else {
return JSON.stringify({ title: '', description: '', tags: [] });
return JSON.stringify({ title: '', description: '', tags: {} });
}
};
@@ -30,11 +35,13 @@ if (dev) {
actions = {
default: async ({ params, request }) => {
const data = await request.formData();
// let newJson = JSON.parse(getOriginalJson(params.stream_id));
const newJson = {};
newJson['title'] = data.get('title');
newJson['description'] = data.get('description');
newJson['tags'] = data.getAll('tags');
const weights = JSON.parse(String(data.get('tags'))) as TagWeights;
const newJson: SideloadJson = {
title: data.get('title'),
description: data.get('description'),
// weight 0 means the tag is off
tags: Object.fromEntries(Object.entries(weights).filter(([, weight]) => weight > 0))
};
writeSideloadJson(params.stream_id, newJson);
}
};
@@ -46,6 +53,8 @@ export function load({ params }) {
error(404);
}
result.descriptionHtml = Bun.markdown.html(result.description || 'No description available.');
if (dev) {
// pass raw JSON for metadata editor
const original = JSON.parse(getOriginalJson(params.stream_id));
+39 -15
View File
@@ -2,26 +2,49 @@
import StreamPage from './StreamPage.svelte';
import MetadataEditor from './MetadataEditor.svelte';
import Player from './Player.svelte';
import { untrack } from 'svelte';
import { dev } from '$app/environment';
import { getStreamContext } from '$lib/streamContext.svelte.ts';
import * as streamCache from '$lib/streamCache.svelte.ts';
let { data } = $props();
const ctx = getStreamContext();
let playerSrc = $state<string | null>(null);
// reactivity runs on `stream` and `streamCache.cached` here
$effect(() => {
const stream = data.stream;
ctx.setCurrent(stream);
playerSrc = null;
if (streamCache.cached.has(stream.id)) {
streamCache.getUrl(stream.id).then((url) => {
playerSrc = url ?? `/media/tracks/${stream.filename}`;
});
} else {
playerSrc = `/media/tracks/${stream.filename}`;
let source = $state<{ id: string; url: string } | null>(null);
const streamId = $derived(data.stream.id);
const isCached = $derived(streamCache.cached.has(streamId));
// update stream context only when we actually swap streams
// this avoids reloading the audio element when we click on the same stream
$effect.pre(() => {
streamId;
ctx.setCurrent(untrack(() => data.stream));
});
$effect.pre(() => {
const id = streamId;
const cached = isCached;
const networkUrl = untrack(() => `/media/tracks/${data.stream.filename}`);
// not in cache (or just deleted from cache)? use network url
if (!cached) {
source = { id, url: networkUrl };
return;
}
// otherwise use the cached blob
// if the cache finished downloading while the stream is playing,
// Player.svelte will notice and swap it in at the right time uninterrupted
let objectUrl: string | null = null;
streamCache.getUrl(id).then((url) => {
objectUrl = url;
source = { id, url: url ?? networkUrl };
});
return () => {
// cleanup blob handle when switching away from cached stream
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
});
</script>
@@ -33,9 +56,10 @@
<StreamPage />
</div>
<div id="player">
{#key playerSrc}
{#if playerSrc}
<Player display={true} src={playerSrc} />
{#key streamId}
<!-- until streamCache.getUrl() resolves, source still points at the previous stream -->
{#if source?.id === streamId}
<Player display={true} src={source.url} />
{/if}
{/key}
</div>
@@ -1,24 +1,20 @@
<script lang="ts">
import { page } from '$app/stores';
import { tagList } from '$lib/stores.svelte.ts';
import { page } from '$app/state';
import { tagList, MAX_TAG_WEIGHT, TAG_WEIGHT_NAMES } from '$lib/stores.svelte.ts';
import type { TagWeights } from '$lib/types';
let { original = $bindable() } = $props();
let tagMap = new Map();
let weights: TagWeights = $derived({ ...original.tags });
// Create a mapping of tags and their checked status
let reloadTags = (original) => {
tagList.forEach((tag) => {
tagMap.set(tag, original.tags.includes(tag));
});
};
$effect.pre(() => {
reloadTags(original);
});
// off -> light -> medium -> heavy -> off, backwards with shift
function cycle(tag: string, back: boolean) {
const step = back ? MAX_TAG_WEIGHT : 1;
weights = { ...weights, [tag]: ((weights[tag] ?? 0) + step) % (MAX_TAG_WEIGHT + 1) };
}
</script>
{#key $page.url.pathname}
{#key page.url.pathname}
<form method="POST">
<br />
<label>
@@ -36,17 +32,24 @@
></textarea>
</label>
<br />
<label>
<div class="tags">
<p>Tags:</p>
<div class="checkboxContainer">
<input type="hidden" name="tags" value={JSON.stringify(weights)} />
<div class="tag-pills">
{#each tagList as tag}
<label>
<input type="checkbox" name="tags" value={tag} checked={tagMap.get(tag)} />
{tag}
</label>
{@const weight = weights[tag] ?? 0}
<button
type="button"
class="tag-pill"
class:off={!weight}
aria-label="{tag}, {TAG_WEIGHT_NAMES[weight]}"
onclick={(e) => cycle(tag, e.shiftKey)}
>
{tag}<span class="dots">{'·'.repeat(weight)}</span>
</button>
{/each}
</div>
</label>
</div>
<br />
<button type="submit">Submit</button>
@@ -58,19 +61,54 @@
margin-left: 10px;
}
label {
label,
.tags {
display: flex;
align-items: center;
}
label > p {
label > p,
.tags > p {
margin-right: 5px;
}
.checkboxContainer {
.tag-pills {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1px;
max-width: 50%;
padding: 4px 8px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
.tag-pill {
background: none;
border: none;
padding: 2px 0;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
user-select: none;
}
.tag-pill.off {
opacity: 0.35;
}
.tag-pill:hover {
color: var(--accent);
}
.tag-pill.off:hover {
opacity: 0.65;
color: inherit;
}
.dots {
margin-left: 2px;
color: var(--accent);
font-weight: bold;
}
</style>
+163 -92
View File
@@ -2,36 +2,34 @@
taken from https://github.com/Linkcube/svelte-audio-controls
ISC License
-->
<svelte:options />
<script module>
let getAudio = null;
<script module lang="ts">
let getAudio: (() => HTMLAudioElement) | null = null;
export function jumpToTrack(s) {
getAudio().currentTime = s;
getAudio().play();
export function jumpToTrack(s: number) {
const el = getAudio?.();
if (!el) return;
el.currentTime = s;
el.play();
}
</script>
<script lang="ts">
import { run, createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import { onMount, onDestroy } from 'svelte';
import { onMount, untrack } from 'svelte';
import { fade } from 'svelte/transition';
import { getStreamContext } from '$lib/streamContext.svelte.ts';
import { readStored, writeStored } from '$lib/utils.ts';
import type { Track } from '$lib/types';
const ctx = getStreamContext();
interface Props {
src: any;
audio?: any;
paused?: boolean;
duration?: number;
muted?: boolean;
volume?: number;
preload?: string;
preload?: 'none' | 'metadata' | 'auto';
iconColor?: string;
textColor?: string;
barPrimaryColor?: string;
@@ -40,11 +38,11 @@
display?: boolean;
inlineTooltip?: boolean;
disableTooltip?: boolean;
onended?: () => void;
}
let {
src,
audio = $bindable(null),
paused = $bindable(true),
duration = $bindable(0),
muted = $bindable(false),
@@ -57,15 +55,34 @@
backgroundColor = 'white',
display = false,
inlineTooltip = false,
disableTooltip = false
disableTooltip = false,
onended
}: Props = $props();
getAudio = () => {
return audio;
};
let audio = $state<HTMLAudioElement>()!;
// if src changes on the same stream id (in practice when a stream cache finishes downloading),
// save current playhead, then call load() to use the new src
// playhead is restored in the onLoadedMetadata below in this file
let pending: { time: number; playing: boolean } | null = null;
$effect(() => {
src;
if (!audio) return;
pending =
audio.currentTime > 0 ? { time: audio.currentTime, playing: !audio.paused } : null;
audio.load();
});
// module scope, so it has to be released or a destroyed instance keeps answering
$effect(() => {
getAudio = () => audio;
return () => {
getAudio = null;
};
});
let currentTime = $state(0);
let tooltip = $state();
let tooltip = $state<HTMLElement>();
let tooltipX = $state(0);
let tooltipY = $state(0);
let showTooltip = $state(false);
@@ -74,16 +91,34 @@
let seeking = $state(false);
let volumeSeeking = $state(false);
let pendingSeekTime = $state<number | null>(null);
let songBar = $state();
let volumeBar = $state();
let innerWidth = $state();
let innerHeight = $state();
let songBar = $state<HTMLProgressElement>();
let volumeBar = $state<HTMLProgressElement>();
let innerWidth = $state(0);
let innerHeight = $state(0);
let isSafari = $state(false);
onMount(() => {
// work around coreaudio bug
// see https://git.webbieweb.org/apt-get/strimserve/issues/1
const ua = navigator.userAgent;
const isIOS = /iPad|iPhone|iPod/.test(ua);
const isDesktopSafari = /Safari/.test(ua) && !/Chrome/.test(ua) && !/Chromium/.test(ua);
isSafari = isIOS || isDesktopSafari;
onMount(async () => {
// default volume
const volumeData = localStorage.getItem('volume');
volume = volumeData ? parseFloat(volumeData) : 0.67;
volume = readStored('volume', 0.67);
setVolume(volume);
// set actions for previous/next track media buttons
// mediaSession is global, so handlers of a destroyed instance would keep firing
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('previoustrack', previousTrack);
navigator.mediaSession.setActionHandler('nexttrack', nextTrack);
return () => {
navigator.mediaSession.setActionHandler('previoustrack', null);
navigator.mediaSession.setActionHandler('nexttrack', null);
};
}
});
// update browser metadata on track changes
@@ -93,14 +128,34 @@
}
});
function seek(event, bounds) {
let x = event.pageX - bounds.left;
function previousTrack() {
if (!ctx.current || ctx.songIndex == null) return;
const tracks = ctx.current.tracks;
const trackStart = tracks[ctx.songIndex][0];
// if more than 3s into the track, restart it; otherwise go to previous
if (audio.currentTime - trackStart > 3 || ctx.songIndex === 0) {
audio.currentTime = currentTime = trackStart;
} else {
audio.currentTime = currentTime = tracks[ctx.songIndex - 1][0];
}
}
function nextTrack() {
if (!ctx.current || ctx.songIndex == null) return;
const tracks = ctx.current.tracks;
if (ctx.songIndex < tracks.length - 1) {
audio.currentTime = currentTime = tracks[ctx.songIndex + 1][0];
}
}
function seek(event: MouseEvent, bounds: DOMRect) {
let x = event.clientX - bounds.left;
return Math.min(Math.max(x / bounds.width, 0), 1);
}
// exponential volume bar
// default is linear, which doesn't correspond to human hearing
function setVolume(volume) {
function setVolume(volume: number) {
if (volume != 0) {
audio.volume = Math.pow(10, 2.5 * (volume - 1));
} else {
@@ -122,109 +177,108 @@
}
}
// workaround for bug https://github.com/sveltejs/svelte/issues/5347
// partly workaround for bug https://github.com/sveltejs/svelte/issues/5347
// need to init duration & volume after SSR first load
function updateAudioAttributes(audio) {
if (audio && audio.duration) {
// also workaround for bug https://github.com/sveltejs/svelte/issues/5914
$effect(() => {
if (!audio) return;
const onLoadedMetadata = () => {
duration = audio.duration;
if (pending) {
// same stream but audio url changed, reload audio at current playhead
audio.currentTime = currentTime = pending.time;
if (pending.playing) audio.play().catch(() => {});
pending = null;
}
paused = audio.paused;
setVolume(volume);
};
// workaround for bug https://github.com/sveltejs/svelte/issues/5914
audio.addEventListener('loadedmetadata', (event) => {
paused = audio.paused;
});
}
}
if (audio.duration) onLoadedMetadata();
function seekAudio(event) {
if (!songBar) return;
audio.currentTime = seek(event, songBar.getBoundingClientRect()) * duration;
}
audio.addEventListener('loadedmetadata', onLoadedMetadata);
return () => audio.removeEventListener('loadedmetadata', onLoadedMetadata);
});
function updateSeekVisual(event) {
function updateSeekVisual(event: MouseEvent) {
if (!songBar) return;
pendingSeekTime = seek(event, songBar.getBoundingClientRect()) * duration;
}
function seekVolume(event) {
function seekVolume(event: MouseEvent) {
if (!volumeBar) return;
volume = seek(event, volumeBar.getBoundingClientRect());
setVolume(volume);
localStorage.setItem('volume', volume.toString());
writeStored('volume', volume);
muted = false;
}
function formatSeconds(totalSeconds) {
function formatSeconds(totalSeconds: number, forceHours = false) {
if (isNaN(totalSeconds)) return 'No Data';
totalSeconds = parseInt(totalSeconds, 10);
var hours = Math.floor(totalSeconds / 3600);
var minutes = Math.floor(totalSeconds / 60) % 60;
var seconds = totalSeconds % 60;
totalSeconds = Math.trunc(totalSeconds);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor(totalSeconds / 60) % 60;
const seconds = totalSeconds % 60;
return [hours, minutes, seconds]
.map((v) => (v < 10 ? '0' + v : v))
.filter((v, i) => v !== '00' || i > 0)
.filter((v, i) => v !== '00' || i > 0 || forceHours)
.join(':');
}
function seekTooltip(event) {
if (!inlineTooltip) {
function seekTooltip(event: MouseEvent) {
if (!songBar) return;
if (!inlineTooltip && tooltip) {
let tooltipBounds = tooltip.getBoundingClientRect();
tooltipX = Math.min(event.pageX + 10, innerWidth - tooltipBounds.width);
tooltipX = Math.min(event.clientX + 10, innerWidth - tooltipBounds.width);
tooltipY = Math.min(songBar.offsetTop - 30, innerHeight - tooltipBounds.height);
}
let bounds = songBar.getBoundingClientRect();
let seekValue = ((event.pageX - bounds.left) * duration) / bounds.width;
let seekValue = ((event.clientX - bounds.left) * duration) / bounds.width;
updateTooltipText(seekValue);
}
function updateTooltipText(seekValue: number) {
if (!ctx.current) return;
let trackArray = ctx.current.tracks[ctx.getSongAtTime(seekValue)];
seekTrack = trackArray[1] + ' - ' + trackArray[2];
seekText = formatSeconds(seekValue);
}
function trackMouse(event) {
function trackPointer(event: MouseEvent) {
if (seeking) updateSeekVisual(event);
if (showTooltip && !disableTooltip) seekTooltip(event);
if (volumeSeeking) seekVolume(event);
}
run(() => {
if (ctx.songIndex != null) {
ctx.updateCurrentSong(currentTime, ctx.songIndex);
}
});
run(() => {
updateAudioAttributes(audio);
});
export {
src,
audio,
paused,
duration,
muted,
volume,
preload,
iconColor,
textColor,
barPrimaryColor,
barSecondaryColor,
backgroundColor,
display,
inlineTooltip,
disableTooltip
};
$effect(() => {
const time = currentTime;
untrack(() => {
if (ctx.songIndex != null) {
ctx.updateCurrentSong(time, ctx.songIndex);
}
});
});
</script>
<svelte:window
bind:innerWidth
bind:innerHeight
onmouseup={() => {
onpointerup={() => {
if (seeking && pendingSeekTime != null) {
audio.currentTime = pendingSeekTime;
currentTime = pendingSeekTime;
updateTooltipText(pendingSeekTime);
pendingSeekTime = null;
}
seeking = volumeSeeking = false;
}}
onmousemove={trackMouse}
onpointercancel={() => {
pendingSeekTime = null;
seeking = volumeSeeking = false;
}}
onpointermove={trackPointer}
/>
{#if display}
@@ -232,6 +286,7 @@
<button
class="material-icons"
style="--icon-color:{iconColor}"
aria-label={paused ? 'Play' : 'Pause'}
onclick={() => (audio.paused ? audio.play() : audio.pause())}
>
{#if paused}
@@ -242,19 +297,25 @@
</button>
<progress
bind:this={songBar}
value={seeking && pendingSeekTime != null ? pendingSeekTime : (currentTime || 0)}
value={seeking && pendingSeekTime != null ? pendingSeekTime : currentTime || 0}
max={duration}
onmousedown={() => (seeking = true)}
onmouseenter={() => (showTooltip = true)}
onmouseleave={() => (showTooltip = false)}
onclick={seekAudio}
onpointerdown={(e) => {
seeking = true;
updateSeekVisual(e);
}}
onpointerenter={() => (showTooltip = true)}
onpointerleave={() => (showTooltip = false)}
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
class="song-progress"
></progress>
<div class="control-times">{formatSeconds(currentTime)}/{formatSeconds(duration)}</div>
<div class="control-times">
{formatSeconds(currentTime, duration >= 3600)}/{formatSeconds(duration)}
</div>
<button
style="--icon-color:{iconColor}"
class="material-icons"
aria-label="Mute"
aria-pressed={muted}
onclick={() => (muted = !muted)}
>
{#if muted}
@@ -270,7 +331,7 @@
<progress
bind:this={volumeBar}
value={volume}
onmousedown={() => (volumeSeeking = true)}
onpointerdown={() => (volumeSeeking = true)}
onclick={seekVolume}
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
class="volume-progress"
@@ -308,11 +369,16 @@
bind:currentTime
{muted}
onplay={setMediaMetadataOnPlay}
onended={bubble('ended')}
{onended}
{preload}
>
<source {src} type="audio/ogg;codecs=opus" />
<source src="{src}.mp3" type="audio/mpeg" />
{#if isSafari}
<source src="{src}.mp3" type="audio/mpeg" />
<source {src} type="audio/ogg;codecs=opus" />
{:else}
<source {src} type="audio/ogg;codecs=opus" />
<source src="{src}.mp3" type="audio/mpeg" />
{/if}
</audio>
<style>
@@ -337,6 +403,9 @@
.control-times {
margin: auto;
margin-right: 5px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
min-width: max-content;
}
.tooltip {
@@ -386,6 +455,8 @@
margin: auto;
margin-left: 5px;
margin-right: 5px;
/* make finger drags seek */
touch-action: none;
}
progress::-webkit-progress-bar {
@@ -2,18 +2,11 @@
import { getStreamContext } from '$lib/streamContext.svelte.ts';
import { shorthandCode, formatTrackTime, formatDate } from '$lib/utils.ts';
import { jumpToTrack } from './Player.svelte';
import { Carta } from 'carta-md';
import DOMPurify from 'isomorphic-dompurify';
const carta = new Carta({
sanitizer: DOMPurify.sanitize
});
import WeightedTags from '$lib/components/WeightedTags.svelte';
const ctx = getStreamContext();
let formattedStreamDate = $derived(
ctx.current ? formatDate(ctx.current.stream_date) : ''
);
let formattedStreamDate = $derived(ctx.current ? formatDate(ctx.current.stream_date) : '');
</script>
<svelte:head>
@@ -26,11 +19,11 @@
<h3 class="stream-id">ID: {shorthandCode(ctx.current.id)}</h3>
<h1 class="stream-date">{formattedStreamDate}</h1>
</div>
<h5 class="stream-tags"><u>Tags</u>: {ctx.current.tags.join(', ')}</h5>
<h5 class="stream-tags"><u>Tags</u>: <WeightedTags tags={ctx.current.tags} /></h5>
</div>
<div class="description-bubble">
{@html carta.renderSSR(ctx.current.description || 'No description available.')}
{@html ctx.current.descriptionHtml}
</div>
<div id="table-container">
@@ -38,12 +31,16 @@
<tbody>
<tr><th>Timestamp</th><th>Artist</th><th>Title</th></tr>
{#each ctx.current.tracks as track, i}
<tr class:current={i == ctx.songIndex}>
<tr
class:current={i == ctx.songIndex}
aria-current={i == ctx.songIndex ? 'true' : undefined}
>
<td class="timestamp-field"
><div class="timestamp-field-flex">
{formatTrackTime(track[0])}
<button
onclick={() => jumpToTrack(track[0])}
aria-label="Play from {formatTrackTime(track[0])}"
class="material-icons"
style="padding: 4px 6px; margin-top: -4px; margin-bottom: -4px; margin-right: -6px"
>fast_forward</button
@@ -187,6 +184,17 @@
border: 0;
}
tr .material-icons:focus-visible {
opacity: 1;
}
/* touch screens can't hover, so always display */
@media (hover: none) {
.material-icons {
opacity: 0.5;
}
}
@media (max-width: 575px) {
.material-icons {
opacity: 0.5;
@@ -1,8 +1,9 @@
import fs from 'fs';
import fs from 'node:fs';
import { json } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export let GET;
export let GET: RequestHandler | undefined;
if (process.env.NODE_ENV === 'development') {
const jsonFolder = './db/stream_json/';
+1
View File
@@ -2,6 +2,7 @@
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"allowImportingTsExtensions": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,