mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-21 11:12:01 +00:00
fix: better breakpoints code for JS fix: macOS external player spawning
This commit is contained in:
commit
95ed1aeaf1
56 changed files with 1772 additions and 96 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ui",
|
"name": "ui",
|
||||||
"version": "6.3.72",
|
"version": "6.4.0",
|
||||||
"license": "BUSL-1.1",
|
"license": "BUSL-1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@9.14.4",
|
"packageManager": "pnpm@9.14.4",
|
||||||
|
|
@ -40,12 +40,14 @@
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vaul-svelte": "^0.3.2",
|
"vaul-svelte": "^0.3.2",
|
||||||
"vite": "^5.4.11"
|
"vite": "^5.4.11",
|
||||||
|
"vite-plugin-static-copy": "^3.0.2"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/speedtest": "^1.4.1",
|
"@cloudflare/speedtest": "^1.4.1",
|
||||||
"@fontsource-variable/nunito": "^5.2.5",
|
"@fontsource-variable/nunito": "^5.2.5",
|
||||||
|
"@fontsource/geist-mono": "^5.2.6",
|
||||||
"@prgm/sveltekit-progress-bar": "2.0.0",
|
"@prgm/sveltekit-progress-bar": "2.0.0",
|
||||||
"@thaunknown/web-irc": "^1.0.3",
|
"@thaunknown/web-irc": "^1.0.3",
|
||||||
"@urql/exchange-auth": "^2.2.1",
|
"@urql/exchange-auth": "^2.2.1",
|
||||||
|
|
@ -59,8 +61,10 @@
|
||||||
"bittorrent-tracker": "10.0.12",
|
"bittorrent-tracker": "10.0.12",
|
||||||
"bottleneck": "^2.19.5",
|
"bottleneck": "^2.19.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cobe": "0.6.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
|
"doc999tor-fast-geoip": "^1.1.335",
|
||||||
"dompurify": "^3.2.5",
|
"dompurify": "^3.2.5",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
|
|
@ -71,6 +75,7 @@
|
||||||
"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",
|
||||||
|
"svelte-headless-table": "^0.18.3",
|
||||||
"svelte-keybinds": "^1.0.9",
|
"svelte-keybinds": "^1.0.9",
|
||||||
"svelte-persisted-store": "^0.12.0",
|
"svelte-persisted-store": "^0.12.0",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.0",
|
||||||
|
|
|
||||||
132
pnpm-lock.yaml
132
pnpm-lock.yaml
|
|
@ -14,6 +14,9 @@ importers:
|
||||||
'@fontsource-variable/nunito':
|
'@fontsource-variable/nunito':
|
||||||
specifier: ^5.2.5
|
specifier: ^5.2.5
|
||||||
version: 5.2.5
|
version: 5.2.5
|
||||||
|
'@fontsource/geist-mono':
|
||||||
|
specifier: ^5.2.6
|
||||||
|
version: 5.2.6
|
||||||
'@prgm/sveltekit-progress-bar':
|
'@prgm/sveltekit-progress-bar':
|
||||||
specifier: 2.0.0
|
specifier: 2.0.0
|
||||||
version: 2.0.0(@sveltejs/kit@2.21.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(terser@5.39.0)))(svelte@4.2.19)(vite@5.4.19(terser@5.39.0)))(svelte@4.2.19)
|
version: 2.0.0(@sveltejs/kit@2.21.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.19)(vite@5.4.19(terser@5.39.0)))(svelte@4.2.19)(vite@5.4.19(terser@5.39.0)))(svelte@4.2.19)
|
||||||
|
|
@ -53,12 +56,18 @@ importers:
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
cobe:
|
||||||
|
specifier: 0.6.3
|
||||||
|
version: 0.6.3
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
debug:
|
debug:
|
||||||
specifier: ^4.4.1
|
specifier: ^4.4.1
|
||||||
version: 4.4.1
|
version: 4.4.1
|
||||||
|
doc999tor-fast-geoip:
|
||||||
|
specifier: ^1.1.335
|
||||||
|
version: 1.1.335
|
||||||
dompurify:
|
dompurify:
|
||||||
specifier: ^3.2.5
|
specifier: ^3.2.5
|
||||||
version: 3.2.5
|
version: 3.2.5
|
||||||
|
|
@ -89,6 +98,9 @@ importers:
|
||||||
simple-store-svelte:
|
simple-store-svelte:
|
||||||
specifier: ^1.0.6
|
specifier: ^1.0.6
|
||||||
version: 1.0.6
|
version: 1.0.6
|
||||||
|
svelte-headless-table:
|
||||||
|
specifier: ^0.18.3
|
||||||
|
version: 0.18.3(svelte@4.2.19)
|
||||||
svelte-keybinds:
|
svelte-keybinds:
|
||||||
specifier: ^1.0.9
|
specifier: ^1.0.9
|
||||||
version: 1.0.9
|
version: 1.0.9
|
||||||
|
|
@ -192,6 +204,9 @@ importers:
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.4.11
|
specifier: ^5.4.11
|
||||||
version: 5.4.19(terser@5.39.0)
|
version: 5.4.19(terser@5.39.0)
|
||||||
|
vite-plugin-static-copy:
|
||||||
|
specifier: ^3.0.2
|
||||||
|
version: 3.0.2(vite@5.4.19(terser@5.39.0))
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
|
@ -409,6 +424,9 @@ packages:
|
||||||
'@fontsource-variable/nunito@5.2.5':
|
'@fontsource-variable/nunito@5.2.5':
|
||||||
resolution: {integrity: sha512-XMrSfi1XrnM6HQA+MMdPVY/5tdnG4vamQScaesQRhaboP8g0dEjxbtUJY50KHFTh2MnQP5lHIyDFuMNM4Kb23A==}
|
resolution: {integrity: sha512-XMrSfi1XrnM6HQA+MMdPVY/5tdnG4vamQScaesQRhaboP8g0dEjxbtUJY50KHFTh2MnQP5lHIyDFuMNM4Kb23A==}
|
||||||
|
|
||||||
|
'@fontsource/geist-mono@5.2.6':
|
||||||
|
resolution: {integrity: sha512-I3hsRP+8Gmhk35cwlPAR4w5xqk7e5pro2F1o51ZmB+lN+dPcwN3jYHKN+u0E5AMuiQKpTdkrqfEpvBjzQax3cQ==}
|
||||||
|
|
||||||
'@gql.tada/cli-utils@1.6.3':
|
'@gql.tada/cli-utils@1.6.3':
|
||||||
resolution: {integrity: sha512-jFFSY8OxYeBxdKi58UzeMXG1tdm4FVjXa8WHIi66Gzu9JWtCE6mqom3a8xkmSw+mVaybFW5EN2WXf1WztJVNyQ==}
|
resolution: {integrity: sha512-jFFSY8OxYeBxdKi58UzeMXG1tdm4FVjXa8WHIi66Gzu9JWtCE6mqom3a8xkmSw+mVaybFW5EN2WXf1WztJVNyQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1005,6 +1023,9 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^4.0.0 || ^5.0.0-next.1
|
svelte: ^4.0.0 || ^5.0.0-next.1
|
||||||
|
|
||||||
|
cobe@0.6.3:
|
||||||
|
resolution: {integrity: sha512-WHr7X4o1ym94GZ96h7b1pNemZJacbOzd02dZtnVwuC4oWBaLg96PBmp2rIS1SAhUDhhC/QyS9WEqkpZIs/ZBTg==}
|
||||||
|
|
||||||
code-red@1.0.4:
|
code-red@1.0.4:
|
||||||
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
|
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
|
||||||
|
|
||||||
|
|
@ -1153,6 +1174,9 @@ packages:
|
||||||
dlv@1.1.3:
|
dlv@1.1.3:
|
||||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||||
|
|
||||||
|
doc999tor-fast-geoip@1.1.335:
|
||||||
|
resolution: {integrity: sha512-/rnNx4yIu84V8i8c0+95fNuVOfSRAGKNJXrSR8PSmrcXDuqzmnLNUsm++8Tqe+c9n1Fsp/3ryATTe5hHgC64hQ==}
|
||||||
|
|
||||||
doctrine@2.1.0:
|
doctrine@2.1.0:
|
||||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -1415,6 +1439,10 @@ packages:
|
||||||
fraction.js@4.3.7:
|
fraction.js@4.3.7:
|
||||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||||
|
|
||||||
|
fs-extra@11.3.0:
|
||||||
|
resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
|
||||||
|
engines: {node: '>=14.14'}
|
||||||
|
|
||||||
fs.realpath@1.0.0:
|
fs.realpath@1.0.0:
|
||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
|
|
||||||
|
|
@ -1736,6 +1764,9 @@ packages:
|
||||||
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsonfile@6.1.0:
|
||||||
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
|
|
||||||
|
|
@ -1941,6 +1972,10 @@ 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'}
|
||||||
|
|
||||||
|
p-map@7.0.3:
|
||||||
|
resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
p2pt@https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316:
|
p2pt@https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316:
|
||||||
resolution: {tarball: https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316}
|
resolution: {tarball: https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316}
|
||||||
version: 1.5.1
|
version: 1.5.1
|
||||||
|
|
@ -1981,6 +2016,9 @@ packages:
|
||||||
periscopic@3.1.0:
|
periscopic@3.1.0:
|
||||||
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
|
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
|
||||||
|
|
||||||
|
phenomenon@1.6.0:
|
||||||
|
resolution: {integrity: sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
|
@ -2352,6 +2390,11 @@ packages:
|
||||||
svelte:
|
svelte:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
svelte-headless-table@0.18.3:
|
||||||
|
resolution: {integrity: sha512-1zVnqXW0dvn6ZceYa94k+ziK+w5Dj9nlWYTQGXBv2JhM0resj9w7CWpclZK1TJwAALfEeH4InPBPO87L5fr+nQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^4.0.0
|
||||||
|
|
||||||
svelte-hmr@0.16.0:
|
svelte-hmr@0.16.0:
|
||||||
resolution: {integrity: sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==}
|
resolution: {integrity: sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==}
|
||||||
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
engines: {node: ^12.20 || ^14.13.1 || >= 16}
|
||||||
|
|
@ -2361,6 +2404,11 @@ packages:
|
||||||
svelte-keybinds@1.0.9:
|
svelte-keybinds@1.0.9:
|
||||||
resolution: {integrity: sha512-bQt9azkXX4SgMJpJzYWQB6D0hj45+Ro2+2Awr4YNtjmuRuKdio+Rxuhky5JJyBBfyRQ7YT63nSR3whH4FACv1A==}
|
resolution: {integrity: sha512-bQt9azkXX4SgMJpJzYWQB6D0hj45+Ro2+2Awr4YNtjmuRuKdio+Rxuhky5JJyBBfyRQ7YT63nSR3whH4FACv1A==}
|
||||||
|
|
||||||
|
svelte-keyed@2.0.0:
|
||||||
|
resolution: {integrity: sha512-7TeEn+QbJC2OJrHiuM0T8vMBkms3DNpTE+Ir+NtnVBnBMA78aL4f1ft9t0Hn/pBbD/TnIXi4YfjFRAgtN+DZ5g==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^4.0.0
|
||||||
|
|
||||||
svelte-persisted-store@0.12.0:
|
svelte-persisted-store@0.12.0:
|
||||||
resolution: {integrity: sha512-BdBQr2SGSJ+rDWH8/aEV5GthBJDapVP0GP3fuUCA7TjYG5ctcB+O9Mj9ZC0+Jo1oJMfZUd1y9H68NFRR5MyIJA==}
|
resolution: {integrity: sha512-BdBQr2SGSJ+rDWH8/aEV5GthBJDapVP0GP3fuUCA7TjYG5ctcB+O9Mj9ZC0+Jo1oJMfZUd1y9H68NFRR5MyIJA==}
|
||||||
engines: {node: '>=0.14'}
|
engines: {node: '>=0.14'}
|
||||||
|
|
@ -2372,11 +2420,21 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^3.54.0 || ^4.0.0 || ^5.0.0 || ^5.0.0-next.1
|
svelte: ^3.54.0 || ^4.0.0 || ^5.0.0 || ^5.0.0-next.1
|
||||||
|
|
||||||
|
svelte-render@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RpB0SurwXm4xhjvHHtjeqMmvd645FURb79GFOotScOSqnKK5vpqBgoBPGC0pp+E/eZgDSQ9rRAdn/+N4ys1mXQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^4.0.0
|
||||||
|
|
||||||
svelte-sonner@0.3.28:
|
svelte-sonner@0.3.28:
|
||||||
resolution: {integrity: sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==}
|
resolution: {integrity: sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1
|
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1
|
||||||
|
|
||||||
|
svelte-subscribe@2.0.1:
|
||||||
|
resolution: {integrity: sha512-eKXIjLxB4C7eQWPqKEdxcGfNXm2g/qJ67zmEZK/GigCZMfrTR3m7DPY93R6MX+5uoqM1FRYxl8LZ1oy4URWi2A==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^4.0.0
|
||||||
|
|
||||||
svelte2tsx@0.7.39:
|
svelte2tsx@0.7.39:
|
||||||
resolution: {integrity: sha512-NX8a7eSqF1hr6WKArvXr7TV7DeE+y0kDFD7L5JP7TWqlwFidzGKaG415p992MHREiiEWOv2xIWXJ+mlONofs0A==}
|
resolution: {integrity: sha512-NX8a7eSqF1hr6WKArvXr7TV7DeE+y0kDFD7L5JP7TWqlwFidzGKaG415p992MHREiiEWOv2xIWXJ+mlONofs0A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -2430,6 +2488,10 @@ packages:
|
||||||
resolution: {integrity: sha512-zxke8goJQpBeEgD82CXABeMh0LSJcj7CXEd0OHOg45HgcofF7pxNwZm9+RknpxpDhwN4gFpySkApKfFYfRQnUA==}
|
resolution: {integrity: sha512-zxke8goJQpBeEgD82CXABeMh0LSJcj7CXEd0OHOg45HgcofF7pxNwZm9+RknpxpDhwN4gFpySkApKfFYfRQnUA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
tinyglobby@0.2.14:
|
||||||
|
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
@ -2498,6 +2560,10 @@ packages:
|
||||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
universalify@2.0.1:
|
||||||
|
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||||
|
engines: {node: '>= 10.0.0'}
|
||||||
|
|
||||||
unordered-array-remove@1.0.2:
|
unordered-array-remove@1.0.2:
|
||||||
resolution: {integrity: sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw==}
|
resolution: {integrity: sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw==}
|
||||||
|
|
||||||
|
|
@ -2531,6 +2597,12 @@ packages:
|
||||||
video-deband@1.0.7:
|
video-deband@1.0.7:
|
||||||
resolution: {integrity: sha512-vwJ2E/e7DfvFlKU5RQ8T8ZEcG7m7A41TIxZ3X57o7Rzw+HSTNyljrtSPJU11UQR2X9wVmAC7WKdOs7zOsxNV6A==}
|
resolution: {integrity: sha512-vwJ2E/e7DfvFlKU5RQ8T8ZEcG7m7A41TIxZ3X57o7Rzw+HSTNyljrtSPJU11UQR2X9wVmAC7WKdOs7zOsxNV6A==}
|
||||||
|
|
||||||
|
vite-plugin-static-copy@3.0.2:
|
||||||
|
resolution: {integrity: sha512-/seLvhUg44s1oU9RhjTZZy/0NPbfNctozdysKcvPovxxXZdI5l19mGq6Ri3IaTf1Dy/qChS4BSR7ayxeu8o9aQ==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
vite: ^5.0.0 || ^6.0.0
|
||||||
|
|
||||||
vite@5.4.19:
|
vite@5.4.19:
|
||||||
resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
|
resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
|
@ -2820,6 +2892,8 @@ snapshots:
|
||||||
|
|
||||||
'@fontsource-variable/nunito@5.2.5': {}
|
'@fontsource-variable/nunito@5.2.5': {}
|
||||||
|
|
||||||
|
'@fontsource/geist-mono@5.2.6': {}
|
||||||
|
|
||||||
'@gql.tada/cli-utils@1.6.3(@0no-co/graphqlsp@1.12.16(graphql@16.10.0)(typescript@5.8.3))(@gql.tada/svelte-support@1.0.1(svelte@4.2.19)(typescript@5.8.3))(graphql@16.10.0)(typescript@5.8.3)':
|
'@gql.tada/cli-utils@1.6.3(@0no-co/graphqlsp@1.12.16(graphql@16.10.0)(typescript@5.8.3))(@gql.tada/svelte-support@1.0.1(svelte@4.2.19)(typescript@5.8.3))(graphql@16.10.0)(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@0no-co/graphqlsp': 1.12.16(graphql@16.10.0)(typescript@5.8.3)
|
'@0no-co/graphqlsp': 1.12.16(graphql@16.10.0)(typescript@5.8.3)
|
||||||
|
|
@ -3492,6 +3566,10 @@ snapshots:
|
||||||
nanoid: 5.1.5
|
nanoid: 5.1.5
|
||||||
svelte: 4.2.19
|
svelte: 4.2.19
|
||||||
|
|
||||||
|
cobe@0.6.3:
|
||||||
|
dependencies:
|
||||||
|
phenomenon: 1.6.0
|
||||||
|
|
||||||
code-red@1.0.4:
|
code-red@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
@ -3630,6 +3708,8 @@ snapshots:
|
||||||
|
|
||||||
dlv@1.1.3: {}
|
dlv@1.1.3: {}
|
||||||
|
|
||||||
|
doc999tor-fast-geoip@1.1.335: {}
|
||||||
|
|
||||||
doctrine@2.1.0:
|
doctrine@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils: 2.0.3
|
esutils: 2.0.3
|
||||||
|
|
@ -4025,6 +4105,12 @@ snapshots:
|
||||||
|
|
||||||
fraction.js@4.3.7: {}
|
fraction.js@4.3.7: {}
|
||||||
|
|
||||||
|
fs-extra@11.3.0:
|
||||||
|
dependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
jsonfile: 6.1.0
|
||||||
|
universalify: 2.0.1
|
||||||
|
|
||||||
fs.realpath@1.0.0: {}
|
fs.realpath@1.0.0: {}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
|
|
@ -4351,6 +4437,12 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
|
|
||||||
|
jsonfile@6.1.0:
|
||||||
|
dependencies:
|
||||||
|
universalify: 2.0.1
|
||||||
|
optionalDependencies:
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
|
|
@ -4529,6 +4621,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
|
|
||||||
|
p-map@7.0.3: {}
|
||||||
|
|
||||||
p2pt@https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316:
|
p2pt@https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316:
|
||||||
dependencies:
|
dependencies:
|
||||||
bittorrent-tracker: 10.0.12
|
bittorrent-tracker: 10.0.12
|
||||||
|
|
@ -4570,6 +4664,8 @@ snapshots:
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
is-reference: 3.0.3
|
is-reference: 3.0.3
|
||||||
|
|
||||||
|
phenomenon@1.6.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
|
|
@ -4998,12 +5094,23 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 4.2.19
|
svelte: 4.2.19
|
||||||
|
|
||||||
|
svelte-headless-table@0.18.3(svelte@4.2.19):
|
||||||
|
dependencies:
|
||||||
|
svelte: 4.2.19
|
||||||
|
svelte-keyed: 2.0.0(svelte@4.2.19)
|
||||||
|
svelte-render: 2.0.1(svelte@4.2.19)
|
||||||
|
svelte-subscribe: 2.0.1(svelte@4.2.19)
|
||||||
|
|
||||||
svelte-hmr@0.16.0(svelte@4.2.19):
|
svelte-hmr@0.16.0(svelte@4.2.19):
|
||||||
dependencies:
|
dependencies:
|
||||||
svelte: 4.2.19
|
svelte: 4.2.19
|
||||||
|
|
||||||
svelte-keybinds@1.0.9: {}
|
svelte-keybinds@1.0.9: {}
|
||||||
|
|
||||||
|
svelte-keyed@2.0.0(svelte@4.2.19):
|
||||||
|
dependencies:
|
||||||
|
svelte: 4.2.19
|
||||||
|
|
||||||
svelte-persisted-store@0.12.0(svelte@4.2.19):
|
svelte-persisted-store@0.12.0(svelte@4.2.19):
|
||||||
dependencies:
|
dependencies:
|
||||||
svelte: 4.2.19
|
svelte: 4.2.19
|
||||||
|
|
@ -5012,10 +5119,19 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
svelte: 4.2.19
|
svelte: 4.2.19
|
||||||
|
|
||||||
|
svelte-render@2.0.1(svelte@4.2.19):
|
||||||
|
dependencies:
|
||||||
|
svelte: 4.2.19
|
||||||
|
svelte-subscribe: 2.0.1(svelte@4.2.19)
|
||||||
|
|
||||||
svelte-sonner@0.3.28(svelte@4.2.19):
|
svelte-sonner@0.3.28(svelte@4.2.19):
|
||||||
dependencies:
|
dependencies:
|
||||||
svelte: 4.2.19
|
svelte: 4.2.19
|
||||||
|
|
||||||
|
svelte-subscribe@2.0.1(svelte@4.2.19):
|
||||||
|
dependencies:
|
||||||
|
svelte: 4.2.19
|
||||||
|
|
||||||
svelte2tsx@0.7.39(svelte@4.2.19)(typescript@5.8.3):
|
svelte2tsx@0.7.39(svelte@4.2.19)(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
dedent-js: 1.0.1
|
dedent-js: 1.0.1
|
||||||
|
|
@ -5102,6 +5218,11 @@ snapshots:
|
||||||
|
|
||||||
time-stamp@2.2.0: {}
|
time-stamp@2.2.0: {}
|
||||||
|
|
||||||
|
tinyglobby@0.2.14:
|
||||||
|
dependencies:
|
||||||
|
fdir: 6.4.4(picomatch@4.0.2)
|
||||||
|
picomatch: 4.0.2
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
@ -5187,6 +5308,8 @@ snapshots:
|
||||||
has-symbols: 1.1.0
|
has-symbols: 1.1.0
|
||||||
which-boxed-primitive: 1.1.1
|
which-boxed-primitive: 1.1.1
|
||||||
|
|
||||||
|
universalify@2.0.1: {}
|
||||||
|
|
||||||
unordered-array-remove@1.0.2: {}
|
unordered-array-remove@1.0.2: {}
|
||||||
|
|
||||||
update-browserslist-db@1.1.3(browserslist@4.24.5):
|
update-browserslist-db@1.1.3(browserslist@4.24.5):
|
||||||
|
|
@ -5222,6 +5345,15 @@ snapshots:
|
||||||
rvfc-polyfill: 1.0.7
|
rvfc-polyfill: 1.0.7
|
||||||
twgl.js: 5.5.4
|
twgl.js: 5.5.4
|
||||||
|
|
||||||
|
vite-plugin-static-copy@3.0.2(vite@5.4.19(terser@5.39.0)):
|
||||||
|
dependencies:
|
||||||
|
chokidar: 3.6.0
|
||||||
|
fs-extra: 11.3.0
|
||||||
|
p-map: 7.0.3
|
||||||
|
picocolors: 1.1.1
|
||||||
|
tinyglobby: 0.2.14
|
||||||
|
vite: 5.4.19(terser@5.39.0)
|
||||||
|
|
||||||
vite@5.4.19(terser@5.39.0):
|
vite@5.4.19(terser@5.39.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: "Geist Mono", ui-monospace, SFMono-Regular, Roboto Mono, Menlo, Monaco, Liberation Mono, DejaVu Sans Mono, Courier New, monospace !important;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 240 10% 3.9%;
|
--foreground: 240 10% 3.9%;
|
||||||
|
|
|
||||||
68
src/app.d.ts
vendored
68
src/app.d.ts
vendored
|
|
@ -38,17 +38,55 @@ export interface Attachment {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TorrentInfo {
|
export interface TorrentInfo {
|
||||||
peers: number
|
|
||||||
progress: number
|
|
||||||
down: number
|
|
||||||
up: number
|
|
||||||
name: string
|
name: string
|
||||||
|
progress: number
|
||||||
|
size: {
|
||||||
|
total: number
|
||||||
|
downloaded: number
|
||||||
|
uploaded: number
|
||||||
|
}
|
||||||
|
speed: {
|
||||||
|
down: number
|
||||||
|
up: number
|
||||||
|
}
|
||||||
|
time: {
|
||||||
|
remaining: number
|
||||||
|
elapsed: number
|
||||||
|
}
|
||||||
|
peers: {
|
||||||
|
seeders: number
|
||||||
|
leechers: number
|
||||||
|
wires: number
|
||||||
|
}
|
||||||
|
pieces: {
|
||||||
|
total: number
|
||||||
|
size: number
|
||||||
|
}
|
||||||
hash: string
|
hash: string
|
||||||
seeders: number
|
}
|
||||||
leechers: number
|
|
||||||
|
export interface PeerInfo {
|
||||||
|
ip: string
|
||||||
|
seeder: boolean
|
||||||
|
client: string
|
||||||
|
progress: number
|
||||||
|
size: {
|
||||||
|
downloaded: number
|
||||||
|
uploaded: number
|
||||||
|
}
|
||||||
|
speed: {
|
||||||
|
down: number
|
||||||
|
up: number
|
||||||
|
}
|
||||||
|
flags: Array<'incoming' | 'outgoing' | 'utp' | 'encrypted'>
|
||||||
|
time: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
name: string
|
||||||
size: number
|
size: number
|
||||||
downloaded: number
|
progress: number
|
||||||
eta: number
|
selections: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TorrentSettings {
|
export interface TorrentSettings {
|
||||||
|
|
@ -95,8 +133,18 @@ export interface Native {
|
||||||
subtitles: (hash: string, id: number, cb: (subtitle: { text: string, time: number, duration: number }, trackNumber: number) => void) => Promise<void>
|
subtitles: (hash: string, id: number, cb: (subtitle: { text: string, time: number, duration: number }, trackNumber: number) => void) => Promise<void>
|
||||||
errors: (cb: (error: Error) => void) => Promise<void>
|
errors: (cb: (error: Error) => void) => Promise<void>
|
||||||
chapters: (hash: string, id: number) => Promise<Array<{ start: number, end: number, text: string }>>
|
chapters: (hash: string, id: number) => Promise<Array<{ start: number, end: number, text: string }>>
|
||||||
torrentStats: (hash: string) => Promise<TorrentInfo>
|
torrentInfo: (hash: string) => Promise<TorrentInfo>
|
||||||
torrents: () => Promise<TorrentInfo[]>
|
peerInfo: (hash: string) => Promise<PeerInfo[]>
|
||||||
|
fileInfo: (hash: string) => Promise<FileInfo[]>
|
||||||
|
protocolStatus: (hash: string) => Promise<{
|
||||||
|
dht: boolean
|
||||||
|
lsd: boolean
|
||||||
|
pex: boolean
|
||||||
|
nat: boolean
|
||||||
|
forwarding: boolean
|
||||||
|
persisting: boolean
|
||||||
|
streaming: boolean
|
||||||
|
}>
|
||||||
setDOH: (dns: string) => Promise<void>
|
setDOH: (dns: string) => Promise<void>
|
||||||
cachedTorrents: () => Promise<string[]>
|
cachedTorrents: () => Promise<string[]>
|
||||||
downloadProgress: (percent: number) => Promise<void>
|
downloadProgress: (percent: number) => Promise<void>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
import { authAggregator, list, progress } from '$lib/modules/auth'
|
import { authAggregator, list, progress } from '$lib/modules/auth'
|
||||||
import { click, dragScroll } from '$lib/modules/navigate'
|
import { click, dragScroll } from '$lib/modules/navigate'
|
||||||
import { liveAnimeProgress } from '$lib/modules/watchProgress'
|
import { liveAnimeProgress } from '$lib/modules/watchProgress'
|
||||||
import { cn, isMobile, since } from '$lib/utils'
|
import { breakpoints, cn, since } from '$lib/utils'
|
||||||
|
|
||||||
export let eps: EpisodesResponse | null
|
export let eps: EpisodesResponse | null
|
||||||
export let media: Media
|
export let media: Media
|
||||||
|
|
@ -150,7 +150,7 @@
|
||||||
<Button size='icon' variant='ghost' on:click={() => setPage(currentPage - 1)} disabled={!hasPrev}>
|
<Button size='icon' variant='ghost' on:click={() => setPage(currentPage - 1)} disabled={!hasPrev}>
|
||||||
<ChevronLeft class='h-4 w-4' />
|
<ChevronLeft class='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
{#if !$isMobile}
|
{#if $breakpoints.md}
|
||||||
{#each pages as { page, type } (page)}
|
{#each pages as { page, type } (page)}
|
||||||
{#if type === 'ellipsis'}
|
{#if type === 'ellipsis'}
|
||||||
<span class='h-9 w-9 text-center'>...</span>
|
<span class='h-9 w-9 text-center'>...</span>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
import Check from 'svelte-radix/Check.svelte'
|
||||||
|
|
||||||
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.CheckboxItemProps
|
||||||
|
type $$Events = DropdownMenuPrimitive.CheckboxItemEvents
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined
|
||||||
|
export let checked: $$Props['checked'] = undefined
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
bind:checked
|
||||||
|
class={cn(
|
||||||
|
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerdown
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<span class='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||||
|
<DropdownMenuPrimitive.CheckboxIndicator>
|
||||||
|
<Check class='h-4 w-4' />
|
||||||
|
</DropdownMenuPrimitive.CheckboxIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
|
import { cn, flyAndScale } from '$lib/utils.js'
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.ContentProps
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined
|
||||||
|
export let sideOffset: $$Props['sideOffset'] = 4
|
||||||
|
export let transition: $$Props['transition'] = flyAndScale
|
||||||
|
export let transitionConfig: $$Props['transitionConfig'] = undefined
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
{sideOffset}
|
||||||
|
class={cn(
|
||||||
|
'bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-md focus:outline-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:keydown
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.Content>
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.ItemProps & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
type $$Events = DropdownMenuPrimitive.ItemEvents
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined
|
||||||
|
export let inset: $$Props['inset'] = undefined
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
class={cn(
|
||||||
|
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerdown
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.Item>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.LabelProps & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined
|
||||||
|
export let inset: $$Props['inset'] = undefined
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
class={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.Label>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.RadioGroupProps
|
||||||
|
|
||||||
|
export let value: $$Props['value'] = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioGroup {...$$restProps} bind:value>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.RadioGroup>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
import DotFilled from 'svelte-radix/DotFilled.svelte'
|
||||||
|
|
||||||
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.RadioItemProps
|
||||||
|
type $$Events = DropdownMenuPrimitive.RadioItemEvents
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined
|
||||||
|
export let value: DropdownMenuPrimitive.RadioItemProps['value']
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
class={cn(
|
||||||
|
'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{value}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerdown
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<span class='absolute left-2 flex h-3.5 w-3.5 items-center justify-center'>
|
||||||
|
<DropdownMenuPrimitive.RadioIndicator>
|
||||||
|
<DotFilled class='h-4 w-4 fill-current' />
|
||||||
|
</DropdownMenuPrimitive.RadioIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.SeparatorProps
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
class={cn('bg-muted -mx-1 my-1 h-px', className)}
|
||||||
|
{...$$restProps}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements'
|
||||||
|
|
||||||
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
|
type $$Props = HTMLAttributes<HTMLSpanElement>
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span class={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
|
import { cn, flyAndScale } from '$lib/utils.js'
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.SubContentProps
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined
|
||||||
|
export let transition: $$Props['transition'] = flyAndScale
|
||||||
|
export let transitionConfig: $$Props['transitionConfig'] = {
|
||||||
|
x: -10,
|
||||||
|
y: 0
|
||||||
|
}
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
{transition}
|
||||||
|
{transitionConfig}
|
||||||
|
class={cn(
|
||||||
|
'bg-popover text-popover-foreground z-50 min-w-[8rem] rounded-md border p-1 shadow-lg focus:outline-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:keydown
|
||||||
|
on:focusout
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuPrimitive.SubContent>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
import ChevronRight from 'svelte-radix/ChevronRight.svelte'
|
||||||
|
|
||||||
|
import { cn } from '$lib/utils.js'
|
||||||
|
|
||||||
|
type $$Props = DropdownMenuPrimitive.SubTriggerProps & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
type $$Events = DropdownMenuPrimitive.SubTriggerEvents
|
||||||
|
|
||||||
|
let className: $$Props['class'] = undefined
|
||||||
|
export let inset: $$Props['inset'] = undefined
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
class={cn(
|
||||||
|
'data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
|
||||||
|
inset && 'pl-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...$$restProps}
|
||||||
|
on:click
|
||||||
|
on:keydown
|
||||||
|
on:focusin
|
||||||
|
on:focusout
|
||||||
|
on:pointerleave
|
||||||
|
on:pointermove
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<ChevronRight class='ml-auto h-4 w-4' />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
49
src/lib/components/ui/dropdown-menu/index.ts
Normal file
49
src/lib/components/ui/dropdown-menu/index.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'
|
||||||
|
|
||||||
|
import CheckboxItem from './dropdown-menu-checkbox-item.svelte'
|
||||||
|
import Content from './dropdown-menu-content.svelte'
|
||||||
|
import Item from './dropdown-menu-item.svelte'
|
||||||
|
import Label from './dropdown-menu-label.svelte'
|
||||||
|
import RadioGroup from './dropdown-menu-radio-group.svelte'
|
||||||
|
import RadioItem from './dropdown-menu-radio-item.svelte'
|
||||||
|
import Separator from './dropdown-menu-separator.svelte'
|
||||||
|
import Shortcut from './dropdown-menu-shortcut.svelte'
|
||||||
|
import SubContent from './dropdown-menu-sub-content.svelte'
|
||||||
|
import SubTrigger from './dropdown-menu-sub-trigger.svelte'
|
||||||
|
|
||||||
|
const Sub = DropdownMenuPrimitive.Sub
|
||||||
|
const Root = DropdownMenuPrimitive.Root
|
||||||
|
const Trigger = DropdownMenuPrimitive.Trigger
|
||||||
|
const Group = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sub,
|
||||||
|
Root,
|
||||||
|
Item,
|
||||||
|
Label,
|
||||||
|
Group,
|
||||||
|
Trigger,
|
||||||
|
Content,
|
||||||
|
Shortcut,
|
||||||
|
Separator,
|
||||||
|
RadioItem,
|
||||||
|
SubContent,
|
||||||
|
SubTrigger,
|
||||||
|
RadioGroup,
|
||||||
|
CheckboxItem,
|
||||||
|
//
|
||||||
|
Root as DropdownMenu,
|
||||||
|
Sub as DropdownMenuSub,
|
||||||
|
Item as DropdownMenuItem,
|
||||||
|
Label as DropdownMenuLabel,
|
||||||
|
Group as DropdownMenuGroup,
|
||||||
|
Content as DropdownMenuContent,
|
||||||
|
Trigger as DropdownMenuTrigger,
|
||||||
|
Shortcut as DropdownMenuShortcut,
|
||||||
|
RadioItem as DropdownMenuRadioItem,
|
||||||
|
Separator as DropdownMenuSeparator,
|
||||||
|
RadioGroup as DropdownMenuRadioGroup,
|
||||||
|
SubContent as DropdownMenuSubContent,
|
||||||
|
SubTrigger as DropdownMenuSubTrigger,
|
||||||
|
CheckboxItem as DropdownMenuCheckboxItem
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import { Comment } from './'
|
import { Comment } from './'
|
||||||
|
|
||||||
import { client } from '$lib/modules/anilist'
|
import { client } from '$lib/modules/anilist'
|
||||||
import { isMobile } from '$lib/utils'
|
import { breakpoints } from '$lib/utils'
|
||||||
|
|
||||||
export let isLocked = false
|
export let isLocked = false
|
||||||
export let threadId: number
|
export let threadId: number
|
||||||
|
|
@ -71,7 +71,7 @@
|
||||||
<Button size='icon' variant='ghost' on:click={() => setPage(currentPage - 1)} disabled={!hasPrev}>
|
<Button size='icon' variant='ghost' on:click={() => setPage(currentPage - 1)} disabled={!hasPrev}>
|
||||||
<ChevronLeft class='h-4 w-4' />
|
<ChevronLeft class='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
{#if !$isMobile}
|
{#if $breakpoints.md}
|
||||||
{#each pages as { page, type } (page)}
|
{#each pages as { page, type } (page)}
|
||||||
{#if type === 'ellipsis'}
|
{#if type === 'ellipsis'}
|
||||||
<span class='h-9 w-9 text-center'>...</span>
|
<span class='h-9 w-9 text-center'>...</span>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
import * as Avatar from '$lib/components/ui/avatar'
|
import * as Avatar from '$lib/components/ui/avatar'
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip'
|
import * as Tooltip from '$lib/components/ui/tooltip'
|
||||||
import { client, type Media } from '$lib/modules/anilist'
|
import { client, type Media } from '$lib/modules/anilist'
|
||||||
import { isMobile, since } from '$lib/utils'
|
import { breakpoints, since } from '$lib/utils'
|
||||||
|
|
||||||
export let media: Media
|
export let media: Media
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
<Button size='icon' variant='ghost' on:click={() => setPage(currentPage - 1)} disabled={!hasPrev}>
|
<Button size='icon' variant='ghost' on:click={() => setPage(currentPage - 1)} disabled={!hasPrev}>
|
||||||
<ChevronLeft class='h-4 w-4' />
|
<ChevronLeft class='h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
{#if !$isMobile}
|
{#if $breakpoints.md}
|
||||||
{#each pages as { page, type } (page)}
|
{#each pages as { page, type } (page)}
|
||||||
{#if type === 'ellipsis'}
|
{#if type === 'ellipsis'}
|
||||||
<span class='h-9 w-9 text-center'>...</span>
|
<span class='h-9 w-9 text-center'>...</span>
|
||||||
|
|
|
||||||
|
|
@ -754,15 +754,15 @@
|
||||||
<!-- {($torrentstats.progress * 100).toFixed(1)}% -->
|
<!-- {($torrentstats.progress * 100).toFixed(1)}% -->
|
||||||
<div class='flex justify-center items-center gap-2'>
|
<div class='flex justify-center items-center gap-2'>
|
||||||
<Users size={18} />
|
<Users size={18} />
|
||||||
{$torrentstats.seeders}
|
{$torrentstats.peers.seeders}
|
||||||
</div>
|
</div>
|
||||||
<div class='flex justify-center items-center gap-2'>
|
<div class='flex justify-center items-center gap-2'>
|
||||||
<ChevronDown size={18} />
|
<ChevronDown size={18} />
|
||||||
{fastPrettyBits($torrentstats.down * 8)}/s
|
{fastPrettyBits($torrentstats.speed.down * 8)}/s
|
||||||
</div>
|
</div>
|
||||||
<div class='flex justify-center items-center gap-2'>
|
<div class='flex justify-center items-center gap-2'>
|
||||||
<ChevronUp size={18} />
|
<ChevronUp size={18} />
|
||||||
{fastPrettyBits($torrentstats.up * 8)}/s
|
{fastPrettyBits($torrentstats.speed.up * 8)}/s
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if seeking}
|
{#if seeking}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@
|
||||||
|
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
|
|
||||||
import { isMobile } from '$lib/utils'
|
import { breakpoints } from '$lib/utils'
|
||||||
|
|
||||||
let open = false // 152 x 140
|
let open = false // 152 x 140
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $isMobile}
|
{#if !$breakpoints.md}
|
||||||
<div class='shrink-0 z-50 bg-black absolute right-4 bottom-4 w-14 h-[52px] flex rounded-md items-end justify-end overflow-clip transition-[width,height] group-fullscreen/fullscreen:hidden' class:!w-[152px]={open} class:!h-[140px]={open}>
|
<div class='shrink-0 z-50 bg-black absolute right-4 bottom-4 w-14 h-[52px] flex rounded-md items-end justify-end overflow-clip transition-[width,height] group-fullscreen/fullscreen:hidden' class:!w-[152px]={open} class:!h-[140px]={open}>
|
||||||
<div class='p-2 grid grid-cols-3 gap-2 shrink-0'>
|
<div class='p-2 grid grid-cols-3 gap-2 shrink-0'>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
export { className as class }
|
export { className as class }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class='relative w-full overflow-auto'>
|
<div class='relative w-full overflow-auto h-full'>
|
||||||
<table class={cn('w-full caption-bottom text-sm', className)} {...$$restProps}>
|
<table class={cn('w-full caption-bottom text-sm', className)} {...$$restProps}>
|
||||||
<slot />
|
<slot />
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
65
src/lib/components/ui/torrentclient/columnheader.svelte
Normal file
65
src/lib/components/ui/torrentclient/columnheader.svelte
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import ArrowDown from 'svelte-radix/ArrowDown.svelte'
|
||||||
|
import ArrowUp from 'svelte-radix/ArrowUp.svelte'
|
||||||
|
|
||||||
|
import { Button } from '$lib/components/ui/button'
|
||||||
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu'
|
||||||
|
import { cn } from '$lib/utils'
|
||||||
|
|
||||||
|
let className: string | undefined | null = ''
|
||||||
|
export { className as class }
|
||||||
|
export let props: {
|
||||||
|
sort: {
|
||||||
|
order: 'desc' | 'asc' | undefined
|
||||||
|
toggle: (_: Event) => void
|
||||||
|
clear: () => void
|
||||||
|
disabled: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAscSort (e: Event) {
|
||||||
|
if (props.sort.order === 'asc') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
props.sort.toggle(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDescSort (e: Event) {
|
||||||
|
if (props.sort.order === 'desc') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (props.sort.order === undefined) {
|
||||||
|
// We can only toggle, so we toggle from undefined to 'asc' first
|
||||||
|
props.sort.toggle(e)
|
||||||
|
}
|
||||||
|
props.sort.toggle(e) // Then we toggle from 'asc' to 'desc'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !props.sort.disabled}
|
||||||
|
<div class={cn('flex items-center', className)}>
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger asChild let:builder>
|
||||||
|
<Button
|
||||||
|
variant='ghost'
|
||||||
|
builders={[builder]}
|
||||||
|
class='h-8 data-[state=open]:bg-accent text-sm px-4 w-full justify-start'
|
||||||
|
size='sm'>
|
||||||
|
<slot />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
<DropdownMenu.Content align='start' sameWidth={true}>
|
||||||
|
<DropdownMenu.Item on:click={handleAscSort} class='cursor-pointer'>
|
||||||
|
<ArrowUp class='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
|
||||||
|
Asc
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item on:click={handleDescSort} class='cursor-pointer'>
|
||||||
|
<ArrowDown class='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
|
||||||
|
Desc
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<slot />
|
||||||
|
{/if}
|
||||||
2
src/lib/components/ui/torrentclient/files/cells/index.ts
Normal file
2
src/lib/components/ui/torrentclient/files/cells/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as NameCell } from './name.svelte'
|
||||||
|
export { default as ProgressCell } from './progress.svelte'
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
export let value: string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='text-xs font-mono'>{value}</div>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
export let value: number
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='min-w-32 w-full overflow-clip rounded-full bg-secondary h-1.5 mt-1.5'>
|
||||||
|
<div class='h-full w-full bg-primary transition-transform transform-gpu' style:--tw-translate-x='{(value * 100) - 100}%' />
|
||||||
|
</div>
|
||||||
|
<div class='text-xs mt-1 text-muted-foreground'>{(value * 100).toFixed(1)}%</div>
|
||||||
97
src/lib/components/ui/torrentclient/files/table.svelte
Normal file
97
src/lib/components/ui/torrentclient/files/table.svelte
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table'
|
||||||
|
import { addSortBy } from 'svelte-headless-table/plugins'
|
||||||
|
|
||||||
|
import Columnheader from '../columnheader.svelte'
|
||||||
|
|
||||||
|
import { NameCell, ProgressCell } from './cells'
|
||||||
|
|
||||||
|
import * as Table from '$lib/components/ui/table'
|
||||||
|
import { server } from '$lib/modules/torrent'
|
||||||
|
import { cn, fastPrettyBytes } from '$lib/utils'
|
||||||
|
|
||||||
|
const table = createTable(server.files, {
|
||||||
|
sort: addSortBy({ toggleOrder: ['asc', 'desc'] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = table.createColumns([
|
||||||
|
table.column({
|
||||||
|
accessor: 'name',
|
||||||
|
header: 'File Name',
|
||||||
|
id: 'name',
|
||||||
|
cell: ({ value }) => createRender(NameCell, { value })
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: 'size',
|
||||||
|
header: 'Size',
|
||||||
|
id: 'size',
|
||||||
|
cell: ({ value }) => fastPrettyBytes(value)
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: 'progress',
|
||||||
|
header: 'Progress',
|
||||||
|
id: 'progress',
|
||||||
|
cell: ({ value }) => createRender(ProgressCell, { value })
|
||||||
|
}),
|
||||||
|
table.column({ accessor: 'selections', header: 'Selections', id: 'selections' })
|
||||||
|
])
|
||||||
|
|
||||||
|
const tableModel = table.createViewModel(columns)
|
||||||
|
|
||||||
|
const { headerRows, pageRows, tableAttrs, tableBodyAttrs } = tableModel
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='rounded-md border max-w-screen-xl h-full overflow-clip contain-strict'>
|
||||||
|
<Table.Root {...$tableAttrs} class='max-h-full'>
|
||||||
|
<Table.Header class='px-5'>
|
||||||
|
{#each $headerRows as headerRow, i (i)}
|
||||||
|
<Subscribe rowAttrs={headerRow.attrs()}>
|
||||||
|
<Table.Row class='sticky top-0 bg-black z-[2]'>
|
||||||
|
{#each headerRow.cells as cell (cell.id)}
|
||||||
|
<Subscribe
|
||||||
|
attrs={cell.attrs()}
|
||||||
|
props={cell.props()}
|
||||||
|
let:attrs
|
||||||
|
let:props>
|
||||||
|
<Table.Head {...attrs} class={cn('px-0 first:pl-2 h-12 last:pr-2', cell.id === 'name' && 'w-full')}>
|
||||||
|
{#if cell.id !== 'flags'}
|
||||||
|
<Columnheader {props}>
|
||||||
|
<Render of={cell.render()} />
|
||||||
|
</Columnheader>
|
||||||
|
{:else}
|
||||||
|
<div class='text-sm px-4'>
|
||||||
|
<Render of={cell.render()} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Table.Head>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body {...$tableBodyAttrs} class='max-h-full overflow-y-scroll'>
|
||||||
|
{#if $pageRows.length}
|
||||||
|
{#each $pageRows as row (row.id)}
|
||||||
|
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
||||||
|
<Table.Row {...rowAttrs} class='h-12'>
|
||||||
|
{#each row.cells as cell (cell.id)}
|
||||||
|
<Subscribe attrs={cell.attrs()} let:attrs>
|
||||||
|
<Table.Cell {...attrs} class={cn('px-4 h-14 first:pl-6 last:pr-6 text-nowrap', (cell.id === 'downloaded' || cell.id === 'episode') && 'text-muted-foreground')}>
|
||||||
|
<Render of={cell.render()} />
|
||||||
|
</Table.Cell>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={columns.length} class='h-40 text-center'>
|
||||||
|
No files downloaded yet.
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
104
src/lib/components/ui/torrentclient/globe.svelte
Normal file
104
src/lib/components/ui/torrentclient/globe.svelte
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import createGlobe from 'cobe'
|
||||||
|
|
||||||
|
import { lookup } from '$lib/modules/geoip'
|
||||||
|
import { server } from '$lib/modules/torrent'
|
||||||
|
import { breakpoints } from '$lib/utils'
|
||||||
|
|
||||||
|
const normalize = (val = 0, max = 0, min = 0) => (val - min) / (max - min) || 0
|
||||||
|
|
||||||
|
const peers = server.peers
|
||||||
|
|
||||||
|
// pick first 64 peers based on download/upload speeds
|
||||||
|
// cobe limits to 64
|
||||||
|
// then normalize the speeds to a range of 0-1
|
||||||
|
$: picked = $peers
|
||||||
|
.sort((a, b) => (b.speed.down + b.speed.up) - (a.speed.down + a.speed.up))
|
||||||
|
.slice(0, 64)
|
||||||
|
|
||||||
|
// then normalize the speeds to a range of 0-1
|
||||||
|
$: lowestSpeed = Math.min(...picked.map(p => p.speed.down + p.speed.up))
|
||||||
|
$: highestSpeed = Math.max(...picked.map(p => p.speed.down + p.speed.up))
|
||||||
|
$: normalized = picked.map(peer => ({
|
||||||
|
...peer,
|
||||||
|
normalizedSpeed: normalize(peer.speed.down + peer.speed.up, highestSpeed, lowestSpeed)
|
||||||
|
}))
|
||||||
|
|
||||||
|
let markers: Record<string, {
|
||||||
|
location: [number, number]
|
||||||
|
size: number
|
||||||
|
} | null>
|
||||||
|
|
||||||
|
$: markers = Object.fromEntries(
|
||||||
|
normalized.map(({ ip }) => [
|
||||||
|
ip,
|
||||||
|
null
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
function createMarker (ip: string, marker: { location: [number, number], size: number }) {
|
||||||
|
if (ip in markers) {
|
||||||
|
markers[ip] = marker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = 0.05
|
||||||
|
const minSize = 0.02
|
||||||
|
|
||||||
|
$: {
|
||||||
|
for (const { ip, normalizedSpeed } of normalized) {
|
||||||
|
lookup(ip).then(({ city, ll }) => {
|
||||||
|
if (!city) {
|
||||||
|
ll[0] += (Math.random() - 0.5) * 4
|
||||||
|
ll[1] += (Math.random() - 0.5) * 4
|
||||||
|
}
|
||||||
|
createMarker(ip, {
|
||||||
|
location: ll,
|
||||||
|
size: Math.min(Math.max(normalizedSpeed * maxSize, minSize), maxSize)
|
||||||
|
})
|
||||||
|
}).catch(() => undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: size = $breakpoints['3xl'] ? 600 : 400
|
||||||
|
|
||||||
|
const scale = 1.5
|
||||||
|
const oneOverScale = 1 / scale
|
||||||
|
|
||||||
|
function makeGlobe (canvas: HTMLCanvasElement) {
|
||||||
|
const globe = createGlobe(canvas, {
|
||||||
|
devicePixelRatio: window.devicePixelRatio,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
phi: 0,
|
||||||
|
theta: 0.1,
|
||||||
|
dark: 1,
|
||||||
|
diffuse: 1.4,
|
||||||
|
mapSamples: 19000,
|
||||||
|
mapBrightness: 6,
|
||||||
|
opacity: 0.8,
|
||||||
|
baseColor: [0.23, 0.23, 0.23],
|
||||||
|
markerColor: [0.05, 1, 0],
|
||||||
|
glowColor: [0, 0, 0],
|
||||||
|
markers: [],
|
||||||
|
scale,
|
||||||
|
offset: [size * 0.8 * oneOverScale, size * oneOverScale * 0.4],
|
||||||
|
onRender: state => {
|
||||||
|
state.phi = Date.now() * 0.0002 % (Math.PI * 2)
|
||||||
|
state.width = size
|
||||||
|
state.height = size
|
||||||
|
state.offset = [size * 0.8 * oneOverScale, size * oneOverScale * 0.4]
|
||||||
|
|
||||||
|
state.markers = Object.values(markers).filter(m => m)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy () {
|
||||||
|
globe.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<canvas use:makeGlobe class='absolute bottom-0 right-0 -z-[1] pointer-events-none' width={size} height={size} />
|
||||||
6
src/lib/components/ui/torrentclient/index.ts
Normal file
6
src/lib/components/ui/torrentclient/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export { default as Globe } from './globe.svelte'
|
||||||
|
export { default as Overview } from './overview.svelte'
|
||||||
|
|
||||||
|
export { default as PeersTable } from './peers/table.svelte'
|
||||||
|
export { default as LibraryTable } from './library/table.svelte'
|
||||||
|
export { default as FilesTable } from './files/table.svelte'
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
export let value: number
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='text-sm'>{new Date(value).toLocaleDateString()}</div>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as StatusCell } from './status.svelte'
|
||||||
|
export { default as NameCell } from './name.svelte'
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
export let value: string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='text-xs font-mono'>{value}</div>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
export let value: boolean
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='flex gap-x-2 items-center'>
|
||||||
|
<div class='w-2 h-2 rounded-full flex-shrink-0 {value ? 'bg-green-500' : 'bg-blue-500'}' />{value ? 'Completed' : 'In Progress'}
|
||||||
|
</div>
|
||||||
117
src/lib/components/ui/torrentclient/library/table.svelte
Normal file
117
src/lib/components/ui/torrentclient/library/table.svelte
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table'
|
||||||
|
import { addSortBy } from 'svelte-headless-table/plugins'
|
||||||
|
|
||||||
|
import Columnheader from '../columnheader.svelte'
|
||||||
|
|
||||||
|
import { NameCell, StatusCell } from './cells'
|
||||||
|
|
||||||
|
import * as Table from '$lib/components/ui/table'
|
||||||
|
import { cn, fastPrettyBytes } from '$lib/utils'
|
||||||
|
|
||||||
|
interface LibraryEntry {
|
||||||
|
series: string
|
||||||
|
episode: string
|
||||||
|
name: string
|
||||||
|
files: number
|
||||||
|
size: number
|
||||||
|
completed: boolean
|
||||||
|
downloaded: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = writable<LibraryEntry[]>([])
|
||||||
|
|
||||||
|
const table = createTable(data, {
|
||||||
|
sort: addSortBy({ toggleOrder: ['asc', 'desc'] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = table.createColumns([
|
||||||
|
table.column({ accessor: 'series', header: 'Series', id: 'series' }),
|
||||||
|
table.column({ accessor: 'episode', header: 'Episode', id: 'episode' }),
|
||||||
|
table.column({
|
||||||
|
accessor: 'name',
|
||||||
|
header: 'Torrent Name',
|
||||||
|
id: 'name',
|
||||||
|
cell: ({ value }) => createRender(NameCell, { value })
|
||||||
|
}),
|
||||||
|
table.column({ accessor: 'files', header: 'Files', id: 'files' }),
|
||||||
|
table.column({
|
||||||
|
accessor: 'size',
|
||||||
|
header: 'Size',
|
||||||
|
id: 'size',
|
||||||
|
cell: ({ value }) => fastPrettyBytes(value)
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: 'completed',
|
||||||
|
header: 'Status',
|
||||||
|
id: 'completed',
|
||||||
|
cell: ({ value }) => createRender(StatusCell, { value })
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: 'downloaded',
|
||||||
|
header: 'Downloaded',
|
||||||
|
id: 'downloaded',
|
||||||
|
cell: ({ value }) => new Date(value).toLocaleDateString()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
const tableModel = table.createViewModel(columns)
|
||||||
|
|
||||||
|
const { headerRows, pageRows, tableAttrs, tableBodyAttrs } = tableModel
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='rounded-md border max-w-screen-xl h-full overflow-clip contain-strict'>
|
||||||
|
<Table.Root {...$tableAttrs} class='max-h-full'>
|
||||||
|
<Table.Header class='px-5'>
|
||||||
|
{#each $headerRows as headerRow, i (i)}
|
||||||
|
<Subscribe rowAttrs={headerRow.attrs()}>
|
||||||
|
<Table.Row class='sticky top-0 bg-black z-[2]'>
|
||||||
|
{#each headerRow.cells as cell (cell.id)}
|
||||||
|
<Subscribe
|
||||||
|
attrs={cell.attrs()}
|
||||||
|
props={cell.props()}
|
||||||
|
let:attrs
|
||||||
|
let:props>
|
||||||
|
<Table.Head {...attrs} class={cn('px-0 first:pl-2 h-12 last:pr-2', cell.id === 'name' && 'w-full')}>
|
||||||
|
{#if cell.id !== 'flags'}
|
||||||
|
<Columnheader {props}>
|
||||||
|
<Render of={cell.render()} />
|
||||||
|
</Columnheader>
|
||||||
|
{:else}
|
||||||
|
<div class='text-sm px-4'>
|
||||||
|
<Render of={cell.render()} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Table.Head>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body {...$tableBodyAttrs} class='max-h-full overflow-y-scroll'>
|
||||||
|
{#if $pageRows.length}
|
||||||
|
{#each $pageRows as row (row.id)}
|
||||||
|
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
||||||
|
<Table.Row {...rowAttrs} class='h-12'>
|
||||||
|
{#each row.cells as cell (cell.id)}
|
||||||
|
<Subscribe attrs={cell.attrs()} let:attrs>
|
||||||
|
<Table.Cell {...attrs} class={cn('px-4 h-14 first:pl-6 last:pr-6 text-nowrap', (cell.id === 'downloaded' || cell.id === 'episode') && 'text-muted-foreground')}>
|
||||||
|
<Render of={cell.render()} />
|
||||||
|
</Table.Cell>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={columns.length} class='h-40 text-center'>
|
||||||
|
No torrents downloaded yet.
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
210
src/lib/components/ui/torrentclient/overview.svelte
Normal file
210
src/lib/components/ui/torrentclient/overview.svelte
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import Clock from 'lucide-svelte/icons/clock'
|
||||||
|
import ClockFading from 'lucide-svelte/icons/clock-fading'
|
||||||
|
import Download from 'lucide-svelte/icons/download'
|
||||||
|
import HardDrive from 'lucide-svelte/icons/hard-drive'
|
||||||
|
import HardDriveDownload from 'lucide-svelte/icons/hard-drive-download'
|
||||||
|
import Link from 'lucide-svelte/icons/link'
|
||||||
|
import Network from 'lucide-svelte/icons/network'
|
||||||
|
import Puzzle from 'lucide-svelte/icons/puzzle'
|
||||||
|
import Timer from 'lucide-svelte/icons/timer'
|
||||||
|
import Upload from 'lucide-svelte/icons/upload'
|
||||||
|
import UserRoundMinus from 'lucide-svelte/icons/user-round-minus'
|
||||||
|
import UserRoundPlus from 'lucide-svelte/icons/user-round-plus'
|
||||||
|
import Users from 'lucide-svelte/icons/users'
|
||||||
|
import Wifi from 'lucide-svelte/icons/wifi'
|
||||||
|
|
||||||
|
import Status from './status.svelte'
|
||||||
|
|
||||||
|
import { server } from '$lib/modules/torrent'
|
||||||
|
import { fastPrettyBits, fastPrettyBytes, eta, safeLocalStorage } from '$lib/utils'
|
||||||
|
|
||||||
|
const stats = server.stats
|
||||||
|
const protocol = server.protocol
|
||||||
|
|
||||||
|
$: torrent = $stats
|
||||||
|
|
||||||
|
$: protocols = $protocol
|
||||||
|
|
||||||
|
$: completed = torrent.progress === 1
|
||||||
|
|
||||||
|
const forwarding = safeLocalStorage<boolean>('torrent-port-forwarding') ?? false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='max-w-6xl flex flex-col gap-12'>
|
||||||
|
<div class='flex items-center gap-4'>
|
||||||
|
<div class='flex-1 w-full'>
|
||||||
|
<h1 class='text-2xl font-bold truncate text-nowrap'>{torrent.name || 'No Name Provided'}</h1>
|
||||||
|
<div class='flex items-center gap-2 mt-2'>
|
||||||
|
<div class='rounded-full px-2.5 py-0.5 text-xs font-bold text-primary-foreground {completed ? 'bg-blue-500 hover:bg-blue-500' : 'bg-green-500 hover:bg-green-500'}'>
|
||||||
|
{completed ? 'Seeding' : 'Downloading'}
|
||||||
|
</div>
|
||||||
|
<span class='text-sm text-muted-foreground'>{torrent.hash}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='flex gap-2' />
|
||||||
|
</div>
|
||||||
|
<div class='space-y-4'>
|
||||||
|
<div class='flex items-center justify-between'>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<HardDriveDownload class='w-5 h-5 mr-1.5' />
|
||||||
|
<span class='text-2xl font-bold'>Progress</span>
|
||||||
|
</div>
|
||||||
|
<span class='text-2xl font-bold'>{(torrent.progress * 100).toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div class='relative w-full overflow-clip rounded-full bg-secondary h-3'>
|
||||||
|
<div class='h-full w-full bg-primary transition-transform transform-gpu' style:--tw-translate-x='{(torrent.progress * 100) - 100}%' />
|
||||||
|
</div>
|
||||||
|
<div class='grid grid-cols-2 md:grid-cols-4 gap-4 text-sm'>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<Download class='w-4 h-4 text-green-500 mx-1' />
|
||||||
|
<div>
|
||||||
|
<span class='text-muted-foreground'>Downloaded</span>
|
||||||
|
<div class='font-medium'>{fastPrettyBytes(torrent.size.downloaded)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<Upload class='w-4 h-4 text-blue-500 mx-1' />
|
||||||
|
<div>
|
||||||
|
<span class='text-muted-foreground'>Uploaded</span>
|
||||||
|
<div class='font-medium'>{fastPrettyBytes(torrent.size.uploaded)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<HardDrive class='w-4 h-4 text-gray-500 mx-1' />
|
||||||
|
<div>
|
||||||
|
<span class='text-muted-foreground'>Total Size</span>
|
||||||
|
<div class='font-medium'>{fastPrettyBytes(torrent.size.total)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<Puzzle class='w-4 h-4 text-gray-500 mx-1' />
|
||||||
|
<div>
|
||||||
|
<span class='text-muted-foreground'>Pieces</span>
|
||||||
|
<div class='font-medium'>{torrent.pieces.total} <span class='text-muted-foreground'>×</span> {fastPrettyBytes(torrent.pieces.size)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='grid grid-cols-1 xl:grid-cols-3 gap-x-12'>
|
||||||
|
<div>
|
||||||
|
<div class='flex flex-col space-y-1.5 py-6'>
|
||||||
|
<h3 class='text-2xl font-bold leading-none flex items-center gap-2'>
|
||||||
|
<Wifi class='w-5 h-5 mr-1.5' />
|
||||||
|
Speed & Transfer
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class='space-y-4'>
|
||||||
|
<div class='grid grid-cols-2 gap-4'>
|
||||||
|
<div class='space-y-2'>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<Download class='w-4 h-4 text-green-500' />
|
||||||
|
<span class='text-sm font-medium text-muted-foreground'>Download</span>
|
||||||
|
</div>
|
||||||
|
<div class='text-2xl font-bold'>{fastPrettyBits(torrent.speed.down * 8)}/s</div>
|
||||||
|
</div>
|
||||||
|
<div class='space-y-2'>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<Upload class='w-4 h-4 text-blue-500' />
|
||||||
|
<span class='text-sm font-medium text-muted-foreground'>Upload</span>
|
||||||
|
</div>
|
||||||
|
<div class='text-2xl font-bold'>{fastPrettyBits(torrent.speed.up * 8)}/s</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class='flex flex-col space-y-1.5 py-6'>
|
||||||
|
<h3 class='text-2xl font-bold leading-none flex items-center gap-2'>
|
||||||
|
<Clock class='w-5 h-5 mr-1.5' />
|
||||||
|
Time Information
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class='space-y-4'>
|
||||||
|
<div class='grid grid-cols-2 gap-4'>
|
||||||
|
<div class='space-y-2'>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<ClockFading class='w-4 h-4 text-orange-500' />
|
||||||
|
<span class='text-sm font-medium text-muted-foreground'>Remaining</span>
|
||||||
|
</div>
|
||||||
|
<div class='text-2xl font-bold'>{eta(torrent.time.remaining)}</div>
|
||||||
|
</div>
|
||||||
|
<div class='space-y-2'>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<Timer class='w-4 h-4 text-purple-500' />
|
||||||
|
<span class='text-sm font-medium text-muted-foreground'>Elapsed</span>
|
||||||
|
</div>
|
||||||
|
<div class='text-2xl font-bold'>{eta(torrent.time.elapsed)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class='flex flex-col space-y-1.5 py-6'>
|
||||||
|
<h3 class='text-2xl font-bold leading-none flex items-center gap-2'>
|
||||||
|
<Users class='w-5 h-5 mr-1.5' />
|
||||||
|
Peers & Connections
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class='space-y-4'>
|
||||||
|
<div class='grid grid-cols-3 gap-4'>
|
||||||
|
<div class='space-y-2'>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<UserRoundPlus class='w-4 h-4 text-green-500' />
|
||||||
|
<span class='text-sm font-medium text-muted-foreground'>Seeders</span>
|
||||||
|
</div>
|
||||||
|
<div class='text-2xl font-bold'>{torrent.peers.seeders}</div>
|
||||||
|
</div>
|
||||||
|
<div class='space-y-2'>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<UserRoundMinus class='w-4 h-4 text-blue-500' />
|
||||||
|
<span class='text-sm font-medium text-muted-foreground'>Leechers</span>
|
||||||
|
</div>
|
||||||
|
<div class='text-2xl font-bold'>{torrent.peers.leechers}</div>
|
||||||
|
</div>
|
||||||
|
<div class='space-y-2'>
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<Link class='w-4 h-4 text-purple-500' />
|
||||||
|
<span class='text-sm font-medium text-muted-foreground'>Wires</span>
|
||||||
|
</div>
|
||||||
|
<div class='text-2xl font-bold'>{torrent.peers.wires}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class='flex flex-col space-y-1.5 py-6'>
|
||||||
|
<h3 class='text-2xl font-bold leading-none flex items-center gap-2'>
|
||||||
|
<Network class='w-5 h-5 mr-1.5' />
|
||||||
|
Protocol Status
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class='py-6 pt-0'>
|
||||||
|
<div class='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12'>
|
||||||
|
<div class='space-y-4'>
|
||||||
|
<h4 class='font-medium'>Network Discovery</h4>
|
||||||
|
<div class='space-y-3'>
|
||||||
|
<Status enabled={protocols.dht} title='DHT' description='Distributed Hash Table for peer discovery' />
|
||||||
|
<Status enabled={protocols.lsd} title='LSD' description='Local Service Discovery on network' />
|
||||||
|
<Status enabled={protocols.pex} title='PEX' description='Peer Exchange with other clients' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='space-y-4'>
|
||||||
|
<h4 class='font-medium'>Connection</h4>
|
||||||
|
<div class='space-y-3'>
|
||||||
|
<Status enabled={protocols.nat} title='NAT' description='NAT-PMP/UPnP automatic forwarding' />
|
||||||
|
<Status enabled={forwarding || protocols.forwarding} title='Forwarding' description='Accepting inbound connections' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='space-y-4'>
|
||||||
|
<h4 class='font-medium'>Storage</h4>
|
||||||
|
<div class='space-y-3'>
|
||||||
|
<Status enabled={protocols.persisting} title='Persisting' description='Storing all torrents' />
|
||||||
|
<Status enabled={protocols.streaming} title='Streaming' description='Downloading only required pieces' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { lookup } from '$lib/modules/geoip'
|
||||||
|
import { codeToEmoji } from '$lib/utils'
|
||||||
|
|
||||||
|
export let value: string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='flex gap-x-2'>
|
||||||
|
{#await lookup(value) then location}
|
||||||
|
<div class='font-twemoji text-xl leading-none content-center line-clamp-1'>
|
||||||
|
{codeToEmoji(location.country)}
|
||||||
|
</div>
|
||||||
|
<div class='text-muted-foreground'>
|
||||||
|
{location.country}
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
48
src/lib/components/ui/torrentclient/peers/cells/flags.svelte
Normal file
48
src/lib/components/ui/torrentclient/peers/cells/flags.svelte
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import Lock from 'lucide-svelte/icons/lock'
|
||||||
|
import Shield from 'lucide-svelte/icons/shield'
|
||||||
|
import Wifi from 'lucide-svelte/icons/wifi'
|
||||||
|
import WifiOff from 'lucide-svelte/icons/wifi-off'
|
||||||
|
|
||||||
|
import { Badge } from '$lib/components/ui/badge'
|
||||||
|
import * as Tooltip from '$lib/components/ui/tooltip'
|
||||||
|
|
||||||
|
export let value: Array<'incoming' | 'outgoing' | 'utp' | 'encrypted'>
|
||||||
|
|
||||||
|
const badgeVariants: Record<typeof value[number], string> = {
|
||||||
|
incoming: 'text-green-500',
|
||||||
|
outgoing: 'text-blue-500',
|
||||||
|
utp: 'text-purple-500',
|
||||||
|
encrypted: 'text-yellow-500'
|
||||||
|
}
|
||||||
|
const labels: Record<typeof value[number], string> = {
|
||||||
|
incoming: 'Incoming',
|
||||||
|
outgoing: 'Outgoing',
|
||||||
|
utp: 'uTP',
|
||||||
|
encrypted: 'Encrypted'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='flex gap-x-2'>
|
||||||
|
{#each value as flag (flag)}
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger>
|
||||||
|
<Badge variant='secondary' class='p-1 {badgeVariants[flag]}'>
|
||||||
|
{#if flag === 'incoming'}
|
||||||
|
<Wifi class='size-3' />
|
||||||
|
{:else if flag === 'outgoing'}
|
||||||
|
<WifiOff class='size-3' />
|
||||||
|
{:else if flag === 'utp'}
|
||||||
|
<Shield class='size-3' />
|
||||||
|
{:else if flag === 'encrypted'}
|
||||||
|
<Lock class='size-3' />
|
||||||
|
{/if}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Content>
|
||||||
|
<p class='font-semibold'>{labels[flag]}</p>
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Root>
|
||||||
|
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
5
src/lib/components/ui/torrentclient/peers/cells/index.ts
Normal file
5
src/lib/components/ui/torrentclient/peers/cells/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export { default as CountryCell } from './country.svelte'
|
||||||
|
export { default as IpCell } from './ip.svelte'
|
||||||
|
export { default as SpeedCell } from './speed.svelte'
|
||||||
|
export { default as ProgressCell } from './progress.svelte'
|
||||||
|
export { default as FlagsCell } from './flags.svelte'
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
export let ip: string
|
||||||
|
export let seeder: boolean
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='flex gap-x-2 font-mono items-center text-xs'>
|
||||||
|
<div class='w-2 h-2 rounded-full flex-shrink-0 {seeder ? 'bg-green-500' : 'bg-blue-500'}' />{ip}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
export let value: number
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='min-w-16 w-full overflow-clip rounded-full bg-secondary h-1.5 mt-1.5'>
|
||||||
|
<div class='h-full w-full bg-primary transition-transform transform-gpu' style:--tw-translate-x='{(value * 100) - 100}%' />
|
||||||
|
</div>
|
||||||
|
<div class='text-xs mt-1 text-muted-foreground'>{(value * 100).toFixed(1)}%</div>
|
||||||
18
src/lib/components/ui/torrentclient/peers/cells/speed.svelte
Normal file
18
src/lib/components/ui/torrentclient/peers/cells/speed.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import Download from 'lucide-svelte/icons/download'
|
||||||
|
import Upload from 'lucide-svelte/icons/upload'
|
||||||
|
|
||||||
|
import { fastPrettyBits } from '$lib/utils'
|
||||||
|
|
||||||
|
export let value: number
|
||||||
|
export let type: 'upload' | 'download'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='flex gap-x-2 items-center'>
|
||||||
|
{#if type === 'download'}
|
||||||
|
<Download class='size-3 text-green-500 mr-0.5' />
|
||||||
|
{:else}
|
||||||
|
<Upload class='size-3 text-blue-500 mr-0.5' />
|
||||||
|
{/if}
|
||||||
|
{fastPrettyBits(value * 8) + '/s'}
|
||||||
|
</div>
|
||||||
136
src/lib/components/ui/torrentclient/peers/table.svelte
Normal file
136
src/lib/components/ui/torrentclient/peers/table.svelte
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table'
|
||||||
|
import { addSortBy } from 'svelte-headless-table/plugins'
|
||||||
|
|
||||||
|
import Columnheader from '../columnheader.svelte'
|
||||||
|
|
||||||
|
import { CountryCell, FlagsCell, IpCell, ProgressCell, SpeedCell } from './cells'
|
||||||
|
|
||||||
|
import * as Table from '$lib/components/ui/table'
|
||||||
|
import { server } from '$lib/modules/torrent'
|
||||||
|
import { cn, eta, fastPrettyBytes } from '$lib/utils'
|
||||||
|
|
||||||
|
const table = createTable(server.peers, {
|
||||||
|
sort: addSortBy({ toggleOrder: ['asc', 'desc'] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = table.createColumns([
|
||||||
|
table.column({
|
||||||
|
accessor: 'ip',
|
||||||
|
header: 'IP Address',
|
||||||
|
id: 'ip',
|
||||||
|
cell: ({ value, row }) => {
|
||||||
|
// @ts-expect-error bad typedefs
|
||||||
|
return createRender(IpCell, { ip: value, seeder: row.original.seeder })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
table.column({ accessor: 'client', header: 'Client', id: 'client' }),
|
||||||
|
table.column({
|
||||||
|
accessor: 'progress',
|
||||||
|
header: 'Progress',
|
||||||
|
id: 'progress',
|
||||||
|
cell: ({ value }) => createRender(ProgressCell, { value })
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: row => row.speed.down,
|
||||||
|
header: 'Download',
|
||||||
|
id: 'down',
|
||||||
|
cell: ({ value }) => createRender(SpeedCell, { value, type: 'download' })
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: row => row.speed.up,
|
||||||
|
header: 'Upload',
|
||||||
|
id: 'up',
|
||||||
|
cell: ({ value }) => createRender(SpeedCell, { value, type: 'upload' })
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: row => row.size.downloaded,
|
||||||
|
header: 'Downloaded',
|
||||||
|
id: 'downloaded',
|
||||||
|
cell: ({ value }) => fastPrettyBytes(value)
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: row => row.size.uploaded,
|
||||||
|
header: 'Uploaded',
|
||||||
|
id: 'uploaded',
|
||||||
|
cell: ({ value }) => fastPrettyBytes(value)
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: 'time',
|
||||||
|
header: 'Time',
|
||||||
|
id: 'time',
|
||||||
|
cell: ({ value }) => eta(value)
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: 'ip',
|
||||||
|
header: 'Country',
|
||||||
|
id: 'country',
|
||||||
|
cell: ({ value }) => createRender(CountryCell, { value })
|
||||||
|
}),
|
||||||
|
table.column({
|
||||||
|
accessor: 'flags',
|
||||||
|
header: 'Flags',
|
||||||
|
id: 'flags',
|
||||||
|
cell: ({ value }) => createRender(FlagsCell, { value })
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
const tableModel = table.createViewModel(columns)
|
||||||
|
|
||||||
|
const { headerRows, pageRows, tableAttrs, tableBodyAttrs } = tableModel
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='rounded-md border max-w-screen-xl h-full overflow-clip contain-strict'>
|
||||||
|
<Table.Root {...$tableAttrs} class='max-h-full'>
|
||||||
|
<Table.Header class='px-5'>
|
||||||
|
{#each $headerRows as headerRow, i (i)}
|
||||||
|
<Subscribe rowAttrs={headerRow.attrs()}>
|
||||||
|
<Table.Row class='sticky top-0 bg-black z-[2]'>
|
||||||
|
{#each headerRow.cells as cell (cell.id)}
|
||||||
|
<Subscribe
|
||||||
|
attrs={cell.attrs()}
|
||||||
|
props={cell.props()}
|
||||||
|
let:attrs
|
||||||
|
let:props>
|
||||||
|
<Table.Head {...attrs} class={cn('px-0 first:pl-2 h-12 last:pr-2', cell.id === 'progress' && 'w-full')}>
|
||||||
|
{#if cell.id !== 'flags'}
|
||||||
|
<Columnheader {props}>
|
||||||
|
<Render of={cell.render()} />
|
||||||
|
</Columnheader>
|
||||||
|
{:else}
|
||||||
|
<div class='text-sm px-4'>
|
||||||
|
<Render of={cell.render()} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Table.Head>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body {...$tableBodyAttrs} class='max-h-full overflow-y-scroll'>
|
||||||
|
{#if $pageRows.length}
|
||||||
|
{#each $pageRows as row (row.id)}
|
||||||
|
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
||||||
|
<Table.Row {...rowAttrs} class='h-12'>
|
||||||
|
{#each row.cells as cell (cell.id)}
|
||||||
|
<Subscribe attrs={cell.attrs()} let:attrs>
|
||||||
|
<Table.Cell {...attrs} class={cn('px-4 h-14 first:pl-6 last:pr-6 text-nowrap', cell.id === 'time' && 'text-muted-foreground')}>
|
||||||
|
<Render of={cell.render()} />
|
||||||
|
</Table.Cell>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
</Subscribe>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={columns.length} class='h-40 text-center'>
|
||||||
|
No peers connected yet.
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
13
src/lib/components/ui/torrentclient/status.svelte
Normal file
13
src/lib/components/ui/torrentclient/status.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
export let enabled = false
|
||||||
|
export let title = ''
|
||||||
|
export let description = ''
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='flex items-center gap-2'>
|
||||||
|
<div class='w-2 h-2 rounded-full {enabled ? 'bg-green-500' : 'bg-red-500' }' />
|
||||||
|
<div class='mx-1'>
|
||||||
|
<span class='text-sm'>{title}</span>
|
||||||
|
<p class='text-xs text-muted-foreground'>{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
71
src/lib/modules/geoip/index.ts
Normal file
71
src/lib/modules/geoip/index.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import params from 'doc999tor-fast-geoip/build/params.js'
|
||||||
|
|
||||||
|
import { binarySearch, firstArrayItem, getNextIp, identity, ipStr2Num, type Format, type indexFile, type ipBlockRecord, type locationRecord } from './utils.ts'
|
||||||
|
|
||||||
|
const MASK = ipStr2Num('255.255.255.255')
|
||||||
|
|
||||||
|
const viteFixedImportWeTrullyHateViteWithAPassionForNotSupportingBasicFeatures = async <T>(path: string) => {
|
||||||
|
// previously: return import('doc999tor-fast-geoip/data/locations.json', { with: { type: 'json' } })
|
||||||
|
const res = await fetch(`/geoip/${path}.json`)
|
||||||
|
return await res.json() as T
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipCache: Record<string, Format> = {}
|
||||||
|
const locationCache = viteFixedImportWeTrullyHateViteWithAPassionForNotSupportingBasicFeatures<locationRecord[]>('locations')
|
||||||
|
|
||||||
|
async function readFile<T extends Format> (filename: string): Promise<T> {
|
||||||
|
if (ipCache[filename] !== undefined) {
|
||||||
|
return await Promise.resolve(ipCache[filename] as T)
|
||||||
|
}
|
||||||
|
const content = await viteFixedImportWeTrullyHateViteWithAPassionForNotSupportingBasicFeatures<T>(filename)
|
||||||
|
ipCache[filename] = content
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IpInfo {
|
||||||
|
range: [number, number]
|
||||||
|
country: string
|
||||||
|
region: string
|
||||||
|
eu: '0'|'1'
|
||||||
|
timezone: string
|
||||||
|
city: string
|
||||||
|
ll: [number, number]
|
||||||
|
metro: number
|
||||||
|
area: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function lookup (stringifiedIp: string): Promise<IpInfo> {
|
||||||
|
const ip = ipStr2Num(stringifiedIp)
|
||||||
|
const data = await readFile<indexFile>('index')
|
||||||
|
|
||||||
|
// IP cannot be NaN
|
||||||
|
if (Number.isNaN(ip)) throw new Error('IP cannot be NaN')
|
||||||
|
const rootIndex = binarySearch(data, ip, identity)
|
||||||
|
// Ip is not in the database, return empty object
|
||||||
|
if (rootIndex === -1) throw new Error('IP not found in the database')
|
||||||
|
|
||||||
|
let nextIp = getNextIp(data, rootIndex, MASK, identity)
|
||||||
|
const data2 = await readFile<indexFile>('i' + rootIndex)
|
||||||
|
const index = binarySearch(data2, ip, identity) + rootIndex * params.NUMBER_NODES_PER_MIDINDEX
|
||||||
|
nextIp = getNextIp(data2, index, nextIp, identity)
|
||||||
|
const data3 = await readFile<ipBlockRecord[]>('' + index)
|
||||||
|
const index1 = binarySearch(data3, ip, firstArrayItem)
|
||||||
|
const ipData = data3[index1]!
|
||||||
|
|
||||||
|
if (!ipData[1]) throw new Error("IP doesn't any region nor country associated")
|
||||||
|
|
||||||
|
nextIp = getNextIp<ipBlockRecord>(data3, index1, nextIp, firstArrayItem)
|
||||||
|
const location = (await locationCache)[ipData[1]]!
|
||||||
|
|
||||||
|
return {
|
||||||
|
range: [ipData[0], nextIp] as [number, number],
|
||||||
|
country: location[0],
|
||||||
|
region: location[1],
|
||||||
|
eu: location[5],
|
||||||
|
timezone: location[4],
|
||||||
|
city: location[2],
|
||||||
|
ll: [ipData[2], ipData[3]] as [number, number],
|
||||||
|
metro: location[3],
|
||||||
|
area: ipData[4]
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/lib/modules/geoip/utils.ts
Normal file
49
src/lib/modules/geoip/utils.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
type extractKeyFunction<recordType> = (record: recordType) => number
|
||||||
|
export type indexFile = number[]
|
||||||
|
export type ipBlockRecord = [number, number|null, number, number, number]
|
||||||
|
|
||||||
|
export type Format = indexFile | ipBlockRecord[] | locationRecord[]
|
||||||
|
|
||||||
|
export type locationRecord = [string, string, string, number, string, '0' | '1']
|
||||||
|
|
||||||
|
export function identity (item: number): number {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
export function binarySearch<recordType> (list: recordType[], item: number, extractKey: extractKeyFunction<recordType>): number {
|
||||||
|
let low = 0
|
||||||
|
let high = list.length - 1
|
||||||
|
while (true) {
|
||||||
|
const i = Math.round((high - low) / 2) + low
|
||||||
|
if (item < extractKey(list[i]!)) {
|
||||||
|
if (i === high && i === low) {
|
||||||
|
return -1 // Item is lower than the first item
|
||||||
|
} else if (i === high) {
|
||||||
|
high = low
|
||||||
|
} else {
|
||||||
|
high = i
|
||||||
|
}
|
||||||
|
} else if (item >= extractKey(list[i]!) && (i === (list.length - 1) || item < extractKey(list[i + 1]!))) {
|
||||||
|
return i
|
||||||
|
} else {
|
||||||
|
low = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ipStr2Num (stringifiedIp: string): number {
|
||||||
|
return stringifiedIp.split('.')
|
||||||
|
.map(e => parseInt(e)).reduce((acc, val, index) => acc + val * Math.pow(256, 3 - index), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function firstArrayItem (item: ipBlockRecord): number {
|
||||||
|
return item[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextIp<recordType = number> (data: recordType[], index: number, currentNextIp: number, extractKey: extractKeyFunction<recordType>): number {
|
||||||
|
if (index < (data.length - 1)) {
|
||||||
|
return extractKey(data[index + 1]!)
|
||||||
|
} else {
|
||||||
|
return currentNextIp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,29 @@ const dummyFiles = [
|
||||||
// id: 1
|
// id: 1
|
||||||
// }
|
// }
|
||||||
]
|
]
|
||||||
|
// function makeRandomPeer (): PeerInfo {
|
||||||
|
// const ip = `${rnd(256)}.${rnd(256)}.${rnd(256)}.${rnd(256)}:${rnd(65536)}`
|
||||||
|
// return {
|
||||||
|
// ip,
|
||||||
|
// seeder: Math.random() < 0.5,
|
||||||
|
// client: ['qBittorrent 4.5.4', 'WebTorrent 1.0.0', 'Transmission 3.00', 'Deluge 2.1.1', 'μTorrent 3.5.5', 'Vuze 5.7.7.0', 'Azureus 5.7.6.0'].sort(() => Math.random() - 0.5)[0]!,
|
||||||
|
// progress: Math.random(),
|
||||||
|
// size: {
|
||||||
|
// downloaded: rnd(1000000),
|
||||||
|
// uploaded: rnd(1000000)
|
||||||
|
// },
|
||||||
|
// speed: {
|
||||||
|
// down: rnd(1000),
|
||||||
|
// up: rnd(1000)
|
||||||
|
// },
|
||||||
|
// time: rnd(1000),
|
||||||
|
// flags: (['encrypted', 'utp', 'incoming', 'outgoing'] as const).filter(() => Math.random() < 0.5).slice(0, 3)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// const dummyPeerInfo: PeerInfo[] = []
|
||||||
|
// for (let i = 0; i < 100; i++) {
|
||||||
|
// dummyPeerInfo.push(makeRandomPeer())
|
||||||
|
// }
|
||||||
|
|
||||||
export default Object.assign<Native, Partial<Native>>({
|
export default Object.assign<Native, Partial<Native>>({
|
||||||
authAL: (url: string) => {
|
authAL: (url: string) => {
|
||||||
|
|
@ -80,7 +103,7 @@ export default Object.assign<Native, Partial<Native>>({
|
||||||
{ start: 1.0 * 60 * 1000, end: 1.2 * 60 * 1000, text: 'Chapter 1' },
|
{ start: 1.0 * 60 * 1000, end: 1.2 * 60 * 1000, text: 'Chapter 1' },
|
||||||
{ start: 1.4 * 60 * 1000, end: 88 * 1000, text: 'Chapter 2 ' }
|
{ start: 1.4 * 60 * 1000, end: 88 * 1000, text: 'Chapter 2 ' }
|
||||||
],
|
],
|
||||||
version: async () => 'v6.3.0',
|
version: async () => 'v6.4.0',
|
||||||
updateSettings: async () => undefined,
|
updateSettings: async () => undefined,
|
||||||
setDOH: async () => undefined,
|
setDOH: async () => undefined,
|
||||||
cachedTorrents: async () => ['40a9047de61859035659e449d7b84286934486b0'],
|
cachedTorrents: async () => ['40a9047de61859035659e449d7b84286934486b0'],
|
||||||
|
|
@ -88,12 +111,30 @@ export default Object.assign<Native, Partial<Native>>({
|
||||||
setHideToTray: async () => undefined,
|
setHideToTray: async () => undefined,
|
||||||
transparency: async () => undefined,
|
transparency: async () => undefined,
|
||||||
setZoom: async () => undefined,
|
setZoom: async () => undefined,
|
||||||
// @ts-expect-error yeah
|
navigate: async () => undefined,
|
||||||
navigate: async (cb) => { globalThis.___navigate = cb },
|
|
||||||
downloadProgress: async () => undefined,
|
downloadProgress: async () => undefined,
|
||||||
updateProgress: async () => undefined,
|
updateProgress: async () => undefined,
|
||||||
torrentStats: async (): Promise<TorrentInfo> => ({ peers: rnd(), seeders: rnd(), leechers: rnd(), progress: Math.random(), down: rnd(100000000), up: rnd(100000000), name: 'Amebku.webm', downloaded: rnd(100000), hash: '1234567890abcdef', size: 1234567890, eta: rnd() }),
|
torrentInfo: async (): Promise<TorrentInfo> => ({
|
||||||
torrents: async (): Promise<TorrentInfo[]> => [{ peers: rnd(), seeders: rnd(), leechers: rnd(), progress: Math.random(), down: rnd(100000000), up: rnd(100000000), name: 'Amebku.webm', downloaded: rnd(100000), hash: '1234567890abcdef', size: 1234567890, eta: rnd() }],
|
name: '',
|
||||||
|
progress: 0,
|
||||||
|
size: { total: 0, downloaded: 0, uploaded: 0 },
|
||||||
|
speed: { down: 0, up: 0 },
|
||||||
|
time: { remaining: 0, elapsed: 0 },
|
||||||
|
peers: { seeders: 0, leechers: 0, wires: 0 },
|
||||||
|
pieces: { total: 0, size: 0 },
|
||||||
|
hash: ''
|
||||||
|
}),
|
||||||
|
fileInfo: async () => [],
|
||||||
|
peerInfo: async () => [],
|
||||||
|
protocolStatus: async () => ({
|
||||||
|
dht: false,
|
||||||
|
lsd: false,
|
||||||
|
pex: false,
|
||||||
|
nat: false,
|
||||||
|
forwarding: false,
|
||||||
|
persisting: false,
|
||||||
|
streaming: false
|
||||||
|
}),
|
||||||
defaultTransparency: () => false,
|
defaultTransparency: () => false,
|
||||||
errors: async () => undefined,
|
errors: async () => undefined,
|
||||||
debug: async () => undefined
|
debug: async () => undefined
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,72 @@ import { persisted } from 'svelte-persisted-store'
|
||||||
import native from '../native'
|
import native from '../native'
|
||||||
import { w2globby } from '../w2g/lobby'
|
import { w2globby } from '../w2g/lobby'
|
||||||
|
|
||||||
import type { TorrentFile, TorrentInfo } from '../../../app'
|
import type { FileInfo, PeerInfo, TorrentFile, TorrentInfo } from '$lib/../app'
|
||||||
import type { Media } from '../anilist'
|
import type { Media } from '../anilist'
|
||||||
|
|
||||||
|
const defaultTorrentInfo: TorrentInfo = {
|
||||||
|
name: '',
|
||||||
|
progress: 0,
|
||||||
|
size: { total: 0, downloaded: 0, uploaded: 0 },
|
||||||
|
speed: { down: 0, up: 0 },
|
||||||
|
time: { remaining: 0, elapsed: 0 },
|
||||||
|
peers: { seeders: 0, leechers: 0, wires: 0 },
|
||||||
|
pieces: { total: 0, size: 0 },
|
||||||
|
hash: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProtocolStatus = { dht: false, lsd: false, pex: false, nat: false, forwarding: false, persisting: false, streaming: false }
|
||||||
|
|
||||||
export const server = new class ServerClient {
|
export const server = new class ServerClient {
|
||||||
last = persisted<{media: Media, id: string, episode: number} | null>('last-torrent', null)
|
last = persisted<{media: Media, id: string, episode: number} | null>('last-torrent', null)
|
||||||
active = writable<Promise<{media: Media, id: string, episode: number, files: TorrentFile[]}| null>>()
|
active = writable<Promise<{media: Media, id: string, episode: number, files: TorrentFile[]}| null>>()
|
||||||
downloaded = writable(this.cachedSet())
|
downloaded = writable(this.cachedSet())
|
||||||
|
|
||||||
stats = readable<TorrentInfo>({ peers: 0, down: 0, up: 0, progress: 0, downloaded: 0, eta: 0, hash: '', leechers: 0, name: '', seeders: 0, size: 0 }, set => {
|
stats = readable(defaultTorrentInfo, set => {
|
||||||
let listener = 0
|
let listener = 0
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
const id = (await get(this.active))?.id
|
const id = (await get(this.active))?.id
|
||||||
if (id) set(await native.torrentStats(id))
|
if (id) set(await native.torrentInfo(id))
|
||||||
listener = setTimeout(update, 1000)
|
listener = setTimeout(update, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
update()
|
update()
|
||||||
return () => clearTimeout(listener)
|
return () => clearTimeout(listener)
|
||||||
})
|
})
|
||||||
|
|
||||||
list = readable<TorrentInfo[]>([], set => {
|
protocol = readable(defaultProtocolStatus, set => {
|
||||||
let listener = 0
|
let listener = 0
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
set(await native.torrents())
|
const id = (await get(this.active))?.id
|
||||||
listener = setTimeout(update, 1000)
|
if (id) set(await native.protocolStatus(id))
|
||||||
|
listener = setTimeout(update, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
update()
|
||||||
|
return () => clearTimeout(listener)
|
||||||
|
})
|
||||||
|
|
||||||
|
peers = readable<PeerInfo[]>([], set => {
|
||||||
|
let listener = 0
|
||||||
|
|
||||||
|
const update = async () => {
|
||||||
|
const id = (await get(this.active))?.id
|
||||||
|
if (id) set(await native.peerInfo(id))
|
||||||
|
listener = setTimeout(update, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
update()
|
||||||
|
return () => clearTimeout(listener)
|
||||||
|
})
|
||||||
|
|
||||||
|
files = readable<FileInfo[]>([], set => {
|
||||||
|
let listener = 0
|
||||||
|
const update = async () => {
|
||||||
|
const id = (await get(this.active))?.id
|
||||||
|
if (id) set(await native.fileInfo(id))
|
||||||
|
listener = setTimeout(update, 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
update()
|
update()
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,42 @@ export function cn (...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MediaQuery<Query extends Record<string, string> = Record<string, string>> = {
|
||||||
|
[K in keyof Query]?: boolean | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateMedia (mqls: Record<string, MediaQueryList>) {
|
||||||
|
const media: MediaQuery = {}
|
||||||
|
for (const [key, query] of Object.entries(mqls)) {
|
||||||
|
media[key] = query.matches
|
||||||
|
}
|
||||||
|
return media
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQueries = {
|
||||||
|
sm: '(min-width: 640px)',
|
||||||
|
md: '(min-width: 768px)',
|
||||||
|
lg: '(min-width: 1024px)',
|
||||||
|
xl: '(min-width: 1280px)',
|
||||||
|
'2xl': '(min-width: 1536px)',
|
||||||
|
'3xl': '(min-width: 1920px)',
|
||||||
|
'4xl': '(min-width: 2560px)',
|
||||||
|
'5xl': '(min-width: 3840px)',
|
||||||
|
'6xl': '(min-width: 5120px)'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const breakpoints = readable<MediaQuery<typeof mediaQueries>>({}, set => {
|
||||||
|
const ctrl = new AbortController()
|
||||||
|
const mqls: Record<string, MediaQueryList> = {}
|
||||||
|
const updateMedia = () => set(calculateMedia(mqls))
|
||||||
|
for (const [key, query] of Object.entries(mediaQueries)) {
|
||||||
|
mqls[key] = window.matchMedia(query)
|
||||||
|
mqls[key].addEventListener('change', updateMedia, { signal: ctrl.signal })
|
||||||
|
}
|
||||||
|
updateMedia()
|
||||||
|
return () => ctrl.abort()
|
||||||
|
})
|
||||||
|
|
||||||
interface FlyAndScaleParams {
|
interface FlyAndScaleParams {
|
||||||
y?: number
|
y?: number
|
||||||
x?: number
|
x?: number
|
||||||
|
|
@ -89,15 +125,7 @@ export const debounce = <T extends (...args: any[]) => unknown>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mql = typeof matchMedia !== 'undefined' ? matchMedia('(min-width: 768px)') : null
|
|
||||||
export const isMobile = readable(!mql?.matches, set => {
|
|
||||||
const check: ({ matches }: { matches: boolean }) => void = ({ matches }) => set(!matches)
|
|
||||||
mql?.addEventListener('change', check)
|
|
||||||
return () => mql?.removeEventListener('change', check)
|
|
||||||
})
|
|
||||||
|
|
||||||
const formatter = new Intl.RelativeTimeFormat('en')
|
const formatter = new Intl.RelativeTimeFormat('en')
|
||||||
const formatterShort = new Intl.RelativeTimeFormat('en', { style: 'short' })
|
|
||||||
const ranges: Partial<Record<Intl.RelativeTimeFormatUnit, number>> = {
|
const ranges: Partial<Record<Intl.RelativeTimeFormatUnit, number>> = {
|
||||||
years: 3600 * 24 * 365,
|
years: 3600 * 24 * 365,
|
||||||
months: 3600 * 24 * 30,
|
months: 3600 * 24 * 30,
|
||||||
|
|
@ -119,16 +147,33 @@ export function since (date: Date) {
|
||||||
}
|
}
|
||||||
return 'now'
|
return 'now'
|
||||||
}
|
}
|
||||||
export function eta (date: Date) {
|
export function eta (seconds: number): string {
|
||||||
const secondsElapsed = (date.getTime() - Date.now()) / 1000
|
if (!Number.isFinite(seconds) || seconds < 0) return '0s'
|
||||||
for (const _key in ranges) {
|
|
||||||
const key = _key as Intl.RelativeTimeFormatUnit
|
const units = [
|
||||||
if ((ranges[key] ?? 0) < Math.abs(secondsElapsed)) {
|
{ label: 'y', secs: 31536000 },
|
||||||
const delta = secondsElapsed / (ranges[key] ?? 0)
|
{ label: 'mo', secs: 2592000 },
|
||||||
return formatterShort.format(Math.round(delta), key)
|
{ label: 'd', secs: 86400 },
|
||||||
|
{ label: 'h', secs: 3600 },
|
||||||
|
{ label: 'm', secs: 60 },
|
||||||
|
{ label: 's', secs: 1 }
|
||||||
|
]
|
||||||
|
|
||||||
|
let remaining = Math.floor(seconds)
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
|
for (const { label, secs } of units) {
|
||||||
|
if (remaining >= secs) {
|
||||||
|
const value = Math.floor(remaining / secs)
|
||||||
|
parts.push(`${value}${label}`)
|
||||||
|
remaining %= secs
|
||||||
|
// Only show up to two largest units (e.g., "1h 2m", "2m 3s")
|
||||||
|
if (parts.length === 2) break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 'now'
|
|
||||||
|
// If nothing matched, show "0s"
|
||||||
|
return parts.length ? parts.join(' ') : '0s'
|
||||||
}
|
}
|
||||||
const bytes = [' B', ' kB', ' MB', ' GB', ' TB']
|
const bytes = [' B', ' kB', ' MB', ' GB', ' TB']
|
||||||
export function fastPrettyBytes (num: number) {
|
export function fastPrettyBytes (num: number) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang='ts'>
|
<script lang='ts'>
|
||||||
import '../app.css'
|
import '../app.css'
|
||||||
import '@fontsource-variable/nunito'
|
import '@fontsource-variable/nunito'
|
||||||
|
import '@fontsource/geist-mono'
|
||||||
import '$lib/modules/navigate'
|
import '$lib/modules/navigate'
|
||||||
import { ProgressBar } from '@prgm/sveltekit-progress-bar'
|
import { ProgressBar } from '@prgm/sveltekit-progress-bar'
|
||||||
import { toast } from 'svelte-sonner'
|
import { toast } from 'svelte-sonner'
|
||||||
|
|
|
||||||
51
src/routes/app/client/+layout.svelte
Normal file
51
src/routes/app/client/+layout.svelte
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import SettingsNav from '$lib/components/SettingsNav.svelte'
|
||||||
|
import { Separator } from '$lib/components/ui/separator'
|
||||||
|
import { Globe } from '$lib/components/ui/torrentclient'
|
||||||
|
import { dragScroll } from '$lib/modules/navigate'
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: 'Overview',
|
||||||
|
href: '/app/client/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Files',
|
||||||
|
href: '/app/client/files/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Peers',
|
||||||
|
href: '/app/client/peers/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Library',
|
||||||
|
href: '/app/client/library/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Settings',
|
||||||
|
href: '/app/settings/client/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='space-y-6 p-10 pb-0 w-full h-full flex flex-col min-w-0'>
|
||||||
|
<div class='space-y-0.5'>
|
||||||
|
<h2 class='text-2xl font-bold'>Torrent Client</h2>
|
||||||
|
<p class='text-muted-foreground'>
|
||||||
|
Monitor your torrents, and configure settings for your torrent client.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Separator class='my-6' />
|
||||||
|
<div class='flex flex-col lg:flex-row gap-x-12 grow min-h-0'>
|
||||||
|
<aside class='lg:grow lg:max-w-60 flex flex-col'>
|
||||||
|
<SettingsNav {items} />
|
||||||
|
<div class='mt-auto text-xs text-muted-foreground px-4 sm:px-2 py-5 flex flex-row lg:flex-col font-light gap-0.5 gap-x-4 flex-wrap'>
|
||||||
|
<div>WebTorrent v2.6.8</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div class='flex-1 overflow-y-scroll' use:dragScroll>
|
||||||
|
<Globe />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -1,41 +1,7 @@
|
||||||
<script lang='ts'>
|
<script lang='ts'>
|
||||||
import * as Table from '$lib/components/ui/table'
|
import { Overview } from '$lib/components/ui/torrentclient'
|
||||||
import { dragScroll } from '$lib/modules/navigate'
|
|
||||||
import { server } from '$lib/modules/torrent'
|
|
||||||
import { fastPrettyBits, fastPrettyBytes, eta as _eta } from '$lib/utils'
|
|
||||||
|
|
||||||
const list = server.list
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class='flex flex-col items-center w-full h-full overflow-y-auto px-5 my-10' use:dragScroll>
|
<div class='flex flex-col h-full'>
|
||||||
<Table.Root>
|
<Overview />
|
||||||
<Table.Header>
|
|
||||||
<Table.Row class='[&>*]:p-4 [&>*]:font-bold'>
|
|
||||||
<Table.Head>Name</Table.Head>
|
|
||||||
<Table.Head class='w-[100px]'>Progress</Table.Head>
|
|
||||||
<Table.Head class='w-[100px]'>Size</Table.Head>
|
|
||||||
<Table.Head class='w-[100px]'>Done</Table.Head>
|
|
||||||
<Table.Head class='w-[110px]'>Download</Table.Head>
|
|
||||||
<Table.Head class='w-[110px]'>Upload</Table.Head>
|
|
||||||
<Table.Head class='w-[110px]'>ETA</Table.Head>
|
|
||||||
<Table.Head class='w-[100px]'>Seeders</Table.Head>
|
|
||||||
<Table.Head class='w-[100px]'>Leechers</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body>
|
|
||||||
{#each $list as { name, progress, size, down, up, eta, seeders, leechers, peers }, i (i)}
|
|
||||||
<Table.Row class='[&>*]:p-4'>
|
|
||||||
<Table.Cell>{name ?? '?'}</Table.Cell>
|
|
||||||
<Table.Cell>{(progress * 100).toFixed(1)}%</Table.Cell>
|
|
||||||
<Table.Cell>{fastPrettyBytes(size)}</Table.Cell>
|
|
||||||
<Table.Cell>{fastPrettyBytes(size * progress)}</Table.Cell>
|
|
||||||
<Table.Cell>{fastPrettyBits(down * 8)}/s</Table.Cell>
|
|
||||||
<Table.Cell>{fastPrettyBits(up * 8)}/s</Table.Cell>
|
|
||||||
<Table.Cell>{_eta(new Date(Date.now() + eta)) ?? 'Done'}</Table.Cell>
|
|
||||||
<Table.Cell>{seeders}<span class='text-muted-foreground'>/{peers}</span></Table.Cell>
|
|
||||||
<Table.Cell>{leechers}<span class='text-muted-foreground'>/{peers}</span></Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
{/each}
|
|
||||||
</Table.Body>
|
|
||||||
</Table.Root>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
5
src/routes/app/client/files/+page.svelte
Normal file
5
src/routes/app/client/files/+page.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { FilesTable } from '$lib/components/ui/torrentclient'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FilesTable />
|
||||||
5
src/routes/app/client/library/+page.svelte
Normal file
5
src/routes/app/client/library/+page.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { LibraryTable } from '$lib/components/ui/torrentclient'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LibraryTable />
|
||||||
5
src/routes/app/client/peers/+page.svelte
Normal file
5
src/routes/app/client/peers/+page.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { PeersTable } from '$lib/components/ui/torrentclient'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PeersTable />
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
import { dedupeAiring } from '$lib/modules/anilist'
|
import { dedupeAiring } from '$lib/modules/anilist'
|
||||||
import { authAggregator, list } from '$lib/modules/auth'
|
import { authAggregator, list } from '$lib/modules/auth'
|
||||||
import { dragScroll } from '$lib/modules/navigate'
|
import { dragScroll } from '$lib/modules/navigate'
|
||||||
import { cn, isMobile } from '$lib/utils'
|
import { cn, breakpoints } from '$lib/utils'
|
||||||
|
|
||||||
const query = authAggregator.schedule()
|
const query = authAggregator.schedule()
|
||||||
|
|
||||||
|
|
@ -127,15 +127,17 @@
|
||||||
{@const sameMonth = isSameMonth(now, day.date)}
|
{@const sameMonth = isSameMonth(now, day.date)}
|
||||||
<div>
|
<div>
|
||||||
<div class='flex flex-col text-xs py-3 h-48' class:opacity-30={!sameMonth}>
|
<div class='flex flex-col text-xs py-3 h-48' class:opacity-30={!sameMonth}>
|
||||||
{#if $isMobile}
|
{#if !$breakpoints.md}
|
||||||
<Drawer.Root shouldScaleBackground portal='html'>
|
<Drawer.Root shouldScaleBackground portal='html'>
|
||||||
<Drawer.Trigger class='h-full flex flex-col'>
|
<Drawer.Trigger class='h-full flex flex-col'>
|
||||||
<div class={cn('w-6 h-6 flex items-center justify-center font-bold mx-3', isToday(day.date) && 'bg-[rgb(61,180,242)] rounded-full')}>
|
<div class={cn('w-6 h-6 flex items-center justify-center font-bold mx-3', isToday(day.date) && 'bg-[rgb(61,180,242)] rounded-full')}>
|
||||||
{day.number}
|
{day.number}
|
||||||
</div>
|
</div>
|
||||||
<div class='px-3 mt-auto text-ellipsis overflow-hidden text-nowrap w-full'>
|
{#if episodes.length}
|
||||||
{episodes.length} eps
|
<div class='px-3 mt-auto text-ellipsis overflow-hidden text-nowrap w-full'>
|
||||||
</div>
|
{episodes.length} ep{episodes.length > 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</Drawer.Trigger>
|
</Drawer.Trigger>
|
||||||
<Drawer.Content tabindex={null}>
|
<Drawer.Content tabindex={null}>
|
||||||
<Drawer.Header>
|
<Drawer.Header>
|
||||||
|
|
@ -165,8 +167,6 @@
|
||||||
<div class={cn('w-6 h-6 flex items-center justify-center font-bold mx-3', isToday(day.date) && 'bg-[rgb(61,180,242)] rounded-full')}>
|
<div class={cn('w-6 h-6 flex items-center justify-center font-bold mx-3', isToday(day.date) && 'bg-[rgb(61,180,242)] rounded-full')}>
|
||||||
{day.number}
|
{day.number}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
{#if !$isMobile}
|
|
||||||
<div class='mt-auto'>
|
<div class='mt-auto'>
|
||||||
{#each episodes.length > 6 ? episodes.slice(0, 5) : episodes as episode, i (i)}
|
{#each episodes.length > 6 ? episodes.slice(0, 5) : episodes as episode, i (i)}
|
||||||
{@const status = _list(episode)}
|
{@const status = _list(episode)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"lucide-svelte/dist/Icon.svelte": [
|
||||||
|
"./node_modules/lucide-svelte/dist/Icon.svelte"
|
||||||
|
],
|
||||||
|
"$lib": [
|
||||||
|
"./src/lib"
|
||||||
|
],
|
||||||
|
"$lib/*": [
|
||||||
|
"./src/lib/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
// these overrides are required, because we want a custom typed eventemitter, importing node types in any fashion will fully override the typed event emitter, making life a pain
|
// these overrides are required, because we want a custom typed eventemitter, importing node types in any fashion will fully override the typed event emitter, making life a pain
|
||||||
// disabling type acquisition does NOT prevent type acquisition from working, WE LOVE TYPESCRIPT, INDUSTRY LEADING TECHNOLOGY
|
// disabling type acquisition does NOT prevent type acquisition from working, WE LOVE TYPESCRIPT, INDUSTRY LEADING TECHNOLOGY
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { resolve } from 'node:path'
|
||||||
import { sveltekit } from '@sveltejs/kit/vite'
|
import { sveltekit } from '@sveltejs/kit/vite'
|
||||||
import license from 'rollup-plugin-license'
|
import license from 'rollup-plugin-license'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
@ -13,6 +14,14 @@ export default defineConfig({
|
||||||
output: resolve(import.meta.dirname, './build/LICENSE.txt'),
|
output: resolve(import.meta.dirname, './build/LICENSE.txt'),
|
||||||
includeSelf: true
|
includeSelf: true
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
viteStaticCopy({
|
||||||
|
targets: [
|
||||||
|
{ // VITE IS DOG AND DOESNT SUPPORT DYNAMIC JSON IMPORTS
|
||||||
|
src: 'node_modules/doc999tor-fast-geoip/data/*.json',
|
||||||
|
dest: 'geoip/'
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue