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": {
"source.fixAll.eslint": "always"
},
"editor.formatOnSave": true,
"extensions.ignoreRecommendations": false,
"eslint.useESLintClass": true,
"eslint.useFlatConfig": true,
"editor.linkedEditing": true,
"editor.tabSize": 2,
"eslint.format.enable": true,
"eslint.probe": [
"javascript",
@ -17,6 +14,8 @@
"svelte",
"html"
],
"eslint.useESLintClass": true,
"eslint.useFlatConfig": true,
"eslint.validate": [
"javascript",
"javascriptreact",
@ -24,5 +23,21 @@
"typescriptreact",
"svelte",
"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",
"version": "6.3.16",
"version": "6.3.17",
"license": "BUSL-1.1",
"private": true,
"packageManager": "pnpm@9.14.4",
@ -36,6 +36,7 @@
"svelte-radix": "^1.1.1",
"svelte-sonner": "^0.3.28",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"vaul-svelte": "^0.3.2",
"vite": "^5.4.11"
},
@ -63,7 +64,7 @@
"js-levenshtein": "^1.1.6",
"lucide-svelte": "^0.511.0",
"marked": "^15.0.11",
"p2pt": "^1.5.1",
"p2pt": "github:ThaUnknown/p2pt#modernise",
"rollup-plugin-license": "^3.6.0",
"semver": "^7.7.2",
"simple-store-svelte": "^1.0.6",

View file

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

6
src/app.d.ts vendored
View file

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

View file

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

View file

