lovely. got a couple of partial implementations....

update git ignore

kaas

remove large file

syncy sync
This commit is contained in:
Chris Kruining 2025-04-03 17:27:35 +02:00 committed by Chris Kruining
parent 89f526e9d9
commit 98cd4d630c
Signed by: chris
SSH key fingerprint: SHA256:nG82MUfuVdRVyCKKWqhY+pCrbz9nbX6uzUns4RKa1Pg
24 changed files with 586 additions and 76 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
dist
public/videos
.solid
.output
.vercel

View file

@ -9,6 +9,7 @@
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.3",
"better-auth": "^1.2.6",
"open-props": "^1.7.14",
"sitemap": "^8.0.0",
"solid-icons": "^1.1.0",
@ -68,6 +69,10 @@
"@babel/types": ["@babel/types@7.26.10", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ=="],
"@better-auth/utils": ["@better-auth/utils@0.2.4", "", { "dependencies": { "typescript": "^5.8.2", "uncrypto": "^0.1.3" } }, "sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="],
"@deno/shim-deno": ["@deno/shim-deno@0.19.2", "", { "dependencies": { "@deno/shim-deno-test": "^0.5.0", "which": "^4.0.0" } }, "sha512-q3VTHl44ad8T2Tw2SpeAvghdGOjlnLPDNO2cpOxwMrBE/PVas6geWpbpIgrM+czOCH0yejp0yi8OaTuB+NU40Q=="],
@ -124,6 +129,8 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.20.2", "", { "os": "win32", "cpu": "x64" }, "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ=="],
"@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="],
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
@ -142,12 +149,18 @@
"@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=="],
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
"@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.0", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg=="],
"@netlify/functions": ["@netlify/functions@3.0.4", "", { "dependencies": { "@netlify/serverless-functions-api": "1.36.0" } }, "sha512-Ox8+ABI+nsLK+c4/oC5dpquXuEIjzfTlJrdQKgQijCsDQoje7inXFAtKDLvvaGvuvE+PVpMLwQcIUL6P9Ob1hQ=="],
"@netlify/serverless-functions-api": ["@netlify/serverless-functions-api@1.36.0", "", {}, "sha512-z6okREyK8in0486a22Oro0k+YsuyEjDXJt46FpgeOgXqKJ9ElM8QPll0iuLBkpbH33ENiNbIPLd1cuClRQnhiw=="],
"@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="],
"@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
@ -186,6 +199,16 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.3.16", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw=="],
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA=="],
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg=="],
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.3.15", "", { "dependencies": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w=="],
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@poppinss/colors": ["@poppinss/colors@4.1.4", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-FA+nTU8p6OcSH4tLDY5JilGYr1bVWHpNmcLr7xmMEdbWmKHa+3QZ+DqefrXKmdjO/brHTnQZo20lLSjaO7ydog=="],
@ -270,6 +293,10 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.1.0", "", {}, "sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg=="],
"@simplewebauthn/server": ["@simplewebauthn/server@13.1.1", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8" } }, "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA=="],
"@sindresorhus/is": ["@sindresorhus/is@7.0.1", "", {}, "sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ=="],
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],
@ -334,6 +361,8 @@
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
@ -342,6 +371,8 @@
"@types/micromatch": ["@types/micromatch@4.0.9", "", { "dependencies": { "@types/braces": "*" } }, "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@22.13.14", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
@ -414,6 +445,8 @@
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
@ -438,6 +471,10 @@
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"better-auth": ["better-auth@1.2.6", "", { "dependencies": { "@better-auth/utils": "0.2.4", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.7", "defu": "^6.1.4", "jose": "^5.9.6", "kysely": "^0.27.6", "nanostores": "^0.11.3", "zod": "^3.24.1" } }, "sha512-RVy6nfNCXpohx49zP2ChUO3zN0nvz5UXuETJIhWU+dshBKpFMk4P4hAQauM3xqTJdd9hfeB5y+segmG1oYGTJQ=="],
"better-call": ["better-call@1.0.7", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-p5kEthErx3HsW9dCCvvEx+uuEdncn0ZrlqrOG3TkR1aVYgynpwYbTVU90nY8/UwfMhROzqZWs8vryainSQxrNg=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
@ -768,6 +805,8 @@
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
"js-levenshtein": ["js-levenshtein@1.1.6", "", {}, "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@ -786,6 +825,8 @@
"knitwork": ["knitwork@1.2.0", "", {}, "sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg=="],
"kysely": ["kysely@0.27.6", "", {}, "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="],
"lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
"lightningcss": ["lightningcss@1.29.3", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.29.3", "lightningcss-darwin-x64": "1.29.3", "lightningcss-freebsd-x64": "1.29.3", "lightningcss-linux-arm-gnueabihf": "1.29.3", "lightningcss-linux-arm64-gnu": "1.29.3", "lightningcss-linux-arm64-musl": "1.29.3", "lightningcss-linux-x64-gnu": "1.29.3", "lightningcss-linux-x64-musl": "1.29.3", "lightningcss-win32-arm64-msvc": "1.29.3", "lightningcss-win32-x64-msvc": "1.29.3" } }, "sha512-GlOJwTIP6TMIlrTFsxTerwC0W6OpQpCGuX1ECRLBUVRh6fpJH3xTqjCjRgQHTb4ZXexH9rtHou1Lf03GKzmhhQ=="],
@ -870,6 +911,8 @@
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="],
"nitropack": ["nitropack@2.11.7", "", { "dependencies": { "@cloudflare/kv-asset-handler": "^0.4.0", "@netlify/functions": "^3.0.2", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-inject": "^5.0.5", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-terser": "^0.4.4", "@vercel/nft": "^0.29.2", "archiver": "^7.0.1", "c12": "^3.0.2", "chokidar": "^4.0.3", "citty": "^0.1.6", "compatx": "^0.1.8", "confbox": "^0.2.1", "consola": "^3.4.2", "cookie-es": "^2.0.0", "croner": "^9.0.0", "crossws": "^0.3.4", "db0": "^0.3.1", "defu": "^6.1.4", "destr": "^2.0.3", "dot-prop": "^9.0.0", "esbuild": "^0.25.1", "escape-string-regexp": "^5.0.0", "etag": "^1.8.1", "exsolve": "^1.0.4", "globby": "^14.1.0", "gzip-size": "^7.0.0", "h3": "^1.15.1", "hookable": "^5.5.3", "httpxy": "^0.1.7", "ioredis": "^5.6.0", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", "listhen": "^1.9.0", "magic-string": "^0.30.17", "magicast": "^0.3.5", "mime": "^4.0.6", "mlly": "^1.7.4", "node-fetch-native": "^1.6.6", "node-mock-http": "^1.0.0", "ofetch": "^1.4.1", "ohash": "^2.0.11", "openapi-typescript": "^7.6.1", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.1.0", "pretty-bytes": "^6.1.1", "radix3": "^1.1.2", "rollup": "^4.36.0", "rollup-plugin-visualizer": "^5.14.0", "scule": "^1.3.0", "semver": "^7.7.1", "serve-placeholder": "^2.0.2", "serve-static": "^1.16.2", "source-map": "^0.7.4", "std-env": "^3.8.1", "ufo": "^1.5.4", "ultrahtml": "^1.5.3", "uncrypto": "^0.1.3", "unctx": "^2.4.1", "unenv": "^2.0.0-rc.15", "unimport": "^4.1.2", "unplugin-utils": "^0.2.4", "unstorage": "^1.15.0", "untyped": "^2.0.0", "unwasm": "^0.3.9", "youch": "^4.1.0-beta.6", "youch-core": "^0.3.2" }, "peerDependencies": { "xml2js": "^0.6.2" }, "optionalPeers": ["xml2js"], "bin": { "nitro": "dist/cli/index.mjs", "nitropack": "dist/cli/index.mjs" } }, "sha512-ghqLa3Q4X9qaQiUyspWxxoU1fY2nwfSJqhOH+COqyCp7Vgj4oM1EM1L0YNSQUF16T2tAoOWg8woXGq0EH5Y6wQ=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
@ -954,6 +997,10 @@
"property-information": ["property-information@7.0.0", "", {}, "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg=="],
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
"pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="],
"quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
@ -1004,6 +1051,8 @@
"rollup-plugin-visualizer": ["rollup-plugin-visualizer@5.14.0", "", { "dependencies": { "open": "^8.4.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^17.5.1" }, "peerDependencies": { "rolldown": "1.x", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA=="],
"rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@ -1026,6 +1075,8 @@
"serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],

View file

@ -19,6 +19,7 @@
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.1.3",
"better-auth": "^1.2.6",
"open-props": "^1.7.14",
"sitemap": "^8.0.0",
"solid-icons": "^1.1.0",

29
src/auth.ts Normal file
View file

@ -0,0 +1,29 @@
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
import { createAuthClient } from "better-auth/solid";
import { genericOAuthClient } from "better-auth/client/plugins";
export const auth = betterAuth({
plugins: [
genericOAuth({
config: [
{
providerId: "authelia",
clientId: "streamarr",
clientSecret:
"ZPuiW2gpVV6MGXIJFk5P3EeSW8V_ICgqduF.hJVCKkrnVmRqIQXRk0o~HSA8ZdCf8joA4m_F",
discoveryUrl:
"https://auth.kruining.eu/.well-known/openid-configuration",
scopes: ["openid", "email", "picture", "profile", "groups"],
accessType: "offline",
pkce: true,
},
],
}),
],
});
export const { signIn, signOut, useSession, ...client } = createAuthClient({
baseURL: "http://localhost:3000",
plugins: [genericOAuthClient()],
});

View file

@ -0,0 +1,39 @@
WEBVTT
00:02.170 --> 00:04.136
Emo, close your eyes
00:04.136 --> 00:05.597
Why?
NOW!
00:05.597 --> 00:07.405
Ok
00:07.405 --> 00:08.803
Good
00:08.803 --> 00:11.541
What do you see at your left side Emo?
00:11.541 --> 00:13.287
Well?
00:13.287 --> 00:16.110
Er nothing?
Really?
00:16.110 --> 00:18.514
No, nothing at all!
00:18.514 --> 00:22.669
Really? and at your right? What do you see at your right side Emo?
00:22.669 --> 00:26.111
Umm, the same Proog
00:26.111 --> 00:28.646
Exactly the same! Nothing!
00:28.646 --> 00:30.794
Great

Binary file not shown.

View file

@ -0,0 +1,54 @@
VTT
1
00:00:00.000 --> 00:00:01.000
overview.jpg#xywh=0,0,320,180
2
00:00:01.000 --> 00:00:02.000
overview.jpg#xywh=320,0,320,180
3
00:00:02.000 --> 00:00:03.000
overview.jpg#xywh=640,0,320,180
00:00:03.000 --> 00:00:04.000
overview.jpg#xywh=960,0,320,180
00:00:04.000 --> 00:00:05.000
overview.jpg#xywh=1280,0,320,180
00:00:05.000 --> 00:00:06.000
overview.jpg#xywh=1600,0,320,180
00:00:06.000 --> 00:00:07.000
overview.jpg#xywh=1920,0,320,180
00:00:07.000 --> 00:00:08.000
overview.jpg#xywh=2240,0,320,180
00:00:08.000 --> 00:00:09.000
overview.jpg#xywh=0,180,320,180
00:00:09.000 --> 00:00:10.000
overview.jpg#xywh=320,180,320,180
00:00:10.000 --> 00:00:11.000
overview.jpg#xywh=640,180,320,180
00:00:11.000 --> 00:00:12.000
overview.jpg#xywh=960,180,320,180
00:00:12.000 --> 00:00:13.000
overview.jpg#xywh=1280,180,320,180
00:00:13.000 --> 00:00:14.000
overview.jpg#xywh=1600,180,320,180
00:00:14.000 --> 00:00:15.000
overview.jpg#xywh=1920,180,320,180
00:00:15.000 --> 00:00:16.000
overview.jpg#xywh=2240,180,320,180

View file

@ -0,0 +1,3 @@
.container {
display: block grid;
}

View file

@ -0,0 +1,17 @@
import { Component, createSignal } from "solid-js";
import css from "./volume.module.css";
interface VolumeProps {
value: number;
}
export const Volume: Component<VolumeProps> = (props) => {
const [volume, setVolume] = createSignal(props.value);
return (
<div class={css.container}>
<button>mute</button>
<input type="range" value={volume()} min="0" max="1" step="0.01" />
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -0,0 +1,5 @@
.player {
& > video::cue {
font-size: 1.5rem;
}
}

View file

@ -12,22 +12,90 @@ import {
onMount,
untrack,
} from "solid-js";
import css from "./player.module.css";
import { Volume } from "./controls/volume";
const metadata = query(async (id: string) => {
"use server";
// thumbnail sprite image created with
// ```bash
// mkdir -p thumbs \
// && ffmpeg -i SampleVideo_1280x720_10mb.mp4 -r 1 -s 320x180 -f image2 thumbs/thumb-%d.jpg \
// && montage thumbs/*.jpg -geometry 320x180 -tile 8x overview.jpg \
// && rm -rf thumbs
// ```
//
// 1. create thumbs directory
// 2. create image every 1 second
// 3. create sprite from images
// 4. remove thumbs
const path = `${import.meta.dirname}/SampleVideo_1280x720_10mb`;
return json({
captions: await Bun.file(`${path}.captions.vtt`).bytes(),
thumbnails: {
track: await Bun.file(`${path}.thumbnails.vtt`).text(),
image: await Bun.file(`${import.meta.dirname}/overview.jpg`).bytes(),
},
});
}, "player.metadata");
interface PlayerProps {
id: string;
}
export const Player: Component<PlayerProps> = (props) => {
const [video, setVideo] = createSignal<HTMLVideoElement>(undefined as unknown as HTMLVideoElement);
const [video, setVideo] = createSignal<HTMLVideoElement>(
undefined as unknown as HTMLVideoElement,
);
const data = createAsync(() => metadata(props.id), {
deferStream: true,
initialValue: {},
});
const captionUrl = createMemo(() => {
const { captions } = data();
const onDurationChange = createEventSignal(video, 'durationchange');
const onTimeUpdate = createEventSignal(video, 'timeupdate');
return captions !== undefined
? URL.createObjectURL(new Blob([captions], { type: "text/vtt" }))
: "";
});
const thumbnails = createMemo(() => {
const { thumbnails } = data();
return thumbnails !== undefined
? URL.createObjectURL(new Blob([thumbnails.track], { type: "text/vtt" }))
: "";
});
createEffect(() => {
const metadata = data();
const el = video();
if (metadata === undefined || el === undefined) {
return;
}
console.log(metadata);
});
createEffect(() => {
thumbnails();
console.log(video()!.textTracks.getTrackById("thumbnails")?.cues);
// const captions = el.addTextTrack("captions", "English", "en");
// captions.
});
const onDurationChange = createEventSignal(video, "durationchange");
const onTimeUpdate = createEventSignal(video, "timeupdate");
const duration = createMemo(() => {
onDurationChange();
onTimeUpdate();
return video()?.duration ?? 100;
return video()?.duration ?? 0;
});
const currentTime = createMemo(() => {
@ -36,10 +104,6 @@ export const Player: Component<PlayerProps> = (props) => {
return video()?.currentTime ?? 0;
});
createEffect(() => {
console.log(duration(), currentTime());
});
createEventListenerMap(() => video()!, {
durationchange(e) {
console.log("durationchange", e);
@ -60,7 +124,11 @@ export const Player: Component<PlayerProps> = (props) => {
console.log("seeking", e);
},
stalled(e) {
console.log("stalled (meaning downloading data failed)", e, video()!.error);
console.log(
"stalled (meaning downloading data failed)",
e,
video()!.error,
);
},
play(e) {
@ -107,17 +175,52 @@ export const Player: Component<PlayerProps> = (props) => {
};
return (
<>
<figure class={css.player}>
<h1>{props.id}</h1>
<video ref={setVideo} width="1280px" height="720px" muted src="/api/stream/video" />
<video
ref={setVideo}
muted
autoplay
controls
src={`/api/content/stream?id=${props.id}`}
lang="en"
>
<track
default
kind="captions"
label="English"
srclang="en"
src={captionUrl()}
/>
<track default kind="chapters" src={thumbnails()} id="thumbnails" />
{/* <track kind="captions" />
<track kind="chapters" />
<track kind="descriptions" />
<track kind="metadata" />
<track kind="subtitles" /> */}
</video>
<figcaption>
<Volume value={0.5} />
</figcaption>
<button onclick={toggle}>play/pause</button>
<span style={{ '--duration': duration(), '--current-time': currentTime() }} />
<span data-duration={duration()} data-current-time={currentTime()} />
<progress max={duration()} value={currentTime()} />
</>
<span>
{formatTime(currentTime())} / {formatTime(duration())}
</span>
<progress max={duration().toFixed(0)} value={currentTime().toFixed(0)} />
</figure>
);
};
const formatTime = (subject: number) => {
const hours = Math.floor(subject / 3600);
const minutes = Math.floor((subject % 3600) / 60);
const seconds = Math.floor(subject % 60);
const sections = hours !== 0 ? [hours, minutes, seconds] : [minutes, seconds];
return sections.map((section) => String(section).padStart(2, "0")).join(":");
};

View file

@ -1,10 +1,64 @@
import { Component } from "solid-js";
import { Component, createEffect, createMemo, Show } from "solid-js";
import { ColorSchemePicker } from "../theme";
import { signIn, signOut, useSession } from "~/auth";
import { hash } from "~/utilities";
import css from "./top.module.css";
export const Top: Component = (props) => {
const session = useSession();
const hashedEmail = hash("SHA-256", () => session().data?.user.email);
const login = async () => {
const response = await signIn.oauth2({
providerId: "authelia",
callbackURL: "/",
});
console.log("signin response", response);
};
const logout = async () => {
const response = await signOut();
console.log("signout response", response);
};
createEffect(() => {
console.log(hashedEmail());
});
return (
<aside class={css.top}>
<Show
when={session().isPending === false && session().isRefetching === false}
>
<Show
when={session().data?.user}
fallback={
<form method="post" onSubmit={login}>
<button type="submit">Sign in</button>
</form>
}
>
{(user) => (
<>
<div>
<img
src={
user().image ??
`https://www.gravatar.com/avatar/${hashedEmail()}`
}
/>
<span>{user().name}</span>
<span>{user().email}</span>
</div>
<form method="post" onSubmit={logout}>
<button type="submit">Log out</button>
</form>
</>
)}
</Show>
</Show>
<ColorSchemePicker />
</aside>
);

View file

@ -1,43 +1,69 @@
import { WiMoonAltFirstQuarter, WiMoonAltFull, WiMoonAltNew } from "solid-icons/wi";
import { Component, createEffect, For, Match, on, Setter, Switch } from "solid-js";
import {
WiMoonAltFirstQuarter,
WiMoonAltFull,
WiMoonAltNew,
} from "solid-icons/wi";
import {
Component,
createEffect,
For,
Match,
on,
Setter,
Switch,
} from "solid-js";
import { ColorScheme, useTheme } from "./context";
import css from './picker.module.css';
import css from "./picker.module.css";
import { Select } from "~/components/select";
const colorSchemes: Record<ColorScheme, keyof typeof ColorScheme> = Object.fromEntries(Object.entries(ColorScheme).map(([k, v]) => [v, k])) as any;
const colorSchemes: Record<ColorScheme, keyof typeof ColorScheme> =
Object.fromEntries(
Object.entries(ColorScheme).map(([k, v]) => [v, k]),
) as any;
export const ColorSchemePicker: Component = (props) => {
const themeContext = useTheme();
const themeContext = useTheme();
const setScheme: Setter<ColorScheme> = (next) => {
const setScheme: Setter<ColorScheme> = (next) => {
if (typeof next === "function") {
next = next();
}
if (typeof next === 'function') {
next = next();
}
themeContext.setColorScheme(next);
};
themeContext.setColorScheme(next);
};
return (
<>
<label aria-label="Color scheme picker">
<Select
id="color-scheme-picker"
class={css.picker}
value={themeContext.theme.colorScheme}
setValue={setScheme}
values={colorSchemes}
>
{(k, v) => (
<>
<Switch>
<Match when={k === ColorScheme.Auto}>
<WiMoonAltFirstQuarter />
</Match>
<Match when={k === ColorScheme.Light}>
<WiMoonAltNew />
</Match>
<Match when={k === ColorScheme.Dark}>
<WiMoonAltFull />
</Match>
</Switch>
{v}
</>
)}
</Select>
</label>
createEffect(on(() => themeContext.theme.colorScheme, (colorScheme) => {
console.log(colorScheme);
}));
return <>
<label aria-label="Color scheme picker">
<Select id="color-scheme-picker" class={css.picker} value={themeContext.theme.colorScheme} setValue={setScheme} values={colorSchemes}>{
(k, v) => <>
<Switch>
<Match when={k === ColorScheme.Auto}><WiMoonAltFirstQuarter /></Match>
<Match when={k === ColorScheme.Light}><WiMoonAltNew /></Match>
<Match when={k === ColorScheme.Dark}><WiMoonAltFull /></Match>
</Switch>
{v}
</>
}</Select>
</label>
{/* <label class={css.hue} aria-label="Hue slider">
{/* <label class={css.hue} aria-label="Hue slider">
<input type="range" min="0" max="360" value={theme.hue} onInput={e => setHue(e.target.valueAsNumber)} />
</label> */}
</>;
};
</>
);
};

View file

@ -0,0 +1,11 @@
import { Component, createMemo, Show } from "solid-js";
export const Avatar: Component = (props) => {
const src = createMemo(() => "");
return (
<Show when={src()}>
<img src={src()} />
</Show>
);
};

View file

@ -0,0 +1 @@
export { Avatar } from "./avatar";

View file

@ -0,0 +1,5 @@
export interface User {
name: string;
email: string;
image: string;
}

View file

@ -1,5 +1,5 @@
import { Meta } from "@solidjs/meta";
import { query, createAsync, action } from "@solidjs/router";
import { createEffect, on, ParentProps } from "solid-js";
import { Shell } from "~/features/shell";
import { useTheme } from "~/features/theme";
@ -7,13 +7,20 @@ import { useTheme } from "~/features/theme";
export default function ShellPage(props: ParentProps) {
const themeContext = useTheme();
createEffect(on(() => themeContext.theme.colorScheme, (colorScheme) => {
document.documentElement.dataset.theme = colorScheme;
}));
createEffect(
on(
() => themeContext.theme.colorScheme,
(colorScheme) => {
document.documentElement.dataset.theme = colorScheme;
},
),
);
return <Shell>
<Meta name="color-scheme" content={themeContext.theme.colorScheme} />
return (
<Shell>
<Meta name="color-scheme" content={themeContext.theme.colorScheme} />
{props.children}
</Shell>;
{props.children}
</Shell>
);
}

View file

@ -1,10 +1,17 @@
import { json, Params, query, redirect, RouteDefinition, useParams } from "@solidjs/router";
import {
json,
Params,
query,
redirect,
RouteDefinition,
useParams,
} from "@solidjs/router";
import { createSlug, getEntry } from "~/features/content";
import { Player } from "~/features/player";
import { toSlug } from "~/utilities";
const healUrl = query(async (slug: string) => {
const entry = await getEntry(slug.slice(slug.lastIndexOf('-') + 1));
const entry = await getEntry(slug.slice(slug.lastIndexOf("-") + 1));
if (entry === undefined) {
return json(null, { status: 404 });
@ -17,11 +24,10 @@ const healUrl = query(async (slug: string) => {
}
throw redirect(`/watch/${actualSlug}`);
}, 'watch.heal');
}, "watch.heal");
interface ItemParams extends Params {
title: string;
id: string;
slug: string;
}
export const route = {
@ -31,11 +37,12 @@ export const route = {
} satisfies RouteDefinition;
export default function Item() {
const params = useParams<ItemParams>();
const { slug } = useParams<ItemParams>();
const id = slug.slice(slug.lastIndexOf("-") + 1);
return (
<>
<Player id={params.id} />
<Player id={id} />
</>
);
}

View file

@ -0,0 +1,4 @@
import { auth } from "~/auth";
import { toSolidStartHandler } from "better-auth/solid-start";
export const { GET, POST } = toSolidStartHandler(auth);

Binary file not shown.

View file

@ -0,0 +1,16 @@
import { json } from "@solidjs/router";
import { APIEvent } from "@solidjs/start/server";
export const GET = async (event: APIEvent) => {
console.log(event.params);
const path = `${import.meta.dirname}/SampleVideo_1280x720_10mb`;
return json({
captions: await Bun.file(`${path}.captions.vtt`).bytes(),
thumbnails: {
track: await Bun.file(`${path}.thumbnails.vtt`).text(),
image: await Bun.file(`${import.meta.dirname}/overview.jpg`).bytes(),
},
});
};

View file

@ -0,0 +1,45 @@
import { APIEvent } from "@solidjs/start/server";
const CHUNK_SIZE = 1 * 1e6; // 1MB
export const GET = async ({ request, ...event }: APIEvent) => {
"use server";
const range = request.headers.get("range");
if (range === null) {
return new Response("Requires Range header", { status: 400 });
}
try {
const file = Bun.file(
import.meta.dirname + "/SampleVideo_1280x720_10mb.mp4",
);
if ((await file.exists()) !== true) {
return new Response("File not found", { status: 404 });
}
const videoSize = file.size;
const start = Number.parseInt(range.replace(/\D/g, ""));
const end = Math.min(start + CHUNK_SIZE, videoSize - 1);
const contentLength = end - start + 1;
return new Response(file.stream());
// return new Response(video.slice(start, end).stream(), {
// status: 206,
// headers: {
// 'Accept-Ranges': 'bytes',
// 'Content-Range': `bytes ${start}-${end}/${videoSize}`,
// 'Content-Length': `${contentLength}`,
// 'Content-type': 'video/mp4',
// },
// });
} catch (e) {
console.error(e);
throw e;
}
};

View file

@ -1,15 +1,46 @@
export const splitAt = (subject: string, index: number): readonly [string, string] => {
if (index < 0) {
return [subject, ''];
}
import { Accessor, createEffect, createSignal, on } from "solid-js";
if (index > subject.length) {
return [subject, ''];
}
export const splitAt = (
subject: string,
index: number,
): readonly [string, string] => {
if (index < 0) {
return [subject, ""];
}
return [subject.slice(0, index), subject.slice(index + 1)];
if (index > subject.length) {
return [subject, ""];
}
return [subject.slice(0, index), subject.slice(index + 1)];
};
export const toSlug = (subject: string) => {
return subject.toLowerCase().replaceAll(' ', '-');
};
export const toSlug = (subject: string) =>
subject.toLowerCase().replaceAll(" ", "-");
export const toHex = (subject: number) => subject.toString(16).padStart(2, "0");
const encoder = new TextEncoder();
export const hash = (
algorithm: AlgorithmIdentifier,
subject: Accessor<string | null | undefined>,
) => {
const [hash, setHash] = createSignal<string>();
createEffect(
on(subject, async (subject) => {
if (subject === null || subject === undefined || subject.length === 0) {
setHash(undefined);
return;
}
const buffer = new Uint8Array(
await crypto.subtle.digest(algorithm, encoder.encode(subject)),
);
setHash(Array.from(buffer).map(toHex).join(""));
}),
);
return hash;
};