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 = { module.exports = {
extends: [ extends: [
// add more generic rule sets here, such as: // add more generic rule sets here, such as:
// 'eslint:recommended', // 'eslint:recommended',
"plugin:svelte/prettier", 'plugin:svelte/prettier',
], 'eslint:recommended',
rules: { 'plugin:@typescript-eslint/recommended'
// override/add rules settings here, such as: ],
// 'svelte/rule-name': 'error' rules: {
}, // override/add rules settings here, such as:
} // 'svelte/rule-name': 'error'
}
};

View File

@@ -1,10 +1,9 @@
{ {
"useTabs": false, "useTabs": false,
"tabWidth": 4, "tabWidth": 4,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"plugins": ["prettier-plugin-svelte"], "plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."], "overrides": [{ "files": "*.svelte", "options": { "parser": "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 Copy `.env` to `env.tpl` and insert the correct folder locations
## running dev server ## running dev server
```bash ```bash
bun --bun run dev bun --bun run dev
``` ```

View File

@@ -27,7 +27,9 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.4", "@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/bun": "^1.2.2",
"svelte-adapter-bun": "https://github.com/gornostay25/svelte-adapter-bun", "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/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=="], "@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=="], "@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/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/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/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/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=="], "@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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], "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=="], "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=="], "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=="], "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": ["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=="], "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=="], "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=="], "@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/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=="], "@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=="], "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=="], "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=="], "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=="], "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=="], "tsutils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
"@eslint/eslintrc/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], "@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); const db = new Database(dbName);
try { try {
console.log(`Connected to the ${dbName} database.`); console.log(`Connected to the ${dbName} database.`);
db.exec(schema); db.exec(schema);
console.log('Schema initialized successfully.'); console.log('Schema initialized successfully.');
} catch (err) { } catch (err) {
console.error(err.message); console.error(err.message);
} finally { } finally {
db.close(); db.close();
} }

View File

@@ -11,15 +11,15 @@ const db = new Database(dbName);
// Map JSON attribute names to database column names // Map JSON attribute names to database column names
// Not needed today, but makes schema changes easier // Not needed today, but makes schema changes easier
const lookup = { const lookup = {
id: 'id', id: 'id',
date: 'stream_date', date: 'stream_date',
filename: 'filename', filename: 'filename',
format: 'format', format: 'format',
title: 'title', title: 'title',
tags: 'tags', tags: 'tags',
description: 'description', description: 'description',
length_seconds: 'length_seconds', length_seconds: 'length_seconds',
tracks: 'tracks' tracks: 'tracks'
}; };
// Retrieve existing IDs from Stream // Retrieve existing IDs from Stream
@@ -32,40 +32,39 @@ const shouldOverwrite = process.argv.includes('--overwrite');
// Process JSON files and insert data into Stream // Process JSON files and insert data into Stream
fs.readdir(jsonFolder, (err, files) => { fs.readdir(jsonFolder, (err, files) => {
if (err) throw err; if (err) throw err;
console.log(`Found ${files.length} JSON file(s) in ${jsonFolder}.`); console.log(`Found ${files.length} JSON file(s) in ${jsonFolder}.`);
files.forEach(file => { files.forEach((file) => {
if (!file.endsWith('.json')) return; if (!file.endsWith('.json')) return;
if (file.endsWith('.sideload.json')) return; if (file.endsWith('.sideload.json')) return;
const jsonString = fs.readFileSync(path.join(jsonFolder, file), 'utf8'); const jsonString = fs.readFileSync(path.join(jsonFolder, file), 'utf8');
let jsonData = JSON.parse(jsonString); let jsonData = JSON.parse(jsonString);
const sideloadPath = path.join(jsonFolder, file.slice(0, -5) + ".sideload.json"); const sideloadPath = path.join(jsonFolder, file.slice(0, -5) + '.sideload.json');
if (fs.existsSync(sideloadPath, 'utf8')) { if (fs.existsSync(sideloadPath, 'utf8')) {
const sideloadData = JSON.parse(fs.readFileSync(sideloadPath)); const sideloadData = JSON.parse(fs.readFileSync(sideloadPath));
jsonData = { ...jsonData, ...sideloadData }; jsonData = { ...jsonData, ...sideloadData };
} }
// Skip if ID already exists in Stream // Skip if ID already exists in Stream
if (idSet.has(jsonData.id)) { if (idSet.has(jsonData.id)) {
if (!shouldOverwrite) { if (!shouldOverwrite) {
console.log(`Skipped data for ID ${jsonData.id} (already exists).`); console.log(`Skipped data for ID ${jsonData.id} (already exists).`);
return; return;
} }
console.log(`Overwriting data for ID ${jsonData.id}.`); console.log(`Overwriting data for ID ${jsonData.id}.`);
} }
// Prepare attributes for insertion // Prepare attributes for insertion
const values = Object.keys(jsonData).map( const values = Object.keys(jsonData).map(
// Serialize value if array // Serialize value if array
key => Array.isArray(jsonData[key]) ? JSON.stringify(jsonData[key]) : jsonData[key] (key) => (Array.isArray(jsonData[key]) ? JSON.stringify(jsonData[key]) : jsonData[key])
); );
const columns = Object.keys(jsonData).map(key => lookup[key]); const columns = Object.keys(jsonData).map((key) => lookup[key]);
const sql = `INSERT OR REPLACE INTO Stream (${columns.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`;
const sql = `INSERT OR REPLACE INTO Stream (${columns.join(', ')}) VALUES (${columns.map(() => '?').join(', ')})`; db.prepare(sql).run(...values);
db.prepare(sql).run(...values); console.log(`Inserted data for ID ${jsonData.id}.`);
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", "name": "strimserve",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build && node ./scripts/create_media_symlink.js", "build": "vite build && node ./scripts/create_media_symlink.js",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .", "lint": "prettier --check . && eslint .",
"format": "prettier --plugin-search-dir . --write .", "format": "prettier --write .",
"init_db": "cd db && node ./init_db.js", "init_db": "cd db && node ./init_db.js",
"update_db": "cd db && node ./update_db.js", "update_db": "cd db && node ./update_db.js",
"generate_iconfont_subset": "cd scripts && bash ./generate_iconfont_subset.sh" "generate_iconfont_subset": "cd scripts && bash ./generate_iconfont_subset.sh"
}, },
"dependencies": { "dependencies": {
"@sveltejs/kit": "^2.17.1", "@sveltejs/kit": "^2.17.1",
"@types/node": "^20.17.17", "@types/bun": "^1.2.2",
"@typescript-eslint/eslint-plugin": "^5.62.0", "@types/node": "^20.17.17",
"@typescript-eslint/parser": "^5.62.0", "@typescript-eslint/eslint-plugin": "^5.62.0",
"carta-md": "^4.6.7", "@typescript-eslint/parser": "^5.62.0",
"dotenv": "^16.4.7", "carta-md": "^4.6.7",
"eslint": "^8.57.1", "dotenv": "^16.4.7",
"eslint-config-prettier": "^8.10.0", "eslint": "^8.57.1",
"eslint-plugin-svelte": "^2.46.1", "eslint-config-prettier": "^8.10.0",
"fontkit": "^2.0.4", "eslint-plugin-svelte": "^2.46.1",
"isomorphic-dompurify": "^2.21.0", "fontkit": "^2.0.4",
"prettier": "^3.4.2", "isomorphic-dompurify": "^2.21.0",
"prettier-plugin-svelte": "^3.3.3", "prettier": "^3.4.2",
"sqids": "0.3.0", "prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.19.9", "sqids": "0.3.0",
"svelte-check": "^4.1.4", "svelte": "^5.19.9",
"svelte-select": "^5.8.3", "svelte-check": "^4.1.4",
"tslib": "^2.8.1", "svelte-select": "^5.8.3",
"typescript": "^5.7.3", "tslib": "^2.8.1",
"vite": "^5.4.14" "typescript": "^5.7.3",
}, "vite": "^5.4.14"
"type": "module", },
"devDependencies": { "type": "module",
"@sveltejs/vite-plugin-svelte": "^4.0.4", "devDependencies": {
"svelte-adapter-bun": "https://github.com/gornostay25/svelte-adapter-bun" "@sveltejs/vite-plugin-svelte": "^4.0.4",
}, "svelte-adapter-bun": "https://github.com/gornostay25/svelte-adapter-bun",
"trustedDependencies": ["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 target = process.env.STREAM_MEDIA_LOCATION;
const linkName = './build/client/media'; const linkName = './build/client/media';
try { try {
symlinkSync(target, linkName); symlinkSync(target, linkName);
console.log(`Symlink created: ${linkName} -> ${target}`); console.log(`Symlink created: ${linkName} -> ${target}`);
} catch (err) { } 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 // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
// interface Locals {} // interface Locals {}
// interface PageData {} // interface PageData {}
// interface Platform {} // interface Platform {}
} }
} }
export {}; export {};

View File

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

View File

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

View File

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

View File

@@ -3,35 +3,35 @@ import Sqids from 'sqids';
let sqids = new Sqids({ minLength: 6, alphabet: 'abcdefghijklmnopqrstuvwxyz0123456789' }); let sqids = new Sqids({ minLength: 6, alphabet: 'abcdefghijklmnopqrstuvwxyz0123456789' });
export function hashcode(str) { export function hashcode(str) {
for (var i = 0, h = 9; i < str.length; ) h = Math.imul(h ^ str.charCodeAt(i++), 9 ** 9); for (var i = 0, h = 9; i < str.length; ) h = Math.imul(h ^ str.charCodeAt(i++), 9 ** 9);
return h ^ (h >>> 9); return h ^ (h >>> 9);
} }
// for mnemonic display // for mnemonic display
export function shorthandCode(str) { export function shorthandCode(str) {
return sqids.encode([hashcode(str) & 0xffff]); return sqids.encode([hashcode(str) & 0xffff]);
} }
// for tag display // for tag display
export function hashColor(str) { export function hashColor(str) {
const hash = hashcode(str); const hash = hashcode(str);
return `hsl(${hash % 360}, ${65 + (hash % 30) + 1}%, ${85 + (hash % 10) + 1}%)`; return `hsl(${hash % 360}, ${65 + (hash % 30) + 1}%, ${85 + (hash % 10) + 1}%)`;
} }
export function formatSecondsToHms(s) { export function formatSecondsToHms(s) {
s = Number(s); s = Number(s);
var h = Math.floor(s / 3600); var h = Math.floor(s / 3600);
var m = Math.ceil((s % 3600) / 60); var m = Math.ceil((s % 3600) / 60);
var hDisplay = h > 0 ? h + (h == 1 ? ' hr' : ' hrs') + (m > 0 ? ', ' : '') : ''; var hDisplay = h > 0 ? h + (h == 1 ? ' hr' : ' hrs') + (m > 0 ? ', ' : '') : '';
var mDisplay = m > 0 ? m + (m == 1 ? ' min' : ' mins') : ''; var mDisplay = m > 0 ? m + (m == 1 ? ' min' : ' mins') : '';
return hDisplay + mDisplay; return hDisplay + mDisplay;
} }
export function formatDate(unix_timestamp) { 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) { 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> <svelte:head>
<style> <style>
body, body,
html { html {
margin: 0; margin: 0;
padding: 0; padding: 0;
background: url(/assets/tile.png) repeat fixed; background: url(/assets/tile.png) repeat fixed;
overflow-y: hidden; overflow-y: hidden;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
@font-face { @font-face {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('/fonts/MaterialIcons-Regular-subset.woff2') format('woff2'); src: url('/fonts/MaterialIcons-Regular-subset.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Montserrat'; font-family: 'Montserrat';
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: url('/fonts/Montserrat-Light.woff2') format('woff2'); src: url('/fonts/Montserrat-Light.woff2') format('woff2');
} }
.material-icons { .material-icons {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-size: 24px; font-size: 24px;
line-height: 1; line-height: 1;
letter-spacing: normal; letter-spacing: normal;
text-transform: none; text-transform: none;
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
word-wrap: normal; word-wrap: normal;
direction: ltr; direction: ltr;
-moz-font-feature-settings: 'liga'; -moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.panel { .panel {
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
overflow: auto; overflow: auto;
border: 4px double; border: 4px double;
border-radius: 10px; border-radius: 10px;
color: white; color: white;
} }
.panel-alt { .panel-alt {
background-color: white; background-color: white;
overflow: auto; overflow: auto;
border: 3px double; border: 3px double;
border-radius: 5px; border-radius: 5px;
color: rgba(0, 0, 0, 0.6); color: rgba(0, 0, 0, 0.6);
} }
</style> </style>
</svelte:head> </svelte:head>
<slot /> {@render children?.()}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,23 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true,
} "plugins": [
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias {
// "name": "typescript-svelte-plugin",
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes "assumeIsSvelteProject": true
// from the referenced tsconfig.json - TypeScript does not merge them in }
]
}
// 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'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()]
}); });