feat: W2G, persist search state tru navigation

fix: window title dissapearing lol
This commit is contained in:
ThaUnknown 2025-05-21 21:56:14 +02:00
parent db2b0a738a
commit 516f26d765
No known key found for this signature in database
24 changed files with 440 additions and 368 deletions

27
.vscode/settings.json vendored
View file

@ -1,13 +1,10 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": "always" "source.fixAll.eslint": "always"
}, },
"editor.formatOnSave": true, "editor.formatOnSave": true,
"extensions.ignoreRecommendations": false, "editor.linkedEditing": true,
"eslint.useESLintClass": true, "editor.tabSize": 2,
"eslint.useFlatConfig": true,
"eslint.format.enable": true, "eslint.format.enable": true,
"eslint.probe": [ "eslint.probe": [
"javascript", "javascript",
@ -17,6 +14,8 @@
"svelte", "svelte",
"html" "html"
], ],
"eslint.useESLintClass": true,
"eslint.useFlatConfig": true,
"eslint.validate": [ "eslint.validate": [
"javascript", "javascript",
"javascriptreact", "javascriptreact",
@ -24,5 +23,21 @@
"typescriptreact", "typescriptreact",
"svelte", "svelte",
"html" "html"
] ],
"javascript.preferences.importModuleSpecifierEnding": "minimal",
"javascript.preferences.quoteStyle": "single",
"javascript.suggest.autoImports": true,
"javascript.updateImportsOnFileMove.enabled": "always",
"javascript.validate.enable": true,
"extensions.ignoreRecommendations": false,
"svelte.plugin.svelte.format.config.singleQuote": true,
"svelte.plugin.svelte.format.enable": false,
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.experimental.expandableHover": true,
"typescript.preferences.importModuleSpecifierEnding": "minimal",
"typescript.preferences.quoteStyle": "single",
"typescript.suggest.autoImports": true,
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.updateImportsOnFileMove.enabled": "always",
"typescript.validate.enable": true
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "ui", "name": "ui",
"version": "6.3.16", "version": "6.3.17",
"license": "BUSL-1.1", "license": "BUSL-1.1",
"private": true, "private": true,
"packageManager": "pnpm@9.14.4", "packageManager": "pnpm@9.14.4",
@ -36,6 +36,7 @@
"svelte-radix": "^1.1.1", "svelte-radix": "^1.1.1",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"vaul-svelte": "^0.3.2", "vaul-svelte": "^0.3.2",
"vite": "^5.4.11" "vite": "^5.4.11"
}, },
@ -63,7 +64,7 @@
"js-levenshtein": "^1.1.6", "js-levenshtein": "^1.1.6",
"lucide-svelte": "^0.511.0", "lucide-svelte": "^0.511.0",
"marked": "^15.0.11", "marked": "^15.0.11",
"p2pt": "^1.5.1", "p2pt": "github:ThaUnknown/p2pt#modernise",
"rollup-plugin-license": "^3.6.0", "rollup-plugin-license": "^3.6.0",
"semver": "^7.7.2", "semver": "^7.7.2",
"simple-store-svelte": "^1.0.6", "simple-store-svelte": "^1.0.6",

View file

