svelte5 migration, formatting cleanup

This commit is contained in:
2025-02-11 13:13:17 +01:00
parent 44875efa7e
commit b5a740c302
32 changed files with 5782 additions and 1374 deletions

View File

@@ -1,11 +1,13 @@
module.exports = {
extends: [
// add more generic rule sets here, such as:
// 'eslint:recommended',
"plugin:svelte/prettier",
],
rules: {
// override/add rules settings here, such as:
// 'svelte/rule-name': 'error'
},
}
extends: [
// add more generic rule sets here, such as:
// 'eslint:recommended',
'plugin:svelte/prettier',
'eslint:recommended',
'plugin:@typescript-eslint/recommended'
],
rules: {
// override/add rules settings here, such as:
// 'svelte/rule-name': 'error'
}
};

View File

@@ -1,10 +1,9 @@
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -12,6 +12,7 @@ bun --bun install
Copy `.env` to `env.tpl` and insert the correct folder locations
## running dev server
```bash
bun --bun run dev
```

View File

@@ -27,7 +27,9 @@
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/bun": "^1.2.2",
"svelte-adapter-bun": "https://github.com/gornostay25/svelte-adapter-bun",
"typescript-svelte-plugin": "^0.3.45",
},
},
},
@@ -121,7 +123,7 @@
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.15", "", {}, "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
@@ -193,6 +195,8 @@
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
"@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=="],
@@ -215,6 +219,8 @@
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@5.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA=="],
@@ -269,6 +275,8 @@
"brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="],
"bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"carta-md": ["carta-md@4.6.7", "", { "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": "^1.4.0", "unified": "^11.0.4" }, "peerDependencies": { "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.1" } }, "sha512-dJFg1SLEBdoXDCfetMjceW+lZ1SfUZ3nsWRttwGUnCx052OPqBYrTP7ticwQqyolcNlR29aMm1neEuNGpYaDWg=="],
@@ -315,6 +323,8 @@
"decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="],
"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=="],
@@ -489,6 +499,8 @@
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="],
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
@@ -597,6 +609,8 @@
"natural-compare-lite": ["natural-compare-lite@1.4.0", "", {}, "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="],
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
"nwsapi": ["nwsapi@2.2.16", "", {}, "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@@ -615,6 +629,8 @@
"parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="],
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
@@ -727,6 +743,8 @@
"svelte-select": ["svelte-select@5.8.3", "", { "dependencies": { "svelte-floating-ui": "1.5.8" } }, "sha512-nQsvflWmTCOZjssdrNptzfD1Ok45hHVMTL5IHay5DINk7dfu5Er+8KsVJnZMJdSircqtR0YlT4YkCFlxOUhVPA=="],
"svelte2tsx": ["svelte2tsx@0.7.34", "", { "dependencies": { "dedent-js": "^1.0.1", "pascal-case": "^3.1.1" }, "peerDependencies": { "svelte": "^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "typescript": "^4.9.4 || ^5.0.0" } }, "sha512-WTMhpNhFf8/h3SMtR5dkdSy2qfveomkhYei/QW9gSPccb0/b82tjHvLop6vT303ZkGswU/da1s6XvrLgthQPCw=="],
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
@@ -761,6 +779,8 @@
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"typescript-svelte-plugin": ["typescript-svelte-plugin@0.3.45", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "svelte2tsx": "~0.7.25" } }, "sha512-65OpqjDdn05Ici3lgZnhmljDKLJb1Mulz5SmUQUFKXDF1Zhs5CQgdp63PkaDghmwhZFxfu/ut9Q8oGD1CKbJow=="],
"undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="],
@@ -827,10 +847,6 @@
"@humanwhocodes/config-array/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="],
"@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"@typescript-eslint/eslint-plugin/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="],
"@typescript-eslint/parser/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="],
@@ -847,9 +863,9 @@
"eslint/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="],
"espree/acorn": ["acorn@8.9.0", "", { "bin": "bin/acorn" }, "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ=="],
"eslint-plugin-svelte/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.4.15", "", {}, "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="],
"esrap/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"espree/acorn": ["acorn@8.9.0", "", { "bin": "bin/acorn" }, "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -859,12 +875,8 @@
"https-proxy-agent/debug": ["debug@4.3.4", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ=="],
"magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"svelte/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
"tsutils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"@eslint/eslintrc/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],

View File

@@ -10,11 +10,11 @@ const schema = fs.readFileSync('schema.sql', 'utf8');
const db = new Database(dbName);
try {
console.log(`Connected to the ${dbName} database.`);
db.exec(schema);
console.log('Schema initialized successfully.');
console.log(`Connected to the ${dbName} database.`);
db.exec(schema);
console.log('Schema initialized successfully.');
} catch (err) {
console.error(err.message);
console.error(err.message);
} finally {
db.close();
db.close();
}

View File