@ -1,6 +1,5 @@
<script lang='ts'>
import { getPFP, type ChatMessage } from '.'
import type { ChatMessage } from '.'
import type { Writable } from 'simple-store-svelte'
export let messages: Writable<ChatMessage[]>
@ -9,7 +8,7 @@
if (!messages.length) return []
const grouped = []
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) {
last.messages.push(message)
} else {
@ -23,11 +22,11 @@
{#each groupMessages($messages) as { type, user, date, messages }, i (i)}
{@const incoming = type === '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='pb-1 flex flex-row items-center px-1'>
<div class='font-bold text-sm'>
{user.nick}
{user.name}
</div>
<div class='text-muted-foreground pl-2 text-[10px] leading-relaxed'>
{date.toLocaleTimeString()}

View file

@ -1,25 +1,14 @@
<script lang='ts'>
import ExternalLink from 'lucide-svelte/icons/external-link'
import { getPFP, type ChatUser } from '.'
import type { Writable } from 'svelte/store'
import type { ChatUser } from '.'
import native from '$lib/modules/native'
import { click } from '$lib/modules/navigate'
export let users: Writable<Record<string, ChatUser>>
export let users: ChatUser[]
function processUsers (users: ChatUser[]) {
return users.map(user => {
return {
...user,
pfp: getPFP(user)
}
})
}
$: processed = processUsers(Object.values($users))
$: processed = Object.entries(users)
</script>
<div class='flex flex-col w-72 max-w-full px-5 overflow-hidden'>
@ -27,13 +16,13 @@
{processed.length} Member(s)
</div>
<div>
{#each processed as { id, pfp, nick } (id)}
{#each processed as [key, user] (key)}
<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'>
{nick}
{user.name}
</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' />
</span>
</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 {
nick: string
id: string
pfpid: string
type: UserType
}
export type ChatUser = Omit<NonNullable<ResultOf<typeof Viewer>['Viewer']>, 'id'> & { id: string | number }
export interface ChatMessage {
message: string
@ -14,13 +10,5 @@ export interface ChatMessage {
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 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'>
import SendHorizontal from 'lucide-svelte/icons/send-horizontal'
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 MessageClient from '$lib/modules/irc'
@ -12,7 +8,7 @@
</script>
<script lang='ts'>
import { Button } from '../button'
import Interface from './interface.svelte'
const viewer = client.viewer.value
@ -28,29 +24,6 @@
}
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>
{#if $irc}
@ -69,26 +42,6 @@
Loading...
</div>
{:then client}
<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(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} /> -->
<Interface {client} />
{/await}
{/if}

View file

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

View file

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

View file

@ -44,8 +44,7 @@
<SidebarButton href='/app/schedule/'>
<Calendar size={18} />
</SidebarButton>
<!-- <SidebarButton href='/app/w2g/'> -->
<SidebarButton disabled={true}>
<SidebarButton href='/app/w2g/'>
<Users size={18} />
</SidebarButton>
<SidebarButton href='/app/chat/'>
@ -55,7 +54,6 @@
<Download size={18} />
</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]'>
<!-- <Heart size={18} fill='currentColor' class='absolute' /> -->
<Heart size={18} fill='currentColor' class={cn(active && 'donate')} />
</Button>
<SidebarButton href='/app/settings/'>

View file

@ -1,5 +1,3 @@
import { EventEmitter } from 'events'
import Client, { createChannelConstructor } from '@thaunknown/web-irc'
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 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 PrivMessage {
@ -24,30 +39,48 @@ export interface PrivMessage {
time: number
}
export default class MessageClient extends EventEmitter {
irc = new Client(null)
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
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>>({})
messages = writable<ChatMessage[]>([])
channel?: IrcChannel
ident
constructor (ident: ChatUser) {
super()
constructor (ident: IRCChatUser) {
this.ident = ident
this.irc.on('userlist', async ({ users }: { users: IRCUser[] }) => {
this.users.value = users.reduce((acc, user) => {
const [nick, pfpid, pfpex] = user.nick.split('_') as [string, string, string]
const [type, id] = user.ident.split('_') as ['al' | 'guest', string]
acc[user.ident] = { nick, id, pfpid: `${pfpid}.${pfpex}`, type }
this.irc.on('userlist', async ({ users }) => {
this.users.value = users.reduce((acc, ircuser) => {
const user = ircIdentToChatUser(ircuser)
acc[ircuser.ident] = user
return acc
}, this.users.value)
})
this.irc.on('join', async (user: IRCUser) => {
this.irc.on('join', async ircuser => {
try {
const [nick, pfpid, pfpex] = user.nick.split('_') as [string, string, string]
const [type, id] = user.ident.split('_') as ['al' | 'guest', string]
this.users.value[user.ident] = { nick, id, pfpid: `${pfpid}.${pfpex}`, type }
const user = ircIdentToChatUser(ircuser)
this.users.value[ircuser.ident] = user
this.users.update(users => users)
} catch (error) {
console.error(error)
@ -63,9 +96,9 @@ export default class MessageClient extends EventEmitter {
this.irc.on('part', deleteUser)
this.irc.on('kick', deleteUser)
this.irc.on('privmsg', async (priv: PrivMessage) => {
const message = await decryptMessage(priv.message)
this.irc.on('privmsg', async priv => {
try {
const message = await decryptMessage(priv.message)
this.messages.update(messages => [...messages, {
message,
user: this.users.value[priv.ident]!,
@ -83,17 +116,17 @@ export default class MessageClient extends EventEmitter {
const encrypted = await encryptMessage(message)
this.channel!.say(encrypted)
this.messages.update(messages => [...messages, {
user: this.ident,
user: ircUserToChatUser(this.ident),
message,
date: new Date(),
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 })
await new Promise(resolve => {
await new Promise<void>(resolve => {
client.irc.once('connected', resolve)
client.irc.connect({
version: null,

View file

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

View file

@ -1,16 +1,23 @@
export default class Event<T = unknown> {
payload
type = ''
constructor (type: string, payload: T) {
import type { ChatUser } from '$lib/components/ui/chat'
export default class Event<K extends keyof W2GEvents = keyof W2GEvents> {
readonly type: K
readonly payload: W2GEvents[K]
constructor (type: K, payload: W2GEvents[K]) {
this.type = type
this.payload = payload
}
}
export const EventTypes = {
SessionInitEvent: 'init',
MagnetLinkEvent: 'magnet',
MediaIndexEvent: 'index',
PlayerStateEvent: 'player',
MessageEvent: 'message'
export interface PlayerState { paused: boolean, time: number }
export interface MediaState { torrent: string, mediaId: number, episode: number }
export interface W2GEvents {
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 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 { ResultOf } from 'gql.tada'
import type { ChatMessage, ChatUser } from '$lib/components/ui/chat'
const debug = Debug('ui:w2g')
function generateRandomHexCode (len: number) {
export function generateRandomHexCode (len: number) {
let hexCode = ''
while (hexCode.length < len) {
@ -23,73 +23,77 @@ function generateRandomHexCode (len: number) {
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 {
static readonly #announce = [
atob('d3NzOi8vdHJhY2tlci5vcGVud2VidG9ycmVudC5jb20='),
atob('d3NzOi8vdHJhY2tlci53ZWJ0b3JyZW50LmRldg=='),
atob('d3NzOi8vdHJhY2tlci5maWxlcy5mbTo3MDczL2Fubm91bmNl'),
atob('d3NzOi8vdHJhY2tlci5idG9ycmVudC54eXov')
]
const ANNOUNCE = [
atob('d3NzOi8vdHJhY2tlci5vcGVud2VidG9ycmVudC5jb20='),
atob('d3NzOi8vdHJhY2tlci53ZWJ0b3JyZW50LmRldg=='),
atob('d3NzOi8vdHJhY2tlci5maWxlcy5mbTo3MDczL2Fubm91bmNl'),
atob('d3NzOi8vdHJhY2tlci5idG9ycmVudC54eXov')
]
export class W2GClient extends EventEmitter<{index: [number], player: [PlayerState]}> {
player: PlayerState = {
paused: true,
time: 0
}
index = 0
magnet: {magnet: string, hash: string} | null = null
isHost = false
#p2pt: P2PT | null
media: MediaState | undefined
isHost
readonly #p2pt
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) }
/** @type {import('simple-store-svelte').Writable<PeerList>} */
self: ChatUser = client.viewer.value?.viewer ?? { id: generateRandomHexCode(16), avatar: null, mediaListOptions: null, name: 'Guest' }
peers = writable<PeerList>({ [this.self.id]: { user: this.self } })
get inviteLink () {
return `https://miru.watch/w2g/${this.code}`
return `https://hayas.ee/w2g/${this.code}`
}
localMediaIndexChanged (index: number) {
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) {
constructor (code: string, isHost: boolean) {
super()
this.isHost = !code
this.isHost = isHost
this.code = code ?? generateRandomHexCode(16)
this.code = code
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()
}
magnetLink (magnet: { hash: string, magnet: string }) {
debug(`magnetLink: ${this.magnet?.hash} ${magnet.hash}`)
if (this.magnet?.hash !== magnet.hash) {
this.magnet = magnet
mediaChange (media: MediaState) {
debug(`mediaChange: ${this.media?.torrent} ${media.torrent}`)
if (this.media?.torrent !== media.torrent) {
this.media = media
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}`)
if (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) {
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) {
@ -123,77 +127,61 @@ export class W2GClient extends EventEmitter {
type: 'outgoing',
date: new Date()
})])
this.#sendToPeers(new Event('message', message))
this._sendToPeers(new Event('message', message))
}
#wireEvents () {
this.#p2pt?.on('peerconnect', this.#onPeerconnect.bind(this))
this.#p2pt?.on('msg', this.#onMsg.bind(this))
this.#p2pt?.on('peerclose', this.#onPeerclose.bind(this))
_sendEvent (peer: Peer, event: Event) {
debug(`sendEvent: ${peer.id} ${JSON.stringify(event)}`)
this.#p2pt.send(peer, event)
}
#sendEvent (peer: Peer, event: Event) {
debug(`#sendEvent: ${peer.id} ${JSON.stringify(event)}`)
this.#p2pt?.send(peer, JSON.stringify(event))
_sendInitialSessionState (peer: Peer) {
this._sendEvent(peer, new Event('media', this.media))
this._sendEvent(peer, new Event('index', this.index))
this._sendEvent(peer, new Event('player', this.player))
}
#sendInitialSessionState (peer: Peer) {
this.#sendEvent(peer, new Event('magnet', this.magnet))
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
_onMsg = async (peer: Peer, data: AppEvent) => {
debug(`onMsg: ${peer.id} ${JSON.stringify(data)}`)
switch (data.type) {
case EventTypes.SessionInitEvent:
case 'init':
this.peers.update(peers => {
peers[peer.id] = {
peer,
user: data.payload as ResultOf<typeof Viewer>['Viewer']
user: data.payload
}
return peers
})
break
case EventTypes.MagnetLinkEvent: {
const cast = data as Event<{magnet: string, hash: string}>
if (cast.payload.magnet === undefined) break
const { hash, magnet } = cast.payload
if (hash !== this.magnet?.hash) {
case 'media': {
if (data.payload?.torrent == null || data.payload?.mediaId == null) break
const { torrent, mediaId, episode } = data.payload
if (torrent !== this.media?.torrent) {
this.isHost = false
this.magnet = cast.payload
add(magnet)
this.media = data.payload
const media = (await client.single(mediaId)).data?.Media
if (media == null) break
server.play(torrent, media, episode)
}
break
}
case EventTypes.MediaIndexEvent: {
const cast = data as Event<{index: number}>
if (cast.payload.index === undefined) break
if (this.index !== cast.payload.index) {
this.index = cast.payload.index
this.emit('index', cast.payload.index)
case 'index': {
if (data.payload == null) break
if (this.index !== data.payload) {
this.index = data.payload
this.emit('index', data.payload)
}
break
}
case EventTypes.PlayerStateEvent: {
const cast = data as Event<PlayerState>
if (cast.payload.time === undefined) break
if (this._playerStateChanged(cast.payload)) this.emit('player', data.payload)
case 'player': {
if (data.payload?.time == null) break
if (this._playerStateChanged(data.payload)) this.emit('player', data.payload)
break
}
case EventTypes.MessageEvent:{
const cast = data as Event<string>
this.messages.update(messages => [...messages, ({ message: cast.payload, user: this.peers.value[peer.id].user, type: 'incoming', date: new Date() })])
case 'message': {
this.messages.update(messages => [...messages, ({ message: data.payload, user: this.peers.value[peer.id]!.user, type: 'incoming', date: new Date() })])
break
}
default:
@ -201,27 +189,16 @@ export class W2GClient extends EventEmitter {
}
}
#onPeerclose (peer: Peer) {
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
_sendToPeers (event: Event) {
for (const { peer } of Object.values(this.peers.value)) {
if (peer) this.#sendEvent(peer, event)
if (peer) this._sendEvent(peer, event)
}
}
destroy () {
debug('destroy')
this.#p2pt?.destroy()
this.#p2pt.destroy()
this.removeAllListeners()
this.#p2pt = null
this.isHost = false
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 Trash from 'lucide-svelte/icons/trash'
import X from 'lucide-svelte/icons/x'
import { tick } from 'svelte'
import MagnifyingGlass from 'svelte-radix/MagnifyingGlass.svelte'
import { toast } from 'svelte-sonner'
@ -10,6 +11,7 @@
import type { Search } from '$lib/modules/anilist/queries'
import type { VariablesOf } from 'gql.tada'
import { replaceState } from '$app/navigation'
import { page } from '$app/stores'
import { badgeVariants } from '$lib/components/ui/badge'
import { Button } from '$lib/components/ui/button'
@ -93,6 +95,8 @@
onList: [] as format[]
}
$: console.log('search updated', search)
let pageNumber = 1
let inputText = ''
@ -123,7 +127,7 @@
}
function searchQuery (filter: Partial<typeof search>, page: number) {
return client.search({
const search = {
page,
ids: filter.ids,
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'>,
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>
})
}
tick().then(() => replaceState('', { search }))
return client.search(search)
}
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'),
// yeah they dont export this for making custom icons, sucks
'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 },