@ -75,8 +75,8 @@ importers:
specifier: ^15.0.11 specifier: ^15.0.11
version: 15.0.11 version: 15.0.11
p2pt: p2pt:
specifier: ^1.5.1 specifier: github:ThaUnknown/p2pt#modernise
version: 1.5.1 version: https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316
rollup-plugin-license: rollup-plugin-license:
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0(picomatch@4.0.2)(rollup@4.40.2) version: 3.6.0(picomatch@4.0.2)(rollup@4.40.2)
@ -171,6 +171,9 @@ importers:
tailwindcss: tailwindcss:
specifier: ^3.4.17 specifier: ^3.4.17
version: 3.4.17 version: 3.4.17
typescript:
specifier: ^5.8.3
version: 5.8.3
vaul-svelte: vaul-svelte:
specifier: ^0.3.2 specifier: ^0.3.2
version: 0.3.2(svelte@4.2.19) version: 0.3.2(svelte@4.2.19)
@ -644,6 +647,12 @@ packages:
'@swc/helpers@0.5.17': '@swc/helpers@0.5.17':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
'@thaunknown/simple-peer@9.12.1':
resolution: {integrity: sha512-IS5BXvXx7cvBAzaxqotJf4s4rJCPk5JABLK6Gbnn7oAmWVcH4hYABabBBrvvJtv/xyUqR4v/H3LalnGRJJfEog==}
'@thaunknown/simple-websocket@9.1.3':
resolution: {integrity: sha512-pf/FCJsgWtLJiJmIpiSI7acOZVq3bIQCpnNo222UFc8Ph1lOUOTpe6LoYhhiOSKB9GUaWJEVUtZ+sK1/aBgU5Q==}
'@thaunknown/web-irc@1.0.1': '@thaunknown/web-irc@1.0.1':
resolution: {integrity: sha512-oP+mrvD2U7gSXHTfT77+A+i2YVT5jp4qCbCXLrNU9aFzXdJ0iRsBbdhdT/AgeB7Nf4O+SC/wVCnwhnAfUoo0Fg==} resolution: {integrity: sha512-oP+mrvD2U7gSXHTfT77+A+i2YVT5jp4qCbCXLrNU9aFzXdJ0iRsBbdhdT/AgeB7Nf4O+SC/wVCnwhnAfUoo0Fg==}
@ -770,8 +779,9 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
addr-to-ip-port@1.5.4: addr-to-ip-port@2.0.0:
resolution: {integrity: sha512-ByxmJgv8vjmDcl3IDToxL2yrWFrRtFpZAToY0f46XFXl8zS081t7El5MXIodwm7RC6DhHBRoOSMLFSPKCtHukg==} resolution: {integrity: sha512-9bYbtjamtdLHZSqVIUXhilOryNPiL+x+Q5J/Unpg4VY3ZIkK3fT52UoErj1NdUeVm3J1t2iBEAur4Ywbl/bahw==}
engines: {node: '>=12.20.0'}
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@ -873,11 +883,9 @@ packages:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'} engines: {node: '>= 0.6.0'}
base64-js@1.5.1: bencode@4.0.0:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==}
engines: {node: '>=12.20.0'}
bencode@2.0.3:
resolution: {integrity: sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==}
binary-extensions@2.3.0: binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
@ -896,14 +904,11 @@ packages:
bittorrent-peerid@1.3.6: bittorrent-peerid@1.3.6:
resolution: {integrity: sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg==} resolution: {integrity: sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg==}
bittorrent-tracker@9.19.0: bittorrent-tracker@10.0.12:
resolution: {integrity: sha512-09d0aD2b+MC+zWvWajkUAKkYMynYW4tMbTKiRSthKtJZbafzEoNQSUHyND24SoCe3ZOb2fKfa6fu2INAESL9wA==} resolution: {integrity: sha512-EYQEwhOYkrRiiwkCFcM9pbzJInsAe7UVmUgevW133duwlZzjwf5ABwDE7pkkmNRS6iwN0b8LbI/94q16dYqiow==}
engines: {node: '>=12'} engines: {node: '>=12.20.0'}
hasBin: true hasBin: true
bn.js@5.2.2:
resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==}
bottleneck@2.19.5: bottleneck@2.19.5:
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
@ -925,9 +930,6 @@ packages:
buffer-from@1.1.2: buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
bufferutil@4.0.9: bufferutil@4.0.9:
resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==}
engines: {node: '>=6.14.2'} engines: {node: '>=6.14.2'}
@ -1497,9 +1499,6 @@ packages:
idb-keyval@6.2.2: idb-keyval@6.2.2:
resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==} resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2: ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@ -1893,8 +1892,9 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
p2pt@1.5.1: p2pt@https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316:
resolution: {integrity: sha512-q1pkIKBRvGcQfv5Q3W0/c9pbBUnjcauWylc/qUZwIqcrQIxu3rfuDQXsqjwEJaBwdPNPWMY06jc5qxwVgJX6MA==} resolution: {tarball: https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316}
version: 1.5.1
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
@ -2034,9 +2034,6 @@ packages:
random-iterate@1.0.1: random-iterate@1.0.1:
resolution: {integrity: sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA==} resolution: {integrity: sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA==}
randombytes@2.1.0:
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
react@19.0.0: react@19.0.0:
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -2044,10 +2041,6 @@ packages:
read-cache@1.0.0: read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
readdirp@3.6.0: readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
@ -2097,9 +2090,6 @@ packages:
run-series@1.1.9: run-series@1.1.9:
resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==} resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==}
rusha@0.8.14:
resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==}
rvfc-polyfill@1.0.7: rvfc-polyfill@1.0.7:
resolution: {integrity: sha512-seBl7J1J3/k0LuzW2T9fG6JIOpni5AbU+/87LA+zTYKgTVhsfShmS8K/yOo1eeEjGJHnAdkVAUUM+PEjN9Mpkw==} resolution: {integrity: sha512-seBl7J1J3/k0LuzW2T9fG6JIOpni5AbU+/87LA+zTYKgTVhsfShmS8K/yOo1eeEjGJHnAdkVAUUM+PEjN9Mpkw==}
@ -2111,9 +2101,6 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-push-apply@1.0.0: safe-push-apply@1.0.0:
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2180,18 +2167,9 @@ packages:
simple-get@4.0.1: simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
simple-peer@9.11.1:
resolution: {integrity: sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==}
simple-sha1@3.1.0:
resolution: {integrity: sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg==}
simple-store-svelte@1.0.6: simple-store-svelte@1.0.6:
resolution: {integrity: sha512-39TaQ2LHRAdH+cpWPmGzDfVyoAm/uZ6UkM27O3YhUtJHk0Vw09+6/jUqprns+BgOkKxOkRetKC9SH4bbj0IZ1A==} resolution: {integrity: sha512-39TaQ2LHRAdH+cpWPmGzDfVyoAm/uZ6UkM27O3YhUtJHk0Vw09+6/jUqprns+BgOkKxOkRetKC9SH4bbj0IZ1A==}
simple-websocket@9.1.0:
resolution: {integrity: sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==}
sirv@3.0.1: sirv@3.0.1:
resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==}
engines: {node: '>=18'} engines: {node: '>=18'}
@ -2262,11 +2240,9 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
string2compact@1.3.2: string2compact@2.0.1:
resolution: {integrity: sha512-3XUxUgwhj7Eqh2djae35QHZZT4mN3fsO7kagZhSGmhhlrQagVvWSFuuFIWnpxFS0CdTB2PlQcaL16RDi14I8uw==} resolution: {integrity: sha512-Bm/T8lHMTRXw+u83LE+OW7fXmC/wM+Mbccfdo533ajSBNxddDHlRrvxE49NdciGHgXkUQM5WYskJ7uTkbBUI0A==}
engines: {node: '>=12.20.0'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1: strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
@ -2600,12 +2576,12 @@ packages:
bundledDependencies: bundledDependencies:
- node-pre-gyp - node-pre-gyp
ws@7.5.10: ws@8.18.2:
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
engines: {node: '>=8.3.0'} engines: {node: '>=10.0.0'}
peerDependencies: peerDependencies:
bufferutil: ^4.0.1 bufferutil: ^4.0.1
utf-8-validate: ^5.0.2 utf-8-validate: '>=5.0.2'
peerDependenciesMeta: peerDependenciesMeta:
bufferutil: bufferutil:
optional: true optional: true
@ -3010,6 +2986,29 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
'@thaunknown/simple-peer@9.12.1':
dependencies:
debug: 4.4.1
err-code: 3.0.1
get-browser-rtc: 1.1.0
queue-microtask: 1.2.3
streamx: 2.22.0
uint8-util: 2.2.5
transitivePeerDependencies:
- supports-color
'@thaunknown/simple-websocket@9.1.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
dependencies:
debug: 4.4.1
queue-microtask: 1.2.3
streamx: 2.22.0
uint8-util: 2.2.5
ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
'@thaunknown/web-irc@1.0.1': '@thaunknown/web-irc@1.0.1':
dependencies: dependencies:
grapheme-splitter: 1.0.4 grapheme-splitter: 1.0.4
@ -3165,7 +3164,7 @@ snapshots:
acorn@8.14.1: {} acorn@8.14.1: {}
addr-to-ip-port@1.5.4: {} addr-to-ip-port@2.0.0: {}
ajv@6.12.6: ajv@6.12.6:
dependencies: dependencies:
@ -3276,9 +3275,9 @@ snapshots:
base64-arraybuffer@1.0.2: {} base64-arraybuffer@1.0.2: {}
base64-js@1.5.1: {} bencode@4.0.0:
dependencies:
bencode@2.0.3: {} uint8-util: 2.2.5
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
@ -3298,11 +3297,12 @@ snapshots:
bittorrent-peerid@1.3.6: {} bittorrent-peerid@1.3.6: {}
bittorrent-tracker@9.19.0: bittorrent-tracker@10.0.12:
dependencies: dependencies:
bencode: 2.0.3 '@thaunknown/simple-peer': 9.12.1
'@thaunknown/simple-websocket': 9.1.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)
bencode: 4.0.0
bittorrent-peerid: 1.3.6 bittorrent-peerid: 1.3.6
bn.js: 5.2.2
chrome-dgram: 3.0.6 chrome-dgram: 3.0.6
clone: 2.1.2 clone: 2.1.2
compact2string: 1.4.1 compact2string: 1.4.1
@ -3313,24 +3313,20 @@ snapshots:
once: 1.4.0 once: 1.4.0
queue-microtask: 1.2.3 queue-microtask: 1.2.3
random-iterate: 1.0.1 random-iterate: 1.0.1
randombytes: 2.1.0
run-parallel: 1.2.0 run-parallel: 1.2.0
run-series: 1.1.9 run-series: 1.1.9
simple-get: 4.0.1 simple-get: 4.0.1
simple-peer: 9.11.1
simple-websocket: 9.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
socks: 2.8.4 socks: 2.8.4
string2compact: 1.3.2 string2compact: 2.0.1
uint8-util: 2.2.5
unordered-array-remove: 1.0.2 unordered-array-remove: 1.0.2
ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)
optionalDependencies: optionalDependencies:
bufferutil: 4.0.9 bufferutil: 4.0.9
utf-8-validate: 5.0.10 utf-8-validate: 5.0.10
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
bn.js@5.2.2: {}
bottleneck@2.19.5: {} bottleneck@2.19.5: {}
brace-expansion@1.1.11: brace-expansion@1.1.11:
@ -3356,11 +3352,6 @@ snapshots:
buffer-from@1.1.2: buffer-from@1.1.2:
optional: true optional: true
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
bufferutil@4.0.9: bufferutil@4.0.9:
dependencies: dependencies:
node-gyp-build: 4.8.4 node-gyp-build: 4.8.4
@ -4068,8 +4059,6 @@ snapshots:
idb-keyval@6.2.2: {} idb-keyval@6.2.2: {}
ieee754@1.2.1: {}
ignore@5.3.2: {} ignore@5.3.2: {}
ignore@7.0.4: {} ignore@7.0.4: {}
@ -4434,11 +4423,10 @@ snapshots:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
p2pt@1.5.1: p2pt@https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316:
dependencies: dependencies:
bittorrent-tracker: 9.19.0 bittorrent-tracker: 10.0.12
randombytes: 2.1.0 uint8-util: 2.2.5
simple-sha1: 3.1.0
optionalDependencies: optionalDependencies:
wrtc: 0.4.7 wrtc: 0.4.7
transitivePeerDependencies: transitivePeerDependencies:
@ -4551,22 +4539,12 @@ snapshots:
random-iterate@1.0.1: {} random-iterate@1.0.1: {}
randombytes@2.1.0:
dependencies:
safe-buffer: 5.2.1
react@19.0.0: {} react@19.0.0: {}
read-cache@1.0.0: read-cache@1.0.0:
dependencies: dependencies:
pify: 2.3.0 pify: 2.3.0
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
readdirp@3.6.0: readdirp@3.6.0:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
@ -4651,8 +4629,6 @@ snapshots:
run-series@1.1.9: {} run-series@1.1.9: {}
rusha@0.8.14: {}
rvfc-polyfill@1.0.7: {} rvfc-polyfill@1.0.7: {}
sade@1.8.1: sade@1.8.1:
@ -4667,8 +4643,6 @@ snapshots:
has-symbols: 1.1.0 has-symbols: 1.1.0
isarray: 2.0.5 isarray: 2.0.5
safe-buffer@5.2.1: {}
safe-push-apply@1.0.0: safe-push-apply@1.0.0:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
@ -4752,37 +4726,8 @@ snapshots:
once: 1.4.0 once: 1.4.0
simple-concat: 1.0.1 simple-concat: 1.0.1
simple-peer@9.11.1:
dependencies:
buffer: 6.0.3
debug: 4.4.1
err-code: 3.0.1
get-browser-rtc: 1.1.0
queue-microtask: 1.2.3
randombytes: 2.1.0
readable-stream: 3.6.2
transitivePeerDependencies:
- supports-color
simple-sha1@3.1.0:
dependencies:
queue-microtask: 1.2.3
rusha: 0.8.14
simple-store-svelte@1.0.6: {} simple-store-svelte@1.0.6: {}
simple-websocket@9.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10):
dependencies:
debug: 4.4.1
queue-microtask: 1.2.3
randombytes: 2.1.0
readable-stream: 3.6.2
ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
sirv@3.0.1: sirv@3.0.1:
dependencies: dependencies:
'@polka/url': 1.0.0-next.29 '@polka/url': 1.0.0-next.29
@ -4878,15 +4823,11 @@ snapshots:
define-properties: 1.2.1 define-properties: 1.2.1
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
string2compact@1.3.2: string2compact@2.0.1:
dependencies: dependencies:
addr-to-ip-port: 1.5.4 addr-to-ip-port: 2.0.0
ipaddr.js: 2.2.0 ipaddr.js: 2.2.0
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1: strip-ansi@6.0.1:
dependencies: dependencies:
ansi-regex: 5.0.1 ansi-regex: 5.0.1
@ -5271,7 +5212,7 @@ snapshots:
domexception: 1.0.1 domexception: 1.0.1
optional: true optional: true
ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10): ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10):
optionalDependencies: optionalDependencies:
bufferutil: 4.0.9 bufferutil: 4.0.9
utf-8-validate: 5.0.10 utf-8-validate: 5.0.10