@@ -11,15 +11,15 @@ const db = new Database(dbName);
// Map JSON attribute names to database column names
// Not needed today, but makes schema changes easier
const lookup = {
id: 'id',
date: 'stream_date',
filename: 'filename',
format: 'format',
title: 'title',
tags: 'tags',
description: 'description',
length_seconds: 'length_seconds',
tracks: 'tracks'
id: 'id',
date: 'stream_date',
filename: 'filename',
format: 'format',
title: 'title',
tags: 'tags',
description: 'description',
length_seconds: 'length_seconds',
tracks: 'tracks'
};
// Retrieve existing IDs from Stream
@@ -32,40 +32,39 @@ 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}.`);
if (err) throw err;
console.log(`Found ${files.length} JSON file(s) in ${jsonFolder}.`);
files.forEach(file => {
if (!file.endsWith('.json')) return;
if (file.endsWith('.sideload.json')) return;
files.forEach((file) => {
if (!file.endsWith('.json')) return;
if (file.endsWith('.sideload.json')) return;
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, '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}.`);
}
// 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]);
// 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]);
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 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}.`);
});
});

4327
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +1,49 @@
{
"name": "strimserve",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build && node ./scripts/create_media_symlink.js",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .",
"init_db": "cd db && node ./init_db.js",
"update_db": "cd db && node ./update_db.js",
"generate_iconfont_subset": "cd scripts && bash ./generate_iconfont_subset.sh"
},
"dependencies": {
"@sveltejs/kit": "^2.17.1",
"@types/node": "^20.17.17",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"carta-md": "^4.6.7",
"dotenv": "^16.4.7",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-svelte": "^2.46.1",
"fontkit": "^2.0.4",
"isomorphic-dompurify": "^2.21.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"sqids": "0.3.0",
"svelte": "^5.19.9",
"svelte-check": "^4.1.4",
"svelte-select": "^5.8.3",
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"vite": "^5.4.14"
},
"type": "module",
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"svelte-adapter-bun": "https://github.com/gornostay25/svelte-adapter-bun"
},
"trustedDependencies": ["svelte-adapter-bun"]
"name": "strimserve",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build && node ./scripts/create_media_symlink.js",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"init_db": "cd db && node ./init_db.js",
"update_db": "cd db && node ./update_db.js",
"generate_iconfont_subset": "cd scripts && bash ./generate_iconfont_subset.sh"
},
"dependencies": {
"@sveltejs/kit": "^2.17.1",
"@types/bun": "^1.2.2",
"@types/node": "^20.17.17",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"carta-md": "^4.6.7",
"dotenv": "^16.4.7",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-svelte": "^2.46.1",
"fontkit": "^2.0.4",
"isomorphic-dompurify": "^2.21.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"sqids": "0.3.0",
"svelte": "^5.19.9",
"svelte-check": "^4.1.4",
"svelte-select": "^5.8.3",
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"vite": "^5.4.14"
},
"type": "module",
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"svelte-adapter-bun": "https://github.com/gornostay25/svelte-adapter-bun",
"typescript-svelte-plugin": "^0.3.45"
},
"trustedDependencies": [
"svelte-adapter-bun"
]
}

View File

@@ -5,8 +5,8 @@ import { symlinkSync } from 'fs';
const target = process.env.STREAM_MEDIA_LOCATION;
const linkName = './build/client/media';
try {
symlinkSync(target, linkName);
console.log(`Symlink created: ${linkName} -> ${target}`);
symlinkSync(target, linkName);
console.log(`Symlink created: ${linkName} -> ${target}`);
} catch (err) {
console.error(`Error creating symlink: ${err}`);
console.error(`Error creating symlink: ${err}`);
}

12
src/app.d.ts vendored
View File

@@ -1,12 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

View File

@@ -1,15 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en" translate="no">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -6,31 +6,31 @@ const dbName = './db/strimserve.db';
const db = new Database(dbName);
export function getStreams() {
const indexData = db
.prepare(
'SELECT id, stream_date, title, tags, length_seconds ' + 'FROM Stream ORDER BY id DESC'
)
.all();
indexData.forEach((stream) => {
stream['stream_date'] = Date.parse(stream['stream_date']);
stream['tags'] = JSON.parse(stream['tags']);
});
return indexData;
const indexData = db
.prepare(
'SELECT id, stream_date, title, tags, length_seconds ' + 'FROM Stream ORDER BY id DESC'
)
.all();
indexData.forEach((stream) => {
stream['stream_date'] = Date.parse(stream['stream_date']);
stream['tags'] = JSON.parse(stream['tags']);
});
return indexData;
}
export function getStreamInfo(streamId) {
const streamData = db
.prepare(
'SELECT id, stream_date, filename, ' +
'format, title, description, tags, ' +
'length_seconds, tracks FROM Stream ' +
'WHERE id = ?'
)
.get(streamId);
if (streamData) {
streamData['stream_date'] = Date.parse(streamData['stream_date']);
streamData['tracks'] = JSON.parse(streamData['tracks']);
streamData['tags'] = JSON.parse(streamData['tags']);
}
return streamData;
const streamData = db
.prepare(
'SELECT id, stream_date, filename, ' +
'format, title, description, tags, ' +
'length_seconds, tracks FROM Stream ' +
'WHERE id = ?'
)
.get(streamId);
if (streamData) {
streamData['stream_date'] = Date.parse(streamData['stream_date']);
streamData['tracks'] = JSON.parse(streamData['tracks']);
streamData['tags'] = JSON.parse(streamData['tags']);
}
return streamData;
}

View File

@@ -5,41 +5,41 @@ export const currentStream = writable({});
export const currentSongIndex = writable(null);
export const favoritedStreams = writable(
new Set(JSON.parse((browser && localStorage.getItem('favoritedStreams')) || '[]'))
new Set(JSON.parse((browser && localStorage.getItem('favoritedStreams')) || '[]'))
);
if (browser) {
favoritedStreams.subscribe((val) => {
localStorage.setItem('favoritedStreams', JSON.stringify(Array.from(val)));
});
favoritedStreams.subscribe((val) => {
localStorage.setItem('favoritedStreams', JSON.stringify(Array.from(val)));
});
}
export const tagList = [
'acoustic',
'electronic',
'orchestral',
'rock',
'pop',
'metal',
'aggressive',
'folk',
'jazzy',
'dance.music',
'untz',
'breakbeats',
'electronica',
'chiptune',
'left.field',
'denpa',
'vocaloid',
'funky',
'lush',
'noisy',
'psychedelic',
'dark',
'calm',
'moody',
'uplifting'
'acoustic',
'electronic',
'orchestral',
'rock',
'pop',
'metal',
'aggressive',
'folk',
'jazzy',
'dance.music',
'untz',
'breakbeats',
'electronica',
'chiptune',
'left.field',
'denpa',
'vocaloid',
'funky',
'lush',
'noisy',
'psychedelic',
'dark',
'calm',
'moody',
'uplifting'
];
let timestampList;
@@ -47,39 +47,39 @@ let timestampList;
// utility
function locationOf(element, array, start, end) {
start = start || 0;
end = end || array.length;
var pivot = parseInt(start + (end - start) / 2, 10);
if (end - start <= 1 || array[pivot] === element) return pivot;
if (array[pivot] < element) {
return locationOf(element, array, pivot, end);
} else {
return locationOf(element, array, start, pivot);
}
start = start || 0;
end = end || array.length;
var pivot = parseInt(start + (end - start) / 2, 10);
if (end - start <= 1 || array[pivot] === element) return pivot;
if (array[pivot] < element) {
return locationOf(element, array, pivot, end);
} else {
return locationOf(element, array, start, pivot);
}
}
// exported methods
export function getSongAtTime(currentTime) {
return locationOf(currentTime, timestampList) - 1;
return locationOf(currentTime, timestampList) - 1;
}
// less operations needed when doing regular lookups
export function updateCurrentSong(currentTime, songIndex) {
function updateCurrentSongRecurse(songIndex) {
if (currentTime >= timestampList[songIndex + 2]) {
return updateCurrentSongRecurse(songIndex + 1);
} else if (currentTime < timestampList[songIndex + 1]) {
return updateCurrentSongRecurse(songIndex - 1);
}
return songIndex;
}
function updateCurrentSongRecurse(songIndex) {
if (currentTime >= timestampList[songIndex + 2]) {
return updateCurrentSongRecurse(songIndex + 1);
} else if (currentTime < timestampList[songIndex + 1]) {
return updateCurrentSongRecurse(songIndex - 1);
}
return songIndex;
}
currentSongIndex.set(updateCurrentSongRecurse(songIndex));
currentSongIndex.set(updateCurrentSongRecurse(songIndex));
}
export function updateCurrentStream(stream) {
currentStream.set(stream);
timestampList = [-Infinity, ...stream.tracks.map((track) => track[0]), Infinity];
currentSongIndex.set(0);
currentStream.set(stream);
timestampList = [-Infinity, ...stream.tracks.map((track) => track[0]), Infinity];
currentSongIndex.set(0);
}

View File

@@ -3,35 +3,35 @@ import Sqids from 'sqids';
let sqids = new Sqids({ minLength: 6, alphabet: 'abcdefghijklmnopqrstuvwxyz0123456789' });
export function hashcode(str) {
for (var i = 0, h = 9; i < str.length; ) h = Math.imul(h ^ str.charCodeAt(i++), 9 ** 9);
return h ^ (h >>> 9);
for (var i = 0, h = 9; i < str.length; ) h = Math.imul(h ^ str.charCodeAt(i++), 9 ** 9);
return h ^ (h >>> 9);
}
// for mnemonic display
export function shorthandCode(str) {
return sqids.encode([hashcode(str) & 0xffff]);
return sqids.encode([hashcode(str) & 0xffff]);
}
// for tag display
export function hashColor(str) {
const hash = hashcode(str);
return `hsl(${hash % 360}, ${65 + (hash % 30) + 1}%, ${85 + (hash % 10) + 1}%)`;
const hash = hashcode(str);
return `hsl(${hash % 360}, ${65 + (hash % 30) + 1}%, ${85 + (hash % 10) + 1}%)`;
}
export function formatSecondsToHms(s) {
s = Number(s);
var h = Math.floor(s / 3600);
var m = Math.ceil((s % 3600) / 60);
s = Number(s);
var h = Math.floor(s / 3600);
var m = Math.ceil((s % 3600) / 60);
var hDisplay = h > 0 ? h + (h == 1 ? ' hr' : ' hrs') + (m > 0 ? ', ' : '') : '';
var mDisplay = m > 0 ? m + (m == 1 ? ' min' : ' mins') : '';
return hDisplay + mDisplay;
var hDisplay = h > 0 ? h + (h == 1 ? ' hr' : ' hrs') + (m > 0 ? ', ' : '') : '';
var mDisplay = m > 0 ? m + (m == 1 ? ' min' : ' mins') : '';
return hDisplay + mDisplay;
}
export function formatDate(unix_timestamp) {
return new Date(unix_timestamp).toISOString().split('T')[0];
return new Date(unix_timestamp).toISOString().split('T')[0];
}
export function formatTrackTime(s) {
return new Date(s * 1000).toISOString().slice(11, 19);
return new Date(s * 1000).toISOString().slice(11, 19);
}

View File

@@ -1,63 +1,71 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<svelte:head>
<style>
body,
html {
margin: 0;
padding: 0;
background: url(/assets/tile.png) repeat fixed;
overflow-y: hidden;
}
<style>
body,
html {
margin: 0;
padding: 0;
background: url(/assets/tile.png) repeat fixed;
overflow-y: hidden;
}
* {
box-sizing: border-box;
}
* {
box-sizing: border-box;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url('/fonts/MaterialIcons-Regular-subset.woff2') format('woff2');
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url('/fonts/MaterialIcons-Regular-subset.woff2') format('woff2');
}
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 300;
src: url('/fonts/Montserrat-Light.woff2') format('woff2');
}
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 300;
src: url('/fonts/Montserrat-Light.woff2') format('woff2');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale;
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale;
}
.panel {
background-color: rgba(0, 0, 0, 0.8);
overflow: auto;
border: 4px double;
border-radius: 10px;
color: white;
}
.panel {
background-color: rgba(0, 0, 0, 0.8);
overflow: auto;
border: 4px double;
border-radius: 10px;
color: white;
}
.panel-alt {
background-color: white;
overflow: auto;
border: 3px double;
border-radius: 5px;
color: rgba(0, 0, 0, 0.6);
}
</style>
.panel-alt {
background-color: white;
overflow: auto;
border: 3px double;
border-radius: 5px;
color: rgba(0, 0, 0, 0.6);
}
</style>
</svelte:head>
<slot />
{@render children?.()}

View File

@@ -1,13 +1,13 @@
<script>
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
if (browser) {
// setTimeout needed for goto to work on ios...
setTimeout(() => goto('/streams', { replaceState: true }), 0);
}
if (browser) {
// setTimeout needed for goto to work on ios...
setTimeout(() => goto('/streams', { replaceState: true }), 0);
}
</script>
<svelte:head>
<title>apt-get's auditorium</title>
<title>apt-get's auditorium</title>
</svelte:head>

View File

@@ -1,7 +1,7 @@
import { getStreams } from '$lib/database.js';
export function load() {
return {
streams: getStreams()
};
return {
streams: getStreams()
};
}

View File

@@ -1,73 +1,73 @@
<script>
import Sidebar from './Sidebar.svelte';
import Footer from './Footer.svelte';
export let data;
<script lang="ts">
import Sidebar from './Sidebar.svelte';
import Footer from './Footer.svelte';
let { data, children } = $props();
</script>
<div id="mainContainer">
<div id="sidebar" class="panel"><Sidebar streams={data.streams} /></div>
<div id="footer" class="panel-alt"><Footer /></div>
<div id="content"><slot /></div>
<div id="sidebar" class="panel"><Sidebar streams={data.streams} /></div>
<div id="footer" class="panel-alt"><Footer /></div>
<div id="content">{@render children?.()}</div>
</div>
<style>
#mainContainer {
display: grid;
grid-template-columns: 25% 1fr;
grid-template-rows: minmax(0, 1fr) auto;
height: calc(100vh - 1em); /* fallback */
height: calc(100dvh - 1em);
gap: 0.5em;
margin: 0.5em;
}
#mainContainer {
display: grid;
grid-template-columns: 25% 1fr;
grid-template-rows: minmax(0, 1fr) auto;
height: calc(100vh - 1em); /* fallback */
height: calc(100dvh - 1em);
gap: 0.5em;
margin: 0.5em;
}
#footer {
grid-row: 2;
grid-column: 1;
height: 100%;
scrollbar-width: thin;
}
#footer {
grid-row: 2;
grid-column: 1;
height: 100%;
scrollbar-width: thin;
}
#sidebar {
grid-row: 1;
grid-column: 1;
overflow-wrap: break-word;
scrollbar-gutter: stable;
overflow-y: scroll;
scrollbar-width: thin;
}
#sidebar {
grid-row: 1;
grid-column: 1;
overflow-wrap: break-word;
scrollbar-gutter: stable;
overflow-y: scroll;
scrollbar-width: thin;
}
#content {
grid-row: 1 / 2;
grid-column: 2;
height: calc(100vh - 1em);
height: calc(100dvh - 1em);
}
#content {
grid-row: 1 / 2;
grid-column: 2;
height: calc(100vh - 1em);
height: calc(100dvh - 1em);
}
@media (max-width: 575px) {
#mainContainer {
grid-template-columns: 100%;
grid-template-rows: 65% 1fr auto;
}
@media (max-width: 575px) {
#mainContainer {
grid-template-columns: 100%;
grid-template-rows: 65% 1fr auto;
}
#footer {
order: 2;
order: 2;
grid-row: 3;
height: 100%;
}
#footer {
order: 2;
order: 2;
grid-row: 3;
height: 100%;
}
#sidebar {
order: 1;
grid-row: 2;
height: 100%;
}
#sidebar {
order: 1;
grid-row: 2;
height: 100%;
}
#content {
order: 0;
grid-row: 1;
grid-column: 1;
height: 100%;
}
}
#content {
order: 0;
grid-row: 1;
grid-column: 1;
height: 100%;
}
}
</style>

View File

@@ -1,24 +1,27 @@
<script>
import WelcomePage from './WelcomePage.svelte';
import WelcomePage from './WelcomePage.svelte';
</script>
<svelte:head>
<title>apt-get's auditorium</title>
<title>apt-get's auditorium</title>
</svelte:head>
<div id="welcome-page" class="panel"><WelcomePage /></div>
<style>
#welcome-page {
height: 100%;
background: local url('/assets/result.png') top right / 50% no-repeat, rgba(0, 0, 0, 0.8);
scrollbar-gutter: stable;
}
#welcome-page {
height: 100%;
background:
local url('/assets/result.png') top right / 50% no-repeat,
rgba(0, 0, 0, 0.8);
scrollbar-gutter: stable;
}
@media (max-width: 575px) {
#welcome-page {
background: local url('/assets/result.png') top right / 80% no-repeat,
rgba(0, 0, 0, 0.8);
}
}
@media (max-width: 575px) {
#welcome-page {
background:
local url('/assets/result.png') top right / 80% no-repeat,
rgba(0, 0, 0, 0.8);
}
}
</style>

View File

@@ -1,44 +1,44 @@
<div class="navbar">
<ul>
<li><a href="https://apt-get.xyz">Home</a></li>
<li><a href="https://apt-get.xyz/journal/">Journal</a></li>
<li><a href="/streams/">Tunes</a></li>
<li><a href="https://apt-get.xyz/contact">Contact</a></li>
</ul>
<ul>
<li><a href="https://apt-get.xyz">Home</a></li>
<li><a href="https://apt-get.xyz/journal/">Journal</a></li>
<li><a href="/streams/">Tunes</a></li>
<li><a href="https://apt-get.xyz/contact">Contact</a></li>
</ul>
</div>
<style>
.navbar {
padding: 4.5px;
text-align: center;
font-family: Verdana;
display: flex;
justify-content: center;
}
.navbar {
padding: 4.5px;
text-align: center;
font-family: Verdana;
display: flex;
justify-content: center;
}
.navbar ul {
margin: 0px;
display: inline-flex;
padding-left: 0px;
list-style: none;
justify-content: space-around;
flex-basis: auto;
}
.navbar ul {
margin: 0px;
display: inline-flex;
padding-left: 0px;
list-style: none;
justify-content: space-around;
flex-basis: auto;
}
.navbar li:hover {
text-decoration: underline;
text-underline-offset: 0.1em;
}
.navbar li a {
padding-left: 5px;
padding-right: 5px;
text-decoration: none;
cursor: pointer;
}
.navbar li:hover {
text-decoration: underline;
text-underline-offset: 0.1em;
}
.navbar li a {
padding-left: 5px;
padding-right: 5px;
text-decoration: none;
cursor: pointer;
}
a,
a:visited {
outline: none;
color: black;
}
a,
a:visited {
outline: none;
color: black;
}
</style>

View File

@@ -1,209 +1,208 @@
<script>
import { goto } from '$app/navigation';
import { currentStream, favoritedStreams } from '$lib/stores.js';
import { hashColor, shorthandCode, formatSecondsToHms, formatDate } from '$lib/utils.js';
import TagSelect from './TagSelect.svelte';
<script lang="ts">
import { goto } from '$app/navigation';
import { currentStream, favoritedStreams } from '$lib/stores.js';
import { hashColor, shorthandCode, formatSecondsToHms, formatDate } from '$lib/utils.js';
import TagSelect from './TagSelect.svelte';
export let streams;
let filteredTags = [];
let listOpen;
let favoritesOnly = false;
let { streams } = $props();
let filteredTags = $state([]);
let listOpen = $state();
let favoritesOnly = $state(false);
$: displayedStreams = streamsToDisplay(filteredTags);
$: remainingTags = getRemainingTags(displayedStreams);
function streamsToDisplay(filteredTags) {
const displayedStreams = streams.filter(
(stream) =>
!('tags' in stream) || filteredTags.every((tag) => stream['tags'].includes(tag))
);
function streamsToDisplay(filteredTags) {
const displayedStreams = streams.filter(
(stream) =>
!('tags' in stream) || filteredTags.every((tag) => stream['tags'].includes(tag))
);
// close tagselect dropdown if we can't select anything else
if (displayedStreams.length == 1) {
listOpen = !listOpen;
}
// close tagselect dropdown if we can't select anything else
if (displayedStreams.length == 1) {
listOpen = !listOpen;
}
return displayedStreams;
}
return displayedStreams;
}
function getRemainingTags(displayedStreams) {
let tagCounts = displayedStreams.reduce((tags, stream) => {
stream['tags'].forEach((tag) => (tag in tags ? (tags[tag] += 1) : (tags[tag] = 1)));
return tags;
}, {});
return Object.entries(tagCounts)
.filter((el) => el[1] != displayedStreams.length)
.map((el) => el[0]);
}
function getRemainingTags(displayedStreams) {
let tagCounts = displayedStreams.reduce((tags, stream) => {
stream['tags'].forEach((tag) => (tag in tags ? (tags[tag] += 1) : (tags[tag] = 1)));
return tags;
}, {});
return Object.entries(tagCounts)
.filter((el) => el[1] != displayedStreams.length)
.map((el) => el[0]);
}
function getDisplayedTagsInList(streamTags, remainingTags) {
return streamTags.filter((tag) => remainingTags.includes(tag));
}
function getDisplayedTagsInList(streamTags, remainingTags) {
return streamTags.filter((tag) => remainingTags.includes(tag));
}
function updateFavorites(stream) {
$favoritedStreams.has(stream['id'])
? $favoritedStreams.delete(stream['id'])
: $favoritedStreams.add(stream['id']);
$favoritedStreams = $favoritedStreams; // for reactivity
}
function updateFavorites(stream) {
$favoritedStreams.has(stream['id'])
? $favoritedStreams.delete(stream['id'])
: $favoritedStreams.add(stream['id']);
$favoritedStreams = $favoritedStreams; // for reactivity
}
let displayedStreams = $derived(streamsToDisplay(filteredTags));
let remainingTags = $derived(getRemainingTags(displayedStreams));
</script>
<div class="tag-select">
<TagSelect bind:listOpen bind:checked={filteredTags} {remainingTags} />
<button
on:click={() => (favoritesOnly = !favoritesOnly)}
class="material-icons {favoritesOnly ? '' : 'un'}select-faves-star">star</button
>
<TagSelect bind:listOpen bind:checked={filteredTags} {remainingTags} />
<button
onclick={() => (favoritesOnly = !favoritesOnly)}
class="material-icons {favoritesOnly ? '' : 'un'}select-faves-star">star</button
>
</div>
<ul class="stream-list">
{#each streams as stream}
{@const favorited = $favoritedStreams.has(stream['id'])}
{@const current = $currentStream['id'] === stream['id']}
<li
hidden={!displayedStreams.includes(stream) || (favoritesOnly && !favorited)}
class="stream-item {current ? 'current-stream' : ''}"
id="stream-{stream['id']}"
>
<button class="stream-item-button" on:click={() => goto('/streams/' + stream['id'])}>
<span class="stream-item-id">
ID:
{shorthandCode(stream['id'])}</span
>
{#each streams as stream}
{@const favorited = $favoritedStreams.has(stream['id'])}
{@const current = $currentStream['id'] === stream['id']}
<li
hidden={!displayedStreams.includes(stream) || (favoritesOnly && !favorited)}
class="stream-item {current ? 'current-stream' : ''}"
id="stream-{stream['id']}"
>
<button class="stream-item-button" onclick={() => goto('/streams/' + stream['id'])}>
<span class="stream-item-id">
ID:
{shorthandCode(stream['id'])}</span
>
<span class="stream-item-date">{formatDate(stream['stream_date'])}</span>
<span class="stream-item-date">{formatDate(stream['stream_date'])}</span>
<span class="stream-item-length"
>{formatSecondsToHms(stream['length_seconds'])}</span
>
<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>
</button>
<p class="stream-item-tags" hidden={!remainingTags.length}>
Tags: <span hidden={!filteredTags.length}>[...] </span>{getDisplayedTagsInList(
stream['tags'],
remainingTags
).join(', ')}
</p>
</button>
<button
on:click={(e) => {
e.stopPropagation();
updateFavorites(stream);
}}
class="material-icons stream-item-star {favorited ? 'stream-item-star-faved' : ''}"
>{favorited ? 'star' : 'star_border'}</button
>
</li>
{/each}
<button
onclick={(e) => {
e.stopPropagation();
updateFavorites(stream);
}}
class="material-icons stream-item-star {favorited ? 'stream-item-star-faved' : ''}"
>{favorited ? 'star' : 'star_border'}</button
>
</li>
{/each}
</ul>
<style>
.stream-list {
list-style-type: none;
padding: 0;
margin: 0;
border: 1px solid black;
border-top: 0;
border-bottom: 0;
}
.stream-list {
list-style-type: none;
padding: 0;
margin: 0;
border: 1px solid black;
border-top: 0;
border-bottom: 0;
}
.stream-item-tags {
margin: 0;
font-family: Tahoma;
font-size: smaller;
}
.stream-item-tags {
margin: 0;
font-family: Tahoma;
font-size: smaller;
}
.stream-item-id {
float: left;
font-family: monospace;
text-decoration: underline;
}
.stream-item-id {
float: left;
font-family: monospace;
text-decoration: underline;
}
.stream-item-date {
float: right;
font-weight: bold;
}
.stream-item-date {
float: right;
font-weight: bold;
}
.stream-item {
width: 100%;
overflow-wrap: anywhere;
border-bottom: 1px solid black;
position: relative;
color: black;
}
.stream-item {
width: 100%;
overflow-wrap: anywhere;
border-bottom: 1px solid black;
position: relative;
color: black;
}
.stream-item-button {
border-radius: 0;
min-width: 100%;
border: none;
padding: 5px;
}
.stream-item-button {
border-radius: 0;
min-width: 100%;
border: none;
padding: 5px;
}
.current-stream > .stream-item-button {
background-color: #add8e6;
}
.current-stream > .stream-item-button {
background-color: #add8e6;
}
.current-stream > .stream-item-button:hover {
background-color: #87c5d9;
}
.current-stream > .stream-item-button:hover {
background-color: #87c5d9;
}
.current-stream > .stream-item-button:active {
background-color: #7aa8b8;
}
.current-stream > .stream-item-button:active {
background-color: #7aa8b8;
}
.tag-select {
text-align: left;
color: black;
display: flex;
margin-bottom: 1px;
}
.tag-select {
text-align: left;
color: black;
display: flex;
margin-bottom: 1px;
}
.material-icons {
margin-bottom: 0px;
color: black;
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
transition: 0.3s;
border: none;
}
.material-icons {
margin-bottom: 0px;
color: black;
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
transition: 0.3s;
border: none;
}
.select-faves-star {
color: rgba(255, 219, 88, 1);
font-size: 24px;
}
.select-faves-star {
color: rgba(255, 219, 88, 1);
font-size: 24px;
}
.unselect-faves-star {
color: #bbbbbb;
font-size: 24px;
}
.unselect-faves-star {
color: #bbbbbb;
font-size: 24px;
}
.stream-item-star {
font-size: 18px;
position: absolute;
bottom: 0;
right: 0;
opacity: 0;
}
.stream-item-star {
font-size: 18px;
position: absolute;
bottom: 0;
right: 0;
opacity: 0;
}
.stream-item-star-faved {
opacity: 0.5;
}
.stream-item:hover .stream-item-star {
opacity: 0.5;
}
.stream-item-star-faved {
opacity: 0.5;
}
.stream-item:hover .stream-item-star {
opacity: 0.5;
}
.stream-item:hover .stream-item-star:hover {
opacity: 1;
}
.stream-item:hover .stream-item-star:hover {
opacity: 1;
}
.material-icons::-moz-focus-inner {
border: 0;
}
.material-icons::-moz-focus-inner {
border: 0;
}
@media (max-width: 575px) {
.stream-item-star {
opacity: 0.5;
font-size: 24px;
}
}
@media (max-width: 575px) {
.stream-item-star {
opacity: 0.5;
font-size: 24px;
}
}
</style>

View File

@@ -1,78 +1,81 @@
<script>
import Select from 'svelte-select';
import { tagList } from '$lib/stores.js';
<script lang="ts">
import Select from 'svelte-select';
import { run } from 'svelte/legacy';
import { tagList } from '$lib/stores.js';
let items = tagList.map((x) => ({ value: x, label: x }));
let items = $state(tagList.map((x) => ({ value: x, label: x })));
let value = [];
export let checked = [];
let isChecked = {};
export let remainingTags = [];
export let listOpen;
let value = $state([]);
let isChecked = $state({});
let { checked = $bindable([]), remainingTags = [], listOpen = $bindable() } = $props();
$: computeValue(checked);
$: computeIsChecked(checked);
function computeIsChecked() {
isChecked = {};
checked.forEach((c) => (isChecked[c] = true));
}
function computeIsChecked() {
isChecked = {};
checked.forEach((c) => (isChecked[c] = true));
}
function computeValue() {
value = checked.map((c) => items.find((i) => i.value === c));
}
function computeValue() {
value = checked.map((c) => items.find((i) => i.value === c));
}
function handleSelectable() {
items = items.map((item) => {
return { ...item, selectable: !checked.includes(item.value) };
});
}
function handleSelectable() {
items = items.map((item) => {
return { ...item, selectable: !checked.includes(item.value) };
});
}
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]);
handleSelectable();
}
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]);
handleSelectable();
}
let itemFilter = function (label, filterText, option) {
return (
remainingTags.includes(option['value']) &&
label.toLowerCase().includes(filterText.toLowerCase())
);
};
let itemFilter = function (label, filterText, option) {
return (
remainingTags.includes(option['value']) &&
label.toLowerCase().includes(filterText.toLowerCase())
);
};
run(() => {
computeValue(checked);
computeIsChecked(checked);
});
</script>
<Select
{items}
{value}
bind:listOpen
multiple={true}
placeholder="Tag Filter..."
filterSelectedItems={true}
closeListOnChange={false}
on:select={handleChange}
on:clear={handleChange}
{itemFilter}
--max-height="42px"
listOffset={0}
{items}
{value}
bind:listOpen
multiple={true}
placeholder="Tag Filter..."
filterSelectedItems={true}
closeListOnChange={false}
on:select={handleChange}
on:clear={handleChange}
{itemFilter}
--max-height="42px"
listOffset={0}
>
<div class="item" slot="item" let:item>
<label for={item.value}>
<input type="checkbox" id={item.value} bind:checked={isChecked[item.value]} />
{item.label}
</label>
</div>
{#snippet item({ item })}
<div class="item">
<label for={item.value}>
<input type="checkbox" id={item.value} bind:checked={isChecked[item.value]} />
{item.label}
</label>
</div>
{/snippet}
</Select>
<style>
.item {
pointer-events: none;
}
.item {
pointer-events: none;
}
:global(.svelte-select:hover) {
max-height: unset !important;
}
:global(.svelte-select:hover) {
max-height: unset !important;
}
</style>

View File

@@ -1,107 +1,107 @@
<script>
import { currentStream } from '$lib/stores.js';
import { currentStream } from '$lib/stores.js';
currentStream.set({});
currentStream.set({});
</script>
<div class="stream-information">
<h1 class="title">Auditorium of Babel</h1>
<h3 class="backlink">ID: aptget</h3>
<h1 class="title">Auditorium of Babel</h1>
<h3 class="backlink">ID: aptget</h3>
</div>
<div class="description-bubble">
<p>Hello, there, welcome to V1.5.</p>
<p>
Still in construction. The design is responsive now, so phones should work well enough!
Needs touch events though.
</p>
<p>
There's gonna be an update feed soon (with RSS, too), but for now, just check the most
recent stream to see if anything new has been uploaded. I'll post something here if I fill
in the back-catalog.
</p>
<p>Hello, there, welcome to V1.5.</p>
<p>
Still in construction. The design is responsive now, so phones should work well enough!
Needs touch events though.
</p>
<p>
There's gonna be an update feed soon (with RSS, too), but for now, just check the most
recent stream to see if anything new has been uploaded. I'll post something here if I fill
in the back-catalog.
</p>
<p>
Also, event sets of mine are available <a href="https://apt-get.xyz/music/">right here</a>.
They've got visuals alongside and are proper DJ mixes, which makes them rather different
from the usual streams. They should live under this subdomain soon enough too!
</p>
<p>
Also, event sets of mine are available <a href="https://apt-get.xyz/music/">right here</a>.
They've got visuals alongside and are proper DJ mixes, which makes them rather different
from the usual streams. They should live under this subdomain soon enough too!
</p>
<p style="font-size: smaller">
Mascot drawn by <a href="https://twitter.com/yuuybee/">yuuybee</a>.
</p>
<p style="font-size: smaller">
Mascot drawn by <a href="https://twitter.com/yuuybee/">yuuybee</a>.
</p>
</div>
<style>
.stream-information {
width: 70%;
padding: 1px;
margin-top: 2%;
margin-left: 3%;
min-height: 8%;
border-radius: 2px;
position: relative;
}
.stream-information {
width: 70%;
padding: 1px;
margin-top: 2%;
margin-left: 3%;
min-height: 8%;
border-radius: 2px;
position: relative;
}
.backlink {
font-family: 'Montserrat';
}
.backlink {
font-family: 'Montserrat';
}
.title {
font-size: x-large;
font-family: 'Montserrat';
right: 0;
position: absolute;
margin-right: 3%;
text-decoration: underline;
}
.title {
font-size: x-large;
font-family: 'Montserrat';
right: 0;
position: absolute;
margin-right: 3%;
text-decoration: underline;
}
.description-bubble {
position: relative;
background-color: rgba(255, 255, 255, 0.75);
border: 1px solid #ccc;
color: black;
padding: 10px;
margin: 10px;
margin-left: 3%;
border-radius: 5px;
min-width: 70%;
max-width: 70%;
width: fit-content;
font-family: Verdana;
font-size: small;
}
.description-bubble {
position: relative;
background-color: rgba(255, 255, 255, 0.75);
border: 1px solid #ccc;
color: black;
padding: 10px;
margin: 10px;
margin-left: 3%;
border-radius: 5px;
min-width: 70%;
max-width: 70%;
width: fit-content;
font-family: Verdana;
font-size: small;
}
.description-bubble :global(:first-child) {
margin-top: 0px;
}
.description-bubble :global(:first-child) {
margin-top: 0px;
}
.description-bubble :global(:last-child) {
margin-bottom: 0px;
}
.description-bubble :global(:last-child) {
margin-bottom: 0px;
}
.description-bubble::after {
content: '';
position: absolute;
border-style: solid;
border-width: 10px 0 10px 10px;
border-color: transparent transparent transparent #ccc;
top: 8px;
right: -10px;
width: 0;
height: 0;
}
.description-bubble::after {
content: '';
position: absolute;
border-style: solid;
border-width: 10px 0 10px 10px;
border-color: transparent transparent transparent #ccc;
top: 8px;
right: -10px;
width: 0;
height: 0;
}
@media (max-width: 575px) {
.stream-information {
width: initial;
}
.description-bubble {
max-width: initial;
}
@media (max-width: 575px) {
.stream-information {
width: initial;
}
.description-bubble {
max-width: initial;
}
.description-bubble::after {
visibility: hidden;
}
}
.description-bubble::after {
visibility: hidden;
}
}
</style>

View File

@@ -1,8 +1,8 @@
import fs from 'fs';
import path from 'path';
import { error } from "@sveltejs/kit";
import { getStreamInfo } from "$lib/database.js";
import { dev } from "$app/environment";
import { error } from '@sveltejs/kit';
import { getStreamInfo } from '$lib/database.js';
import { dev } from '$app/environment';
import { STREAM_JSON_LOCATION } from '$env/static/private';
let getOriginalJson, writeSideloadJson;
@@ -10,52 +10,52 @@ export let actions;
// utilities for manipulating original stream JSONs
if (dev) {
const jsonFolder = path.resolve(STREAM_JSON_LOCATION);
const jsonFolder = path.resolve(STREAM_JSON_LOCATION);
getOriginalJson = function (streamId) {
const filePath = path.join(jsonFolder, streamId + ".sideload.json");
if (fs.existsSync(filePath)) {
const jsonString = fs.readFileSync(filePath, 'utf8');
return jsonString;
} else {
return JSON.stringify({ 'title': '', 'description': '', 'tags': [] });
}
}
getOriginalJson = function (streamId) {
const filePath = path.join(jsonFolder, streamId + '.sideload.json');
if (fs.existsSync(filePath)) {
const jsonString = fs.readFileSync(filePath, 'utf8');
return jsonString;
} else {
return JSON.stringify({ title: '', description: '', tags: [] });
}
};
writeSideloadJson = function (streamId, newJson) {
const filePath = path.join(jsonFolder, streamId + ".sideload.json");
fs.writeFileSync(filePath, JSON.stringify(newJson), 'utf8');
}
writeSideloadJson = function (streamId, newJson) {
const filePath = path.join(jsonFolder, streamId + '.sideload.json');
fs.writeFileSync(filePath, JSON.stringify(newJson), 'utf8');
};
actions = {
default: async ({ params, request }) => {
const data = await request.formData();
// let newJson = JSON.parse(getOriginalJson(params.stream_id));
let newJson = {};
newJson['title'] = data.get('title');
newJson['description'] = data.get('description');
newJson['tags'] = data.getAll('tags');
writeSideloadJson(params.stream_id, newJson);
}
}
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');
writeSideloadJson(params.stream_id, newJson);
}
};
}
export function load({ params }) {
const result = getStreamInfo(params.stream_id);
if (result === null) {
error(404);
}
const result = getStreamInfo(params.stream_id);
if (result === null) {
error(404);
}
if (dev) {
// pass raw JSON for metadata editor
let original = JSON.parse(getOriginalJson(params.stream_id));
return {
stream: result,
original: original
}
} else {
return {
stream: result
};
}
if (dev) {
// pass raw JSON for metadata editor
const original = JSON.parse(getOriginalJson(params.stream_id));
return {
stream: result,
original: original
};
} else {
return {
stream: result
};
}
}

View File

@@ -1,53 +1,61 @@
<script>
import StreamPage from './StreamPage.svelte';
import MetadataEditor from './MetadataEditor.svelte';
import Player from './Player.svelte';
import { dev } from '$app/environment';
import { currentStream, updateCurrentStream } from '$lib/stores.js';
<script lang="ts">
import { run } from 'svelte/legacy';
export let data;
$: updateCurrentStream(data.stream);
import StreamPage from './StreamPage.svelte';
import MetadataEditor from './MetadataEditor.svelte';
import Player from './Player.svelte';
import { dev } from '$app/environment';
import { currentStream, updateCurrentStream } from '$lib/stores.js';
let { data } = $props();
run(() => {
updateCurrentStream(data.stream);
});
</script>
<div id="streamContainer">
<div id="streamPage" class="panel">
{#if dev}
<MetadataEditor {...data} />
{/if}
<StreamPage />
</div>
<div id="player">
{#key $currentStream}
<Player display={true} src="/media/tracks/{$currentStream.filename}" />
{/key}
</div>
<div id="streamPage" class="panel">
{#if dev}
<MetadataEditor {...data} />
{/if}
<StreamPage />
</div>
<div id="player">
{#key $currentStream}
<Player display={true} src="/media/tracks/{$currentStream.filename}" />
{/key}
</div>
</div>
<style>
#streamContainer {
display: grid;
grid-template-rows: 1fr auto;
height: 100%;
gap: 0.5em;
}
#streamContainer {
display: grid;
grid-template-rows: 1fr auto;
height: 100%;
gap: 0.5em;
}
#streamPage {
grid-row: 1 / 2;
overflow: auto;
background: local url('/assets/result.png') top right / 50% no-repeat, rgba(0, 0, 0, 0.8);
}
#streamPage {
grid-row: 1 / 2;
overflow: auto;
background:
local url('/assets/result.png') top right / 50% no-repeat,
rgba(0, 0, 0, 0.8);
}
#player {
grid-row: 2 / 3;
margin-left: 1px;
margin-right: 1px;
height: 100%;
}
#player {
grid-row: 2 / 3;
margin-left: 1px;
margin-right: 1px;
height: 100%;
}
@media (max-width: 575px) {
#streamPage {
background: local url('/assets/result.png') top right / 80% no-repeat,
rgba(0, 0, 0, 0.8);
}
}
@media (max-width: 575px) {
#streamPage {
background:
local url('/assets/result.png') top right / 80% no-repeat,
rgba(0, 0, 0, 0.8);
}
}
</style>

View File

@@ -1,74 +1,77 @@
<script>
import { page } from '$app/stores';
import { tagList } from '$lib/stores.js';
<script lang="ts">
import { page } from '$app/stores';
import { tagList } from '$lib/stores.js';
import { run } from 'svelte/legacy';
export let original;
let { original = $bindable() } = $props();
let tagMap = new Map();
let tagMap = new Map();
// Create a mapping of tags and their checked status
let reloadTags = (original) => {
tagList.forEach((tag) => {
tagMap.set(tag, original.tags.includes(tag));
});
};
// Create a mapping of tags and their checked status
let reloadTags = (original) => {
tagList.forEach((tag) => {
tagMap.set(tag, original.tags.includes(tag));
});
};
$: reloadTags(original);
run(() => {
reloadTags(original);
});
</script>
{#key $page.url.pathname}
<form method="POST">
<br />
<label>
<p>Title:</p>
<input type="text" name="title" bind:value={original.title} />
</label>
<br />
<form method="POST">
<br />
<label>
<p>Title:</p>
<input type="text" name="title" bind:value={original.title} />
</label>
<br />
<label>
<p>Description:</p>
<textarea
style="min-width:400px;min-height:100px"
name="description"
bind:value={original.description}
/>
</label>
<br />
<label>
<p>Tags:</p>
<div class="checkboxContainer">
{#each tagList as tag}
<label>
<input type="checkbox" name="tags" value={tag} checked={tagMap.get(tag)} />
{tag}
</label>
{/each}
</div>
</label>
<br />
<label>
<p>Description:</p>
<textarea
style="min-width:400px;min-height:100px"
name="description"
bind:value={original.description}
></textarea>
</label>
<br />
<label>
<p>Tags:</p>
<div class="checkboxContainer">
{#each tagList as tag}
<label>
<input type="checkbox" name="tags" value={tag} checked={tagMap.get(tag)} />
{tag}
</label>
{/each}
</div>
</label>
<br />
<button type="submit">Submit</button>
</form>
<button type="submit">Submit</button>
</form>
{/key}
<style>
form {
margin-left: 10px;
}
form {
margin-left: 10px;
}
label {
display: flex;
align-items: center;
}
label {
display: flex;
align-items: center;
}
label > p {
margin-right: 5px;
}
label > p {
margin-right: 5px;
}
.checkboxContainer {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1px;
max-width: 50%;
}
.checkboxContainer {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 1px;
max-width: 50%;
}
</style>

View File

@@ -2,358 +2,402 @@
taken from https://github.com/Linkcube/svelte-audio-controls
ISC License
-->
<svelte:options accessors />
<svelte:options />
<script context="module">
let getAudio = null;
<script module>
let getAudio = null;
export function jumpToTrack(s) {
getAudio().currentTime = s;
getAudio().play();
}
export function jumpToTrack(s) {
getAudio().currentTime = s;
getAudio().play();
}
</script>
<script>
import { onMount, onDestroy } from 'svelte';
import { fade } from 'svelte/transition';
import {
currentSongIndex,
currentStream,
getSongAtTime,
updateCurrentSong
} from '$lib/stores.js';
<script lang="ts">
import { run, createBubbler } from 'svelte/legacy';
export let src;
export let audio = null;
export let paused = true;
export let duration = 0;
export let muted = false;
export let volume = 0.67;
export let preload = 'metadata';
export let iconColor = 'gray';
export let textColor = 'gray';
export let barPrimaryColor = 'lightblue';
export let barSecondaryColor = '#6f6f6f';
export let backgroundColor = 'white';
export let display = false;
export let inlineTooltip = false;
export let disableTooltip = false;
const bubble = createBubbler();
import { onMount, onDestroy } from 'svelte';
import { fade } from 'svelte/transition';
import {
currentSongIndex,
currentStream,
getSongAtTime,
updateCurrentSong
} from '$lib/stores.js';
getAudio = () => {
return audio;
};
interface Props {
src: any;
audio?: any;
paused?: boolean;
duration?: number;
muted?: boolean;
volume?: number;
preload?: string;
iconColor?: string;
textColor?: string;
barPrimaryColor?: string;
barSecondaryColor?: string;
backgroundColor?: string;
display?: boolean;
inlineTooltip?: boolean;
disableTooltip?: boolean;
}
let currentTime = 0;
let tooltip;
let tooltipX = 0;
let tooltipY = 0;
let showTooltip = false;
let seekText = '';
let seekTrack = '';
let seeking = false;
let volumeSeeking = false;
let songBar;
let volumeBar;
let innerWidth;
let innerHeight;
let {
src,
audio = $bindable(null),
paused = $bindable(true),
duration = $bindable(0),
muted = $bindable(false),
volume = $bindable(0.67),
preload = 'metadata',
iconColor = 'gray',
textColor = 'gray',
barPrimaryColor = 'lightblue',
barSecondaryColor = '#6f6f6f',
backgroundColor = 'white',
display = false,
inlineTooltip = false,
disableTooltip = false
}: Props = $props();
onMount(async () => {
// default volume
const volumeData = localStorage.getItem('volume');
volume = volumeData ? parseFloat(volumeData) : 0.67;
setVolume(volume);
getAudio = () => {
return audio;
};
// update browser metadata on track changes
if ('mediaSession' in navigator) {
currentSongIndex.subscribe((val) => {
if (val != null && !paused) {
setMediaMetadata($currentStream.tracks[val]);
}
});
}
});
let currentTime = $state(0);
let tooltip = $state();
let tooltipX = $state(0);
let tooltipY = $state(0);
let showTooltip = $state(false);
let seekText = $state('');
let seekTrack = $state('');
let seeking = $state(false);
let volumeSeeking = $state(false);
let songBar = $state();
let volumeBar = $state();
let innerWidth = $state();
let innerHeight = $state();
$: updateCurrentSong(currentTime, $currentSongIndex);
$: updateAudioAttributes(audio);
onMount(async () => {
// default volume
const volumeData = localStorage.getItem('volume');
volume = volumeData ? parseFloat(volumeData) : 0.67;
setVolume(volume);
function seek(event, bounds) {
let x = event.pageX - bounds.left;
return Math.min(Math.max(x / bounds.width, 0), 1);
}
// update browser metadata on track changes
if ('mediaSession' in navigator) {
currentSongIndex.subscribe((val) => {
if (val != null && !paused) {
setMediaMetadata($currentStream.tracks[val]);
}
});
}
});
// exponential volume bar
// default is linear, which doesn't correspond to human hearing
function setVolume(volume) {
if (volume != 0) {
audio.volume = Math.pow(10, 2.5 * (volume - 1));
} else {
audio.volume = volume;
}
}
function seek(event, bounds) {
let x = event.pageX - bounds.left;
return Math.min(Math.max(x / bounds.width, 0), 1);
}
function setMediaMetadata(track) {
navigator.mediaSession.metadata = new MediaMetadata({
artist: track[1],
title: track[2]
});
}
// exponential volume bar
// default is linear, which doesn't correspond to human hearing
function setVolume(volume) {
if (volume != 0) {
audio.volume = Math.pow(10, 2.5 * (volume - 1));
} else {
audio.volume = volume;
}
}
// browsers don't like if you update this while no media is playing
function setMediaMetadataOnPlay() {
if ('mediaSession' in navigator) {
setMediaMetadata($currentStream.tracks[$currentSongIndex]);
}
}
function setMediaMetadata(track) {
navigator.mediaSession.metadata = new MediaMetadata({
artist: track[1],
title: track[2]
});
}
// 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) {
duration = audio.duration;
setVolume(volume);
// browsers don't like if you update this while no media is playing
function setMediaMetadataOnPlay() {
if ('mediaSession' in navigator) {
setMediaMetadata($currentStream.tracks[$currentSongIndex]);
}
}
// workaround for bug https://github.com/sveltejs/svelte/issues/5914
audio.addEventListener('loadedmetadata', (event) => {
paused = audio.paused;
});
}
}
// 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) {
duration = audio.duration;
setVolume(volume);
function seekAudio(event) {
if (!songBar) return;
audio.currentTime = seek(event, songBar.getBoundingClientRect()) * duration;
}
// workaround for bug https://github.com/sveltejs/svelte/issues/5914
audio.addEventListener('loadedmetadata', (event) => {
paused = audio.paused;
});
}
}
function seekVolume(event) {
if (!volumeBar) return;
volume = seek(event, volumeBar.getBoundingClientRect());
setVolume(volume);
localStorage.setItem('volume', volume.toString());
muted = false;
}
function seekAudio(event) {
if (!songBar) return;
audio.currentTime = seek(event, songBar.getBoundingClientRect()) * duration;
}
function formatSeconds(totalSeconds) {
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;
function seekVolume(event) {
if (!volumeBar) return;
volume = seek(event, volumeBar.getBoundingClientRect());
setVolume(volume);
localStorage.setItem('volume', volume.toString());
muted = false;
}
return [hours, minutes, seconds]
.map((v) => (v < 10 ? '0' + v : v))
.filter((v, i) => v !== '00' || i > 0)
.join(':');
}
function formatSeconds(totalSeconds) {
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;
function seekTooltip(event) {
if (!inlineTooltip) {
let tooltipBounds = tooltip.getBoundingClientRect();
tooltipX = Math.min(event.pageX + 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 trackArray = $currentStream.tracks[getSongAtTime(seekValue)];
seekTrack = trackArray[1] + ' - ' + trackArray[2];
seekText = formatSeconds(seekValue);
}
return [hours, minutes, seconds]
.map((v) => (v < 10 ? '0' + v : v))
.filter((v, i) => v !== '00' || i > 0)
.join(':');
}
function trackMouse(event) {
if (seeking) seekAudio(event);
if (showTooltip && !disableTooltip) seekTooltip(event);
if (volumeSeeking) seekVolume(event);
}
function seekTooltip(event) {
if (!inlineTooltip) {
let tooltipBounds = tooltip.getBoundingClientRect();
tooltipX = Math.min(event.pageX + 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 trackArray = $currentStream.tracks[getSongAtTime(seekValue)];
seekTrack = trackArray[1] + ' - ' + trackArray[2];
seekText = formatSeconds(seekValue);
}
function trackMouse(event) {
if (seeking) seekAudio(event);
if (showTooltip && !disableTooltip) seekTooltip(event);
if (volumeSeeking) seekVolume(event);
}
run(() => {
updateCurrentSong(currentTime, $currentSongIndex);
});
run(() => {
updateAudioAttributes(audio);
});
export {
src,
audio,
paused,
duration,
muted,
volume,
preload,
iconColor,
textColor,
barPrimaryColor,
barSecondaryColor,
backgroundColor,
display,
inlineTooltip,
disableTooltip
};
</script>
<svelte:window
bind:innerWidth
bind:innerHeight
on:mouseup={() => (seeking = volumeSeeking = false)}
on:mousemove={trackMouse}
bind:innerWidth
bind:innerHeight
onmouseup={() => (seeking = volumeSeeking = false)}
onmousemove={trackMouse}
/>
{#if display}
<div class="controls" style="--color:{textColor}; --background-color:{backgroundColor}">
<button
class="material-icons"
style="--icon-color:{iconColor}"
on:click={() => (audio.paused ? audio.play() : audio.pause())}
>
{#if paused}
play_arrow
{:else}
pause
{/if}
</button>
<progress
bind:this={songBar}
value={currentTime ? currentTime : 0}
max={duration}
on:mousedown={() => (seeking = true)}
on:mouseenter={() => (showTooltip = true)}
on:mouseleave={() => (showTooltip = false)}
on:click={seekAudio}
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
class="song-progress"
/>
<div class="control-times">{formatSeconds(currentTime)}/{formatSeconds(duration)}</div>
<button
style="--icon-color:{iconColor}"
class="material-icons"
on:click={() => (muted = !muted)}
>
{#if muted}
volume_off
{:else if volume < 0.01}
volume_mute
{:else if volume < 0.5}
volume_down
{:else}
volume_up
{/if}
</button>
<progress
bind:this={volumeBar}
value={volume}
on:mousedown={() => (volumeSeeking = true)}
on:click={seekVolume}
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
class="volume-progress"
/>
{#if !disableTooltip && (inlineTooltip || showTooltip)}
<div
class:hover-tooltip={!inlineTooltip}
transition:fade
bind:this={tooltip}
class="tooltip"
style="--left:{tooltipX}px;
<div class="controls" style="--color:{textColor}; --background-color:{backgroundColor}">
<button
class="material-icons"
style="--icon-color:{iconColor}"
onclick={() => (audio.paused ? audio.play() : audio.pause())}
>
{#if paused}
play_arrow
{:else}
pause
{/if}
</button>
<progress
bind:this={songBar}
value={currentTime ? currentTime : 0}
max={duration}
onmousedown={() => (seeking = true)}
onmouseenter={() => (showTooltip = true)}
onmouseleave={() => (showTooltip = false)}
onclick={seekAudio}
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
class="song-progress"
></progress>
<div class="control-times">{formatSeconds(currentTime)}/{formatSeconds(duration)}</div>
<button
style="--icon-color:{iconColor}"
class="material-icons"
onclick={() => (muted = !muted)}
>
{#if muted}
volume_off
{:else if volume < 0.01}
volume_mute
{:else if volume < 0.5}
volume_down
{:else}
volume_up
{/if}
</button>
<progress
bind:this={volumeBar}
value={volume}
onmousedown={() => (volumeSeeking = true)}
onclick={seekVolume}
style="--primary-color:{barPrimaryColor}; --secondary-color:{barSecondaryColor}"
class="volume-progress"
></progress>
{#if !disableTooltip && (inlineTooltip || showTooltip)}
<div
class:hover-tooltip={!inlineTooltip}
transition:fade
bind:this={tooltip}
class="tooltip"
style="--left:{tooltipX}px;
--top:{tooltipY}px;
--background-color:{backgroundColor};
--box-color:{barSecondaryColor};
--text-color:{textColor}"
>
{#if showTooltip}
{seekText}
<br />
{seekTrack}
{:else if duration > 3600}
--:--:--
{:else}
--:--
{/if}
</div>
{/if}
</div>
>
{#if showTooltip}
{seekText}
<br />
{seekTrack}
{:else if duration > 3600}
--:--:--
{:else}
--:--
{/if}
</div>
{/if}
</div>
{/if}
<audio
bind:this={audio}
bind:paused
bind:duration
bind:currentTime
{muted}
{volume}
on:play={setMediaMetadataOnPlay}
on:ended
{preload}
bind:this={audio}
bind:paused
bind:duration
bind:currentTime
{muted}
{volume}
onplay={setMediaMetadataOnPlay}
onended={bubble('ended')}
{preload}
>
<source {src} type="audio/ogg;codecs=opus" />
<source src="{src}.mp3" type="audio/mpeg" />
<source {src} type="audio/ogg;codecs=opus" />
<source src="{src}.mp3" type="audio/mpeg" />
</audio>
<style>
.controls {
display: flex;
flex-flow: row;
justify-content: space-around;
color: var(--color);
background-color: var(--background-color);
padding-left: 10px;
padding-right: 10px;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10+ and Edge */
user-select: none; /* Standard syntax */
padding-top: 5px;
padding-bottom: 5px;
border: 3px double;
border-radius: 5px;
color: rgba(0, 0, 0, 0.6);
}
.controls {
display: flex;
flex-flow: row;
justify-content: space-around;
color: var(--color);
background-color: var(--background-color);
padding-left: 10px;
padding-right: 10px;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10+ and Edge */
user-select: none; /* Standard syntax */
padding-top: 5px;
padding-bottom: 5px;
border: 3px double;
border-radius: 5px;
color: rgba(0, 0, 0, 0.6);
}
.control-times {
margin: auto;
margin-right: 5px;
}
.control-times {
margin: auto;
margin-right: 5px;
}
.tooltip {
background-color: var(--background-color);
padding: 1px;
border-radius: 5px;
border-width: 3px;
box-shadow: 6px 6px var(--box-color);
color: var(--text-color);
pointer-events: none;
min-width: 50px;
text-align: center;
margin-bottom: 5px;
}
.tooltip {
background-color: var(--background-color);
padding: 1px;
border-radius: 5px;
border-width: 3px;
box-shadow: 6px 6px var(--box-color);
color: var(--text-color);
pointer-events: none;
min-width: 50px;
text-align: center;
margin-bottom: 5px;
}
.hover-tooltip {
position: absolute;
top: var(--top);
left: var(--left);
}
.hover-tooltip {
position: absolute;
top: var(--top);
left: var(--left);
}
.material-icons {
font-size: 16px;
margin-bottom: 0px;
color: var(--icon-color);
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
transition: 0.3s;
border: none;
border-radius: 25px;
}
.material-icons {
font-size: 16px;
margin-bottom: 0px;
color: var(--icon-color);
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
transition: 0.3s;
border: none;
border-radius: 25px;
}
.material-icons:hover {
box-shadow: 0px 6px rgba(0, 0, 0, 0.6);
}
.material-icons:hover {
box-shadow: 0px 6px rgba(0, 0, 0, 0.6);
}
.material-icons::-moz-focus-inner {
border: 0;
}
.material-icons::-moz-focus-inner {
border: 0;
}
progress {
display: block;
color: var(--primary-color);
background: var(--secondary-color);
border: none;
height: 15px;
margin: auto;
margin-left: 5px;
margin-right: 5px;
}
progress {
display: block;
color: var(--primary-color);
background: var(--secondary-color);
border: none;
height: 15px;
margin: auto;
margin-left: 5px;
margin-right: 5px;
}
progress::-webkit-progress-bar {
background-color: var(--secondary-color);
width: 100%;
}
progress::-webkit-progress-bar {
background-color: var(--secondary-color);
width: 100%;
}
progress::-moz-progress-bar {
background: var(--primary-color);
}
progress::-moz-progress-bar {
background: var(--primary-color);
}
progress::-webkit-progress-value {
background: var(--primary-color);
}
progress::-webkit-progress-value {
background: var(--primary-color);
}
.song-progress {
width: 100%;
}
.song-progress {
width: 100%;
}
.volume-progress {
width: 10%;
max-width: 100px;
min-width: 50px;
}
.volume-progress {
width: 10%;
max-width: 100px;
min-width: 50px;
}
</style>

View File

@@ -1,215 +1,204 @@
<script>
import { currentStream, currentSongIndex } from '$lib/stores.js';
import { shorthandCode, formatTrackTime, formatDate } from '$lib/utils.js';
import { jumpToTrack } from './Player.svelte';
import { Carta } from 'carta-md';
import DOMPurify from 'isomorphic-dompurify';
import { currentStream, currentSongIndex } from '$lib/stores.js';
import { shorthandCode, formatTrackTime, formatDate } from '$lib/utils.js';
import { jumpToTrack } from './Player.svelte';
import { Carta } from 'carta-md';
import DOMPurify from 'isomorphic-dompurify';
const carta = new Carta({
sanitizer: DOMPurify.sanitize
});
const carta = new Carta({
sanitizer: DOMPurify.sanitize
});
$: formattedStreamDate = formatDate($currentStream.stream_date);
let formattedStreamDate = $derived(formatDate($currentStream.stream_date));
</script>
<svelte:head>
<title>{formattedStreamDate} | apt-get's auditorium</title>
<title>{formattedStreamDate} | apt-get's auditorium</title>
</svelte:head>
<div class="stream-information">
<div class="stream-information-flexbox">
<h3 class="stream-id">ID: {shorthandCode($currentStream.id)}</h3>
<h1 class="stream-date">{formattedStreamDate}</h1>
</div>
<h5 class="stream-tags"><u>Tags</u>: {$currentStream.tags.join(', ')}</h5>
<div class="stream-information-flexbox">
<h3 class="stream-id">ID: {shorthandCode($currentStream.id)}</h3>
<h1 class="stream-date">{formattedStreamDate}</h1>
</div>
<h5 class="stream-tags"><u>Tags</u>: {$currentStream.tags.join(', ')}</h5>
</div>
<div class="description-bubble">
{@html carta.renderSSR($currentStream.description || 'No description available.')}
{@html carta.renderSSR($currentStream.description || 'No description available.')}
</div>
<div id="table-container">
<table>
<tbody>
<tr><th>Timestamp</th><th>Artist</th><th>Title</th></tr>
{#each $currentStream.tracks as track, i}
<tr class:current={i == $currentSongIndex}>
<td class="timestamp-field"
><div class="timestamp-field-flex">
{formatTrackTime(track[0])}
<button
on:click={() => jumpToTrack(track[0])}
class="material-icons"
style="padding: 4px 6px; margin-top: -4px; margin-bottom: -4px; margin-right: -6px"
>fast_forward</button
>
</div>
</td>
<td class="artist-field">{track[1]}</td>
<td class="track-field">{track[2]}</td>
</tr>
{/each}
</tbody>
</table>
<table>
<tbody>
<tr><th>Timestamp</th><th>Artist</th><th>Title</th></tr>
{#each $currentStream.tracks as track, i}
<tr class:current={i == $currentSongIndex}>
<td class="timestamp-field"
><div class="timestamp-field-flex">
{formatTrackTime(track[0])}
<button
onclick={() => jumpToTrack(track[0])}
class="material-icons"
style="padding: 4px 6px; margin-top: -4px; margin-bottom: -4px; margin-right: -6px"
>fast_forward</button
>
</div>
</td>
<td class="artist-field">{track[1]}</td>
<td class="track-field">{track[2]}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<style>
.stream-information {
width: 70%;
padding: 1px;
margin-top: 2%;
margin-left: 3%;
border-radius: 2px;
position: relative;
}
.stream-information {
width: 70%;
padding: 1px;
margin-top: 2%;
margin-left: 3%;
border-radius: 2px;
position: relative;
}
.stream-information-flexbox {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: calc(1.17em - 4px); /* autism */
}
.stream-information-flexbox {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: calc(1.17em - 4px); /* autism */
}
#table-container {
margin-left: 3%;
}
#table-container {
margin-left: 3%;
}
table {
border-spacing: 0px;
}
table {
border-spacing: 0px;
}
th {
text-align: left;
}
th {
text-align: left;
}
th,
td {
padding: 3px;
line-height: 1.15;
}
th,
td {
padding: 3px;
line-height: 1.15;
}
.timestamp-field {
white-space: nowrap;
}
.timestamp-field {
white-space: nowrap;
}
.timestamp-field-flex {
display: flex;
align-items: center;
}
.timestamp-field-flex {
display: flex;
align-items: center;
}
.artist-field {
padding-right: 2em;
}
.artist-field {
padding-right: 2em;
}
.stream-date {
font-size: x-large;
font-family: 'Montserrat';
margin-right: 3%;
margin-bottom: 0;
text-decoration: underline;
}
.stream-date {
font-size: x-large;
font-family: 'Montserrat';
margin-right: 3%;
margin-bottom: 0;
text-decoration: underline;
}
.stream-tags {
margin: 0;
font-family: Times New Roman;
}
.stream-id {
font-family: 'Montserrat';
margin-bottom: 0;
}
.stream-tags {
margin: 0;
font-family: Times New Roman;
}
.stream-id {
font-family: 'Montserrat';
margin-bottom: 0;
}
.current {
background-color: rgba(128, 128, 128, 0.8);
}
.current {
background-color: rgba(128, 128, 128, 0.8);
}
.description-bubble {
position: relative;
background-color: rgba(255, 255, 255, 0.75);
border: 1px solid #ccc;
color: black;
padding: 10px;
margin: 10px;
margin-left: 3%;
border-radius: 5px;
max-width: 70%;
width: fit-content;
min-width: 50%;
font-family: Verdana;
font-size: small;
}
.description-bubble {
position: relative;
background-color: rgba(255, 255, 255, 0.75);
border: 1px solid #ccc;
color: black;
padding: 10px;
margin: 10px;
margin-left: 3%;
border-radius: 5px;
max-width: 70%;
width: fit-content;
min-width: 50%;
font-family: Verdana;
font-size: small;
}
.description-bubble :global(:first-child) {
margin-top: 0px;
}
.description-bubble :global(:first-child) {
margin-top: 0px;
}
.description-bubble :global(:last-child) {
margin-bottom: 0px;
}
.description-bubble :global(:last-child) {
margin-bottom: 0px;
}
.description-bubble::after {
content: '';
position: absolute;
border-style: solid;
border-width: 10px 0 10px 10px;
border-color: transparent transparent transparent #ccc;
top: 8px;
right: -10px;
width: 0;
height: 0;
}
.description-bubble::after {
content: '';
position: absolute;
border-style: solid;
border-width: 10px 0 10px 10px;
border-color: transparent transparent transparent #ccc;
top: 8px;
right: -10px;
width: 0;
height: 0;
}
.material-icons {
font-size: 16px;
margin-bottom: 0px;
color: white;
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
transition: 0.3s;
border: none;
opacity: 0;
}
.material-icons {
font-size: 16px;
margin-bottom: 0px;
color: white;
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
transition: 0.3s;
border: none;
opacity: 0;
}
tr:hover .material-icons {
opacity: 0.5;
}
tr:hover .material-icons {
opacity: 0.5;
}
tr:hover .material-icons:hover {
opacity: 1;
}
tr:hover .material-icons:hover {
opacity: 1;
}
.material-icons::-moz-focus-inner {
border: 0;
}
.material-icons::-moz-focus-inner {
border: 0;
}
.back-button {
opacity: 0.75;
border: 1px solid white;
transform: rotateX(180deg);
}
@media (max-width: 575px) {
.material-icons {
opacity: 0.5;
}
.back-button:hover {
opacity: 1;
border: 1px solid white;
}
.stream-id {
margin-bottom: 0px;
}
@media (max-width: 575px) {
.material-icons {
opacity: 0.5;
}
.stream-information {
width: initial;
}
.description-bubble {
max-width: initial;
}
.stream-id {
margin-bottom: 0px;
}
.stream-information {
width: initial;
}
.description-bubble {
max-width: initial;
}
.description-bubble::after {
visibility: hidden;
}
}
.description-bubble::after {
visibility: hidden;
}
}
</style>

View File

@@ -5,15 +5,15 @@ import { error } from '@sveltejs/kit';
export let GET;
if (process.env.NODE_ENV === 'development') {
const jsonFolder = './db/stream_json/';
const jsonFolder = './db/stream_json/';
GET = function ({ params }) {
const filePath = jsonFolder + params.stream_id + ".json";
if (fs.existsSync(filePath)) {
const jsonString = fs.readFileSync(filePath, 'utf8');
return json(jsonString);
} else {
error(404);
}
}
GET = function ({ params }) {
const filePath = jsonFolder + params.stream_id + '.json';
if (fs.existsSync(filePath)) {
const jsonString = fs.readFileSync(filePath, 'utf8');
return json(jsonString);
} else {
error(404);
}
};
}

View File

@@ -7,19 +7,22 @@ process.env.ASSETS_FOLDER = assets_folder;
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
files: {
assets: assets_folder
}
}
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
compilerOptions: {
// disable all accessibility warnings
warningFilter: (warning) => !warning.code.startsWith('a11y')
},
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
files: {
assets: assets_folder
}
}
};
export default config;

View File

@@ -1,17 +1,23 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"plugins": [
{
"name": "typescript-svelte-plugin",
"assumeIsSvelteProject": true
}
]
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@@ -2,5 +2,5 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
plugins: [sveltekit()]
});