6
src/app.d.ts vendored
View file

@ -137,9 +137,9 @@ declare global {
} }
} }
declare module '*.svelte' { // declare module '*.svelte' {
export default SvelteComponentTyped // export default SvelteComponentTyped
} // }
} }
declare module '*.svelte' { declare module '*.svelte' {

View file

@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Hayase</title>
<link rel="icon" href="%sveltekit.assets%/logo.svg" /> <link rel="icon" href="%sveltekit.assets%/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0" />
%sveltekit.head% %sveltekit.head%

View file

@ -1,6 +1,5 @@
<script lang='ts'> <script lang='ts'>
import { getPFP, type ChatMessage } from '.' import type { ChatMessage } from '.'
import type { Writable } from 'simple-store-svelte' import type { Writable } from 'simple-store-svelte'
export let messages: Writable<ChatMessage[]> export let messages: Writable<ChatMessage[]>
@ -9,7 +8,7 @@
if (!messages.length) return [] if (!messages.length) return []
const grouped = [] const grouped = []
for (const { message, user, type, date } of messages) { for (const { message, user, type, date } of messages) {
const last = grouped[grouped.length - 1] const last = grouped[grouped.length - 1]!
if (grouped.length && last.user.id === user.id) { if (grouped.length && last.user.id === user.id) {
last.messages.push(message) last.messages.push(message)
} else { } else {
@ -23,11 +22,11 @@
{#each groupMessages($messages) as { type, user, date, messages }, i (i)} {#each groupMessages($messages) as { type, user, date, messages }, i (i)}
{@const incoming = type === 'incoming'} {@const incoming = type === 'incoming'}
<div class='message flex flex-row mt-3' class:flex-row={incoming} class:flex-row-reverse={!incoming}> <div class='message flex flex-row mt-3' class:flex-row={incoming} class:flex-row-reverse={!incoming}>
<img src={getPFP(user)} alt='ProfilePicture' class='w-10 h-10 rounded-full p-1 mt-auto' loading='lazy' decoding='async' /> <img src={user.avatar?.medium ?? ''} alt='ProfilePicture' class='w-10 h-10 rounded-full p-1 mt-auto' loading='lazy' decoding='async' />
<div class='flex flex-col px-2 items-start flex-auto' class:items-start={incoming} class:items-end={!incoming}> <div class='flex flex-col px-2 items-start flex-auto' class:items-start={incoming} class:items-end={!incoming}>
<div class='pb-1 flex flex-row items-center px-1'> <div class='pb-1 flex flex-row items-center px-1'>
<div class='font-bold text-sm'> <div class='font-bold text-sm'>
{user.nick} {user.name}
</div> </div>
<div class='text-muted-foreground pl-2 text-[10px] leading-relaxed'> <div class='text-muted-foreground pl-2 text-[10px] leading-relaxed'>
{date.toLocaleTimeString()} {date.toLocaleTimeString()}

View file

@ -1,25 +1,14 @@
<script lang='ts'> <script lang='ts'>
import ExternalLink from 'lucide-svelte/icons/external-link' import ExternalLink from 'lucide-svelte/icons/external-link'
import { getPFP, type ChatUser } from '.' import type { ChatUser } from '.'
import type { Writable } from 'svelte/store'
import native from '$lib/modules/native' import native from '$lib/modules/native'
import { click } from '$lib/modules/navigate' import { click } from '$lib/modules/navigate'
export let users: Writable<Record<string, ChatUser>> export let users: ChatUser[]
function processUsers (users: ChatUser[]) { $: processed = Object.entries(users)
return users.map(user => {
return {
...user,
pfp: getPFP(user)
}
})
}
$: processed = processUsers(Object.values($users))
</script> </script>
<div class='flex flex-col w-72 max-w-full px-5 overflow-hidden'> <div class='flex flex-col w-72 max-w-full px-5 overflow-hidden'>
@ -27,13 +16,13 @@
{processed.length} Member(s) {processed.length} Member(s)
</div> </div>
<div> <div>
{#each processed as { id, pfp, nick } (id)} {#each processed as [key, user] (key)}
<div class='flex items-center pb-2'> <div class='flex items-center pb-2'>
<img src={pfp} alt='ProfilePicture' class='w-10 h-10 rounded-full p-1 mt-auto' loading='lazy' decoding='async' /> <img src={user.avatar?.medium} alt='ProfilePicture' class='w-10 h-10 rounded-full p-1 mt-auto' loading='lazy' decoding='async' />
<div class='text-md pl-2'> <div class='text-md pl-2'>
{nick} {user.name}
</div> </div>
<span class='cursor-pointer flex items-center ml-auto text-blue-600' use:click={() => native.openURL('https://anilist.co/user/' + id)}> <span class='cursor-pointer flex items-center ml-auto text-blue-600' use:click={() => native.openURL('https://anilist.co/user/' + user.id)}>
<ExternalLink size='18' /> <ExternalLink size='18' />
</span> </span>
</div> </div>

View file

@ -1,11 +1,7 @@
export type UserType = 'al' | 'guest' import type { Viewer } from '$lib/modules/anilist/queries'
import type { ResultOf } from 'gql.tada'
export interface ChatUser { export type ChatUser = Omit<NonNullable<ResultOf<typeof Viewer>['Viewer']>, 'id'> & { id: string | number }
nick: string
id: string
pfpid: string
type: UserType
}
export interface ChatMessage { export interface ChatMessage {
message: string message: string
@ -14,13 +10,5 @@ export interface ChatMessage {
date: Date date: Date
} }
export function getPFP (user: ChatUser) {
if (user.type === 'al') {
return `https://s4.anilist.co/file/anilistcdn/user/avatar/medium/b${user.id}-${user.pfpid}`
} else {
return 'https://s4.anilist.co/file/anilistcdn/user/avatar/medium/default.png'
}
}
export { default as UserList } from './UserList.svelte' export { default as UserList } from './UserList.svelte'
export { default as Messages } from './Messages.svelte' export { default as Messages } from './Messages.svelte'

View file

@ -0,0 +1,61 @@
<script lang='ts'>
import SendHorizontal from 'lucide-svelte/icons/send-horizontal'
import { Button } from '../button'
import { Messages, UserList } from '../chat'
import type MessageClient from '$lib/modules/irc'
import { Textarea } from '$lib/components/ui/textarea'
export let client: MessageClient
let message = ''
let rows = 1
function sendMessage () {
if (message.trim()) {
client.say(message.trim())
message = ''
rows = 1
}
}
async function checkInput (e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey && message.trim()) {
e.preventDefault()
sendMessage()
} else {
rows = message.split('\n').length || 1
}
}
function updateRows () {
rows = message.split('\n').length || 1
}
$: users = client.users
$: processedUsers = Object.values($users)
</script>
<div class='flex flex-col w-full relative px-md-4 h-full overflow-hidden'>
<div class='flex md:flex-row flex-col-reverse w-full h-full pt-4'>
<div class='flex flex-col justify-end overflow-hidden flex-grow px-4 md:pb-4'>
<Messages messages={client.messages} />
<div class='flex mt-4'>
<Textarea
bind:value={message}
class='h-auto px-3 w-full flex-grow-1 resize-none min-h-0 border-0 bg-background select:bg-accent select:text-accent-foreground'
{rows}
autocomplete='off'
maxlength={256}
placeholder='Message' on:keydown={checkInput} on:input={updateRows} />
<Button on:click={sendMessage} size='icon' class='mt-auto ml-2 border-0' variant='outline'>
<SendHorizontal size={18} />
</Button>
</div>
</div>
<UserList users={processedUsers} />
</div>
</div>

View file

@ -1,10 +1,6 @@
<script lang='ts' context='module'> <script lang='ts' context='module'>
import SendHorizontal from 'lucide-svelte/icons/send-horizontal'
import { writable, type Writable } from 'simple-store-svelte' import { writable, type Writable } from 'simple-store-svelte'
import { Messages, UserList } from '../chat'
import { Textarea } from '$lib/components/ui/textarea'
import { client } from '$lib/modules/anilist' import { client } from '$lib/modules/anilist'
import MessageClient from '$lib/modules/irc' import MessageClient from '$lib/modules/irc'
@ -12,7 +8,7 @@
</script> </script>
<script lang='ts'> <script lang='ts'>
import { Button } from '../button' import Interface from './interface.svelte'
const viewer = client.viewer.value const viewer = client.viewer.value
@ -28,29 +24,6 @@
} }
irc.value ??= MessageClient.new(ident) irc.value ??= MessageClient.new(ident)
let message = ''
let rows = 1
function sendMessage (client: MessageClient) {
if (message.trim()) {
client.say(message.trim())
message = ''
rows = 1
}
}
async function checkInput (e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey && message.trim()) {
e.preventDefault()
sendMessage(await irc.value!)
} else {
rows = message.split('\n').length || 1
}
}
function updateRows () {
rows = message.split('\n').length || 1
}
</script> </script>
{#if $irc} {#if $irc}
@ -69,26 +42,6 @@
Loading... Loading...
</div> </div>
{:then client} {:then client}
<div class='flex flex-col w-full relative px-md-4 h-full overflow-hidden'> <Interface {client} />
<div class='flex md:flex-row flex-col-reverse w-full h-full pt-4'>
<div class='flex flex-col justify-end overflow-hidden flex-grow px-4 md:pb-4'>
<Messages messages={client.messages} />
<div class='flex mt-4'>
<Textarea
bind:value={message}
class='h-auto px-3 w-full flex-grow-1 resize-none min-h-0 border-0 bg-background select:bg-accent select:text-accent-foreground'
{rows}
autocomplete='off'
maxlength={256}
placeholder='Message' on:keydown={checkInput} on:input={updateRows} />
<Button on:click={() => sendMessage(client)} size='icon' class='mt-auto ml-2 border-0' variant='outline'>
<SendHorizontal size={18} />
</Button>
</div>
</div>
<UserList users={client.users} />
</div>
</div>
<!-- <Chat {client} /> -->
{/await} {/await}
{/if} {/if}

View file

@ -9,6 +9,7 @@
import { fillerEpisodes } from '$lib/components/EpisodesList.svelte' import { fillerEpisodes } from '$lib/components/EpisodesList.svelte'
import { cover, episodes, title } from '$lib/modules/anilist' import { cover, episodes, title } from '$lib/modules/anilist'
import { settings } from '$lib/modules/settings' import { settings } from '$lib/modules/settings'
import { w2globby } from '$lib/modules/w2g/lobby'
export let mediaInfo: NonNullable<Awaited<ReturnType<typeof resolveFilesPoorly>>> export let mediaInfo: NonNullable<Awaited<ReturnType<typeof resolveFilesPoorly>>>
@ -27,6 +28,14 @@
let current = fileToMedaInfo(mediaInfo.target) let current = fileToMedaInfo(mediaInfo.target)
$: $w2globby?.mediaIndexChanged(mediaInfo.resolvedFiles.indexOf(current.file))
$: $w2globby?.on('index', index => {
const file = mediaInfo.resolvedFiles[index]
if (file) {
current = fileToMedaInfo(file)
}
})
function findEpisode (episode: number) { function findEpisode (episode: number) {
return mediaInfo.targetAnimeFiles.find(file => file.metadata.episode === episode) return mediaInfo.targetAnimeFiles.find(file => file.metadata.episode === episode)
} }
@ -70,6 +79,7 @@
function selectFile (file: ResolvedFile) { function selectFile (file: ResolvedFile) {
current = fileToMedaInfo(file) current = fileToMedaInfo(file)
$w2globby?.mediaIndexChanged(mediaInfo.resolvedFiles.indexOf(current.file))
} }
$: next = hasNext(current) $: next = hasNext(current)

View file

@ -57,6 +57,7 @@
import { click } from '$lib/modules/navigate' import { click } from '$lib/modules/navigate'
import { settings } from '$lib/modules/settings' import { settings } from '$lib/modules/settings'
import { server } from '$lib/modules/torrent' import { server } from '$lib/modules/torrent'
import { w2globby } from '$lib/modules/w2g/lobby'
import { toTS, fastPrettyBits } from '$lib/utils' import { toTS, fastPrettyBits } from '$lib/utils'
export let mediaInfo: MediaInfo export let mediaInfo: MediaInfo
@ -652,6 +653,12 @@
return { destroy: () => ctrl.abort() } return { destroy: () => ctrl.abort() }
} }
$: $w2globby?.playerStateChanged({ paused, time: Math.floor(currentTime) })
$: $w2globby?.on('player', state => {
currentTime = state.time
paused = state.paused
})
</script> </script>
<svelte:document bind:fullscreenElement bind:visibilityState use:holdToFF={'key'} /> <svelte:document bind:fullscreenElement bind:visibilityState use:holdToFF={'key'} />

View file

@ -44,8 +44,7 @@
<SidebarButton href='/app/schedule/'> <SidebarButton href='/app/schedule/'>
<Calendar size={18} /> <Calendar size={18} />
</SidebarButton> </SidebarButton>
<!-- <SidebarButton href='/app/w2g/'> --> <SidebarButton href='/app/w2g/'>
<SidebarButton disabled={true}>
<Users size={18} /> <Users size={18} />
</SidebarButton> </SidebarButton>
<SidebarButton href='/app/chat/'> <SidebarButton href='/app/chat/'>
@ -55,7 +54,6 @@
<Download size={18} /> <Download size={18} />
</SidebarButton> </SidebarButton>
<Button variant='ghost' on:click={() => native.openURL('https://github.com/sponsors/ThaUnknown/')} class='px-2 w-full relative mt-auto select:!bg-transparent text-[#fa68b6] select:text-[#fa68b6]'> <Button variant='ghost' on:click={() => native.openURL('https://github.com/sponsors/ThaUnknown/')} class='px-2 w-full relative mt-auto select:!bg-transparent text-[#fa68b6] select:text-[#fa68b6]'>
<!-- <Heart size={18} fill='currentColor' class='absolute' /> -->
<Heart size={18} fill='currentColor' class={cn(active && 'donate')} /> <Heart size={18} fill='currentColor' class={cn(active && 'donate')} />
</Button> </Button>
<SidebarButton href='/app/settings/'> <SidebarButton href='/app/settings/'>

View file

@ -1,5 +1,3 @@
import { EventEmitter } from 'events'
import Client, { createChannelConstructor } from '@thaunknown/web-irc' import Client, { createChannelConstructor } from '@thaunknown/web-irc'
import { writable } from 'simple-store-svelte' import { writable } from 'simple-store-svelte'
@ -7,6 +5,23 @@ import { decryptMessage, encryptMessage } from './crypt'
import type { ChatMessage, ChatUser } from '$lib/components/ui/chat' import type { ChatMessage, ChatUser } from '$lib/components/ui/chat'
import type IrcChannel from '@thaunknown/web-irc/channel' import type IrcChannel from '@thaunknown/web-irc/channel'
import type IrcClient from '@thaunknown/web-irc/client'
import type { EventEmitter } from 'events'
export type UserType = 'al' | 'guest'
export interface IRCChatUser {
nick: string
id: string
pfpid: string
type: UserType
}
export function getPFP (user: Pick<IRCChatUser, 'id' | 'pfpid' | 'type'>) {
if (user.type === 'al') {
return `https://s4.anilist.co/file/anilistcdn/user/avatar/medium/b${user.id}-${user.pfpid}`
} else {
return 'https://s4.anilist.co/file/anilistcdn/user/avatar/medium/default.png'
}
}
export interface IRCUser { nick: string, ident: string, hostname: string, modes: string[], tags: object } export interface IRCUser { nick: string, ident: string, hostname: string, modes: string[], tags: object }
export interface PrivMessage { export interface PrivMessage {
@ -24,30 +39,48 @@ export interface PrivMessage {
time: number time: number
} }
export default class MessageClient extends EventEmitter { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
irc = new Client(null) type IRCEvents = {
userlist: [{ users: IRCUser[] }]
join: [IRCUser]
part: [IRCUser]
quit: [IRCUser]
kick: [IRCUser]
privmsg: [PrivMessage]
connected: []
}
function ircUserToChatUser ({ id, pfpid, type, nick }: IRCChatUser): ChatUser {
return { id, avatar: { medium: getPFP({ id, pfpid, type }) }, name: nick, mediaListOptions: null }
}
function ircIdentToChatUser (user: IRCUser): ChatUser {
const [nick, pfpid, pfpex] = user.nick.split('_') as [string, string, string]
const [type, id] = user.ident.split('_') as ['al' | 'guest', string]
return ircUserToChatUser({ id, pfpid: `${pfpid}.${pfpex}`, type, nick })
}
export default class MessageClient {
irc = new Client(null) as IrcClient & EventEmitter<IRCEvents>
users = writable<Record<string, ChatUser>>({}) users = writable<Record<string, ChatUser>>({})
messages = writable<ChatMessage[]>([]) messages = writable<ChatMessage[]>([])
channel?: IrcChannel channel?: IrcChannel
ident ident
constructor (ident: ChatUser) { constructor (ident: IRCChatUser) {
super()
this.ident = ident this.ident = ident
this.irc.on('userlist', async ({ users }: { users: IRCUser[] }) => { this.irc.on('userlist', async ({ users }) => {
this.users.value = users.reduce((acc, user) => { this.users.value = users.reduce((acc, ircuser) => {
const [nick, pfpid, pfpex] = user.nick.split('_') as [string, string, string] const user = ircIdentToChatUser(ircuser)
const [type, id] = user.ident.split('_') as ['al' | 'guest', string] acc[ircuser.ident] = user
acc[user.ident] = { nick, id, pfpid: `${pfpid}.${pfpex}`, type }
return acc return acc
}, this.users.value) }, this.users.value)
}) })
this.irc.on('join', async (user: IRCUser) => { this.irc.on('join', async ircuser => {
try { try {
const [nick, pfpid, pfpex] = user.nick.split('_') as [string, string, string] const user = ircIdentToChatUser(ircuser)
const [type, id] = user.ident.split('_') as ['al' | 'guest', string] this.users.value[ircuser.ident] = user
this.users.value[user.ident] = { nick, id, pfpid: `${pfpid}.${pfpex}`, type }
this.users.update(users => users) this.users.update(users => users)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -63,9 +96,9 @@ export default class MessageClient extends EventEmitter {
this.irc.on('part', deleteUser) this.irc.on('part', deleteUser)
this.irc.on('kick', deleteUser) this.irc.on('kick', deleteUser)
this.irc.on('privmsg', async (priv: PrivMessage) => { this.irc.on('privmsg', async priv => {
const message = await decryptMessage(priv.message)
try { try {
const message = await decryptMessage(priv.message)
this.messages.update(messages => [...messages, { this.messages.update(messages => [...messages, {
message, message,
user: this.users.value[priv.ident]!, user: this.users.value[priv.ident]!,
@ -83,17 +116,17 @@ export default class MessageClient extends EventEmitter {
const encrypted = await encryptMessage(message) const encrypted = await encryptMessage(message)
this.channel!.say(encrypted) this.channel!.say(encrypted)
this.messages.update(messages => [...messages, { this.messages.update(messages => [...messages, {
user: this.ident, user: ircUserToChatUser(this.ident),
message, message,
date: new Date(), date: new Date(),
type: 'outgoing' type: 'outgoing'
}]) }])
} }
static async new ({ nick, id, pfpid, type }: ChatUser) { static async new ({ nick, id, pfpid, type }: IRCChatUser) {
const client = new this({ nick, id, pfpid, type }) const client = new this({ nick, id, pfpid, type })
await new Promise(resolve => { await new Promise<void>(resolve => {
client.irc.once('connected', resolve) client.irc.once('connected', resolve)
client.irc.connect({ client.irc.connect({
version: null, version: null,

View file

@ -3,6 +3,7 @@ import { get } from 'svelte/store'
import { persisted } from 'svelte-persisted-store' import { persisted } from 'svelte-persisted-store'
import native from '../native' import native from '../native'
import { w2globby } from '../w2g/lobby'
import type { TorrentFile, TorrentInfo } from '../../../app' import type { TorrentFile, TorrentInfo } from '../../../app'
import type { Media } from '../anilist' import type { Media } from '../anilist'
@ -53,6 +54,7 @@ export const server = new class ServerClient {
play (id: string, media: Media, episode: number) { play (id: string, media: Media, episode: number) {
this.last.set({ id, media, episode }) this.last.set({ id, media, episode })
this.active.value = this._play(id, media, episode) this.active.value = this._play(id, media, episode)
w2globby.value?.mediaChange({ episode, mediaId: media.id, torrent: id })
return this.active.value return this.active.value
} }

View file

@ -1,16 +1,23 @@
export default class Event<T = unknown> { import type { ChatUser } from '$lib/components/ui/chat'
payload
type = '' export default class Event<K extends keyof W2GEvents = keyof W2GEvents> {
constructor (type: string, payload: T) { readonly type: K
readonly payload: W2GEvents[K]
constructor (type: K, payload: W2GEvents[K]) {
this.type = type this.type = type
this.payload = payload this.payload = payload
} }
} }
export const EventTypes = { export interface PlayerState { paused: boolean, time: number }
SessionInitEvent: 'init',
MagnetLinkEvent: 'magnet', export interface MediaState { torrent: string, mediaId: number, episode: number }
MediaIndexEvent: 'index',
PlayerStateEvent: 'player', export interface W2GEvents {
MessageEvent: 'message' init: ChatUser
media?: MediaState
index?: number
player: PlayerState
message: string
} }

View file

@ -5,15 +5,15 @@ import P2PT, { type Peer } from 'p2pt'
import { writable } from 'simple-store-svelte' import { writable } from 'simple-store-svelte'
import client from '../anilist/client.js' import client from '../anilist/client.js'
import { server } from '../torrent'
import Event, { EventTypes } from './events.js' import Event, { type MediaState, type PlayerState, type W2GEvents } from './events.js'
import type { Viewer } from '../anilist/queries' import type { ChatMessage, ChatUser } from '$lib/components/ui/chat'
import type { ResultOf } from 'gql.tada'
const debug = Debug('ui:w2g') const debug = Debug('ui:w2g')
function generateRandomHexCode (len: number) { export function generateRandomHexCode (len: number) {
let hexCode = '' let hexCode = ''
while (hexCode.length < len) { while (hexCode.length < len) {
@ -23,73 +23,77 @@ function generateRandomHexCode (len: number) {
return hexCode return hexCode
} }
type PeerList = Record<string, { user: ResultOf<typeof Viewer>['Viewer'] | {id: string }, peer?: Peer }> type AppEvent = {
[K in keyof W2GEvents]-?: Event<K>
}[keyof W2GEvents]
interface PlayerState {paused: boolean, time: number} type PeerList = Record<string, { user: ChatUser, peer?: Peer }>
export class W2GClient extends EventEmitter { const ANNOUNCE = [
static readonly #announce = [ atob('d3NzOi8vdHJhY2tlci5vcGVud2VidG9ycmVudC5jb20='),
atob('d3NzOi8vdHJhY2tlci5vcGVud2VidG9ycmVudC5jb20='), atob('d3NzOi8vdHJhY2tlci53ZWJ0b3JyZW50LmRldg=='),
atob('d3NzOi8vdHJhY2tlci53ZWJ0b3JyZW50LmRldg=='), atob('d3NzOi8vdHJhY2tlci5maWxlcy5mbTo3MDczL2Fubm91bmNl'),
atob('d3NzOi8vdHJhY2tlci5maWxlcy5mbTo3MDczL2Fubm91bmNl'), atob('d3NzOi8vdHJhY2tlci5idG9ycmVudC54eXov')
atob('d3NzOi8vdHJhY2tlci5idG9ycmVudC54eXov') ]
]
export class W2GClient extends EventEmitter<{index: [number], player: [PlayerState]}> {
player: PlayerState = { player: PlayerState = {
paused: true, paused: true,
time: 0 time: 0
} }
index = 0 index = 0
magnet: {magnet: string, hash: string} | null = null media: MediaState | undefined
isHost = false isHost
#p2pt: P2PT | null readonly #p2pt
code code
messages = writable<Array<{message: string, user: ResultOf<typeof Viewer>['Viewer'] | {id: string }, type: 'incoming' | 'outgoing', date: Date}>>([]) messages = writable<ChatMessage[]>([])
self = client.viewer.value?.viewer ?? { id: generateRandomHexCode(16) } self: ChatUser = client.viewer.value?.viewer ?? { id: generateRandomHexCode(16), avatar: null, mediaListOptions: null, name: 'Guest' }
/** @type {import('simple-store-svelte').Writable<PeerList>} */
peers = writable<PeerList>({ [this.self.id]: { user: this.self } }) peers = writable<PeerList>({ [this.self.id]: { user: this.self } })
get inviteLink () { get inviteLink () {
return `https://miru.watch/w2g/${this.code}` return `https://hayas.ee/w2g/${this.code}`
} }
localMediaIndexChanged (index: number) { constructor (code: string, isHost: boolean) {
this.index = index
this.mediaIndexChanged(index)
}
localPlayerStateChanged ({ payload }: Event<PlayerState>) {
debug(`localPlayerStateChanged: ${JSON.stringify(payload)}`)
this.player.paused = payload.paused
this.player.time = payload.time
this.playerStateChanged(this.player)
}
constructor (code?: string) {
super() super()
this.isHost = !code this.isHost = isHost
this.code = code ?? generateRandomHexCode(16) this.code = code
debug(`W2GClient: ${this.code}, ${this.isHost}`) debug(`W2GClient: ${this.code}, ${this.isHost}`)
this.#p2pt = new P2PT(W2GClient.#announce, this.code) this.#p2pt = new P2PT<Event>(ANNOUNCE, this.code)
this.#p2pt.on('peerconnect', peer => {
debug(`peerconnect: ${peer.id}`)
this._sendEvent(peer, new Event('init', this.self))
if (this.isHost) this._sendInitialSessionState(peer)
})
this.#p2pt.on('peerclose', peer => {
debug(`peerclose: ${peer.id}`)
this.peers.update(peers => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete peers[peer.id]
return peers
})
})
this.#p2pt.on('msg', this._onMsg)
this.#wireEvents()
this.#p2pt.start() this.#p2pt.start()
} }
magnetLink (magnet: { hash: string, magnet: string }) { mediaChange (media: MediaState) {
debug(`magnetLink: ${this.magnet?.hash} ${magnet.hash}`) debug(`mediaChange: ${this.media?.torrent} ${media.torrent}`)
if (this.magnet?.hash !== magnet.hash) { if (this.media?.torrent !== media.torrent) {
this.magnet = magnet this.media = media
this.isHost = true this.isHost = true
this.#sendToPeers(new Event('magnet', magnet)) this._sendToPeers(new Event('media', media))
} }
} }
@ -97,7 +101,7 @@ export class W2GClient extends EventEmitter {
debug(`mediaIndexChanged: ${this.index} ${index}`) debug(`mediaIndexChanged: ${this.index} ${index}`)
if (this.index !== index) { if (this.index !== index) {
this.index = index this.index = index
this.#sendToPeers(new Event('index', index)) this._sendToPeers(new Event('index', index))
} }
} }
@ -112,7 +116,7 @@ export class W2GClient extends EventEmitter {
playerStateChanged (state: PlayerState) { playerStateChanged (state: PlayerState) {
debug(`playerStateChanged: ${JSON.stringify(state)}`) debug(`playerStateChanged: ${JSON.stringify(state)}`)
if (this._playerStateChanged(state)) this.#sendToPeers(new Event('player', state)) if (this._playerStateChanged(state)) this._sendToPeers(new Event('player', state))
} }
message (message: string) { message (message: string) {
@ -123,77 +127,61 @@ export class W2GClient extends EventEmitter {
type: 'outgoing', type: 'outgoing',
date: new Date() date: new Date()
})]) })])
this.#sendToPeers(new Event('message', message)) this._sendToPeers(new Event('message', message))
} }
#wireEvents () { _sendEvent (peer: Peer, event: Event) {
this.#p2pt?.on('peerconnect', this.#onPeerconnect.bind(this)) debug(`sendEvent: ${peer.id} ${JSON.stringify(event)}`)
this.#p2pt?.on('msg', this.#onMsg.bind(this)) this.#p2pt.send(peer, event)
this.#p2pt?.on('peerclose', this.#onPeerclose.bind(this))
} }
#sendEvent (peer: Peer, event: Event) { _sendInitialSessionState (peer: Peer) {
debug(`#sendEvent: ${peer.id} ${JSON.stringify(event)}`) this._sendEvent(peer, new Event('media', this.media))
this.#p2pt?.send(peer, JSON.stringify(event)) this._sendEvent(peer, new Event('index', this.index))
this._sendEvent(peer, new Event('player', this.player))
} }
#sendInitialSessionState (peer: Peer) { _onMsg = async (peer: Peer, data: AppEvent) => {
this.#sendEvent(peer, new Event('magnet', this.magnet)) debug(`onMsg: ${peer.id} ${JSON.stringify(data)}`)
this.#sendEvent(peer, new Event('index', this.index))
this.#sendEvent(peer, new Event('player', this.player))
}
async #onPeerconnect (peer: Peer) {
debug(`#onPeerconnect: ${peer.id}`)
this.#sendEvent(peer, new Event('init', this.self))
if (this.isHost) this.#sendInitialSessionState(peer)
}
#onMsg (peer: Peer, data: Event<PlayerState | {magnet: string, hash: string} | string | ResultOf<typeof Viewer>['Viewer'] | {index: number}> | string) {
debug(`#onMsg: ${peer.id} ${JSON.stringify(data)}`)
data = typeof data === 'string' ? JSON.parse(data) as Event<PlayerState | {magnet: string, hash: string} | string | ResultOf<typeof Viewer>['Viewer'] | {index: number}> : data
switch (data.type) { switch (data.type) {
case EventTypes.SessionInitEvent: case 'init':
this.peers.update(peers => { this.peers.update(peers => {
peers[peer.id] = { peers[peer.id] = {
peer, peer,
user: data.payload as ResultOf<typeof Viewer>['Viewer'] user: data.payload
} }
return peers return peers
}) })
break break
case EventTypes.MagnetLinkEvent: { case 'media': {
const cast = data as Event<{magnet: string, hash: string}> if (data.payload?.torrent == null || data.payload?.mediaId == null) break
if (cast.payload.magnet === undefined) break const { torrent, mediaId, episode } = data.payload
const { hash, magnet } = cast.payload if (torrent !== this.media?.torrent) {
if (hash !== this.magnet?.hash) {
this.isHost = false this.isHost = false
this.magnet = cast.payload this.media = data.payload
add(magnet) const media = (await client.single(mediaId)).data?.Media
if (media == null) break
server.play(torrent, media, episode)
} }
break break
} }
case EventTypes.MediaIndexEvent: { case 'index': {
const cast = data as Event<{index: number}> if (data.payload == null) break
if (cast.payload.index === undefined) break if (this.index !== data.payload) {
if (this.index !== cast.payload.index) { this.index = data.payload
this.index = cast.payload.index this.emit('index', data.payload)
this.emit('index', cast.payload.index)
} }
break break
} }
case EventTypes.PlayerStateEvent: { case 'player': {
const cast = data as Event<PlayerState> if (data.payload?.time == null) break
if (cast.payload.time === undefined) break if (this._playerStateChanged(data.payload)) this.emit('player', data.payload)
if (this._playerStateChanged(cast.payload)) this.emit('player', data.payload)
break break
} }
case EventTypes.MessageEvent:{ case 'message': {
const cast = data as Event<string> this.messages.update(messages => [...messages, ({ message: data.payload, user: this.peers.value[peer.id]!.user, type: 'incoming', date: new Date() })])
this.messages.update(messages => [...messages, ({ message: cast.payload, user: this.peers.value[peer.id].user, type: 'incoming', date: new Date() })])
break break
} }
default: default:
@ -201,27 +189,16 @@ export class W2GClient extends EventEmitter {
} }
} }
#onPeerclose (peer: Peer) { _sendToPeers (event: Event) {
debug(`#onPeerclose: ${peer.id}`)
this.peers.update(peers => {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete peers[peer.id]
return peers
})
}
#sendToPeers (event: Event) {
if (!this.#p2pt) return
for (const { peer } of Object.values(this.peers.value)) { for (const { peer } of Object.values(this.peers.value)) {
if (peer) this.#sendEvent(peer, event) if (peer) this._sendEvent(peer, event)
} }
} }
destroy () { destroy () {
debug('destroy') debug('destroy')
this.#p2pt?.destroy() this.#p2pt.destroy()
this.removeAllListeners() this.removeAllListeners()
this.#p2pt = null
this.isHost = false this.isHost = false
this.peers.value = {} this.peers.value = {}
} }

View file

@ -0,0 +1,5 @@
import { writable } from 'simple-store-svelte'
import type { W2GClient } from '.'
export const w2globby = writable<W2GClient | undefined>()

View file

@ -2,6 +2,7 @@
import FileImage from 'lucide-svelte/icons/file-image' import FileImage from 'lucide-svelte/icons/file-image'
import Trash from 'lucide-svelte/icons/trash' import Trash from 'lucide-svelte/icons/trash'
import X from 'lucide-svelte/icons/x' import X from 'lucide-svelte/icons/x'
import { tick } from 'svelte'
import MagnifyingGlass from 'svelte-radix/MagnifyingGlass.svelte' import MagnifyingGlass from 'svelte-radix/MagnifyingGlass.svelte'
import { toast } from 'svelte-sonner' import { toast } from 'svelte-sonner'
@ -10,6 +11,7 @@
import type { Search } from '$lib/modules/anilist/queries' import type { Search } from '$lib/modules/anilist/queries'
import type { VariablesOf } from 'gql.tada' import type { VariablesOf } from 'gql.tada'
import { replaceState } from '$app/navigation'
import { page } from '$app/stores' import { page } from '$app/stores'
import { badgeVariants } from '$lib/components/ui/badge' import { badgeVariants } from '$lib/components/ui/badge'
import { Button } from '$lib/components/ui/button' import { Button } from '$lib/components/ui/button'
@ -93,6 +95,8 @@
onList: [] as format[] onList: [] as format[]
} }
$: console.log('search updated', search)
let pageNumber = 1 let pageNumber = 1
let inputText = '' let inputText = ''
@ -123,7 +127,7 @@
} }
function searchQuery (filter: Partial<typeof search>, page: number) { function searchQuery (filter: Partial<typeof search>, page: number) {
return client.search({ const search = {
page, page,
ids: filter.ids, ids: filter.ids,
search: filter.name, search: filter.name,
@ -134,7 +138,11 @@
format: filter.formats?.map(f => f.value) as Array<'MUSIC' | 'MANGA' | 'TV' | 'TV_SHORT' | 'MOVIE' | 'SPECIAL' | 'OVA' | 'ONA' | 'NOVEL' | 'ONE_SHOT'>, format: filter.formats?.map(f => f.value) as Array<'MUSIC' | 'MANGA' | 'TV' | 'TV_SHORT' | 'MOVIE' | 'SPECIAL' | 'OVA' | 'ONA' | 'NOVEL' | 'ONE_SHOT'>,
status: filter.status?.map(s => s.value) as Array<'FINISHED' | 'RELEASING' | 'NOT_YET_RELEASED' | 'CANCELLED' | 'HIATUS' | null>, status: filter.status?.map(s => s.value) as Array<'FINISHED' | 'RELEASING' | 'NOT_YET_RELEASED' | 'CANCELLED' | 'HIATUS' | null>,
sort: [filter.sort?.[0]?.value ?? 'SEARCH_MATCH'] as Array<'TITLE_ROMAJI_DESC' | 'ID' | 'START_DATE_DESC' | 'SCORE_DESC' | 'POPULARITY_DESC' | 'TRENDING_DESC' | 'UPDATED_AT_DESC' | 'ID_DESC' | 'TITLE_ROMAJI' | 'TITLE_ENGLISH' | 'TITLE_ENGLISH_DESC' | null> sort: [filter.sort?.[0]?.value ?? 'SEARCH_MATCH'] as Array<'TITLE_ROMAJI_DESC' | 'ID' | 'START_DATE_DESC' | 'SCORE_DESC' | 'POPULARITY_DESC' | 'TRENDING_DESC' | 'UPDATED_AT_DESC' | 'ID_DESC' | 'TITLE_ROMAJI' | 'TITLE_ENGLISH' | 'TITLE_ENGLISH_DESC' | null>
}) }
tick().then(() => replaceState('', { search }))
return client.search(search)
} }
const updateText = debounce((e: FormInputEvent) => { const updateText = debounce((e: FormInputEvent) => {

View file

@ -0,0 +1,10 @@
import { redirect } from '@sveltejs/kit'
import { generateRandomHexCode, W2GClient } from '$lib/modules/w2g'
import { w2globby } from '$lib/modules/w2g/lobby'
export function load () {
w2globby.value ??= new W2GClient(generateRandomHexCode(16), true)
redirect(302, '/app/w2g/' + w2globby.value.code)
}

View file

@ -0,0 +1,60 @@
<script lang='ts' context='module'>
import SendHorizontal from 'lucide-svelte/icons/send-horizontal'
import { Button } from '$lib/components/ui/button'
import { Messages, UserList } from '$lib/components/ui/chat'
import { Textarea } from '$lib/components/ui/textarea'
import { W2GClient } from '$lib/modules/w2g'
</script>
<script lang='ts'>
import { w2globby } from '$lib/modules/w2g/lobby'
export let data
$w2globby ??= new W2GClient(data.id, false)
$: users = $w2globby!.peers
$: messages = $w2globby!.messages
let message = ''
let rows = 1
function sendMessage () {
$w2globby?.message(message.trim())
}
async function checkInput (e: KeyboardEvent) {
if (e.key === 'Enter' && !e.shiftKey && message.trim()) {
sendMessage()
} else {
rows = message.split('\n').length || 1
}
}
function updateRows () {
rows = message.split('\n').length || 1
}
$: prcoessedUsers = Object.values($users).map(({ user }) => user)
</script>
<div class='flex flex-col w-full relative px-md-4 h-full overflow-hidden'>
<div class='flex md:flex-row flex-col-reverse w-full h-full pt-4'>
<div class='flex flex-col justify-end overflow-hidden flex-grow px-4 md:pb-4'>
<Messages {messages} />
<div class='flex mt-4'>
<Textarea
bind:value={message}
class='h-auto px-3 w-full flex-grow-1 resize-none min-h-0 border-0 bg-background select:bg-accent select:text-accent-foreground'
{rows}
autocomplete='off'
maxlength={256}
placeholder='Message' on:keydown={checkInput} on:input={updateRows} />
<Button on:click={sendMessage} size='icon' class='mt-auto ml-2 border-0' variant='outline'>
<SendHorizontal size={18} />
</Button>
</div>
</div>
<UserList users={prcoessedUsers} />
</div>
</div>

View file

@ -0,0 +1,5 @@
import type { PageLoad } from './$types'
export const load: PageLoad = ({ params }) => {
return params
}

View file

@ -25,6 +25,8 @@ export default defineConfig({
'./Scripts': resolve(__dirname, 'src/patches/empty.cjs'), './Scripts': resolve(__dirname, 'src/patches/empty.cjs'),
// yeah they dont export this for making custom icons, sucks // yeah they dont export this for making custom icons, sucks
'lucide-svelte/dist/Icon.svelte': resolve(__dirname, 'node_modules/lucide-svelte/dist/Icon.svelte'), 'lucide-svelte/dist/Icon.svelte': resolve(__dirname, 'node_modules/lucide-svelte/dist/Icon.svelte'),
// no exports :/
'bittorrent-tracker/lib/client/websocket-tracker.js': resolve(__dirname, 'node_modules/bittorrent-tracker/lib/client/websocket-tracker.js'),
} }
}, },
server: { port: 7344 }, server: { port: 7344 },