From 12155eaabbea52371cf15d5c9ecaaa0619c295ea Mon Sep 17 00:00:00 2001 From: ThaUnknown <6506529+ThaUnknown@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:51:27 +0200 Subject: [PATCH] feat: new torrent client UI --- package.json | 9 +- pnpm-lock.yaml | 132 +++++++++++ src/app.css | 4 + src/app.d.ts | 69 +++++- src/lib/components/EpisodesList.svelte | 4 +- .../dropdown-menu-checkbox-item.svelte | 35 +++ .../dropdown-menu-content.svelte | 26 +++ .../dropdown-menu/dropdown-menu-item.svelte | 31 +++ .../dropdown-menu/dropdown-menu-label.svelte | 19 ++ .../dropdown-menu-radio-group.svelte | 11 + .../dropdown-menu-radio-item.svelte | 35 +++ .../dropdown-menu-separator.svelte | 14 ++ .../dropdown-menu-shortcut.svelte | 13 ++ .../dropdown-menu-sub-content.svelte | 29 +++ .../dropdown-menu-sub-trigger.svelte | 32 +++ src/lib/components/ui/dropdown-menu/index.ts | 48 ++++ src/lib/components/ui/forums/Comments.svelte | 4 +- src/lib/components/ui/forums/Threads.svelte | 4 +- src/lib/components/ui/player/player.svelte | 6 +- src/lib/components/ui/sidebar/sidebar.svelte | 4 +- src/lib/components/ui/table/table.svelte | 2 +- .../ui/torrentclient/columnheader.svelte | 65 ++++++ .../ui/torrentclient/files/cells/index.ts | 2 + .../ui/torrentclient/files/cells/name.svelte | 5 + .../torrentclient/files/cells/progress.svelte | 8 + .../ui/torrentclient/files/table.svelte | 97 ++++++++ .../components/ui/torrentclient/globe.svelte | 104 +++++++++ src/lib/components/ui/torrentclient/index.ts | 6 + .../torrentclient/library/cells/date.svelte | 5 + .../ui/torrentclient/library/cells/index.ts | 2 + .../torrentclient/library/cells/name.svelte | 5 + .../torrentclient/library/cells/status.svelte | 7 + .../ui/torrentclient/library/table.svelte | 117 ++++++++++ .../ui/torrentclient/overview.svelte | 209 ++++++++++++++++++ .../torrentclient/peers/cells/country.svelte | 17 ++ .../ui/torrentclient/peers/cells/flags.svelte | 48 ++++ .../ui/torrentclient/peers/cells/index.ts | 5 + .../ui/torrentclient/peers/cells/ip.svelte | 8 + .../torrentclient/peers/cells/progress.svelte | 8 + .../ui/torrentclient/peers/cells/speed.svelte | 18 ++ .../ui/torrentclient/peers/table.svelte | 136 ++++++++++++ .../components/ui/torrentclient/status.svelte | 13 ++ src/lib/modules/geoip/index.ts | 71 ++++++ src/lib/modules/geoip/utils.ts | 49 ++++ src/lib/modules/native.ts | 66 +++++- src/lib/modules/torrent/client.ts | 53 ++++- src/lib/utils.ts | 77 +++++-- src/routes/+layout.svelte | 1 + src/routes/app/client/+layout.svelte | 51 +++++ src/routes/app/client/+page.svelte | 41 +--- src/routes/app/client/files/+page.svelte | 5 + src/routes/app/client/library/+page.svelte | 5 + src/routes/app/client/peers/+page.svelte | 5 + src/routes/app/schedule/+page.svelte | 14 +- tailwind.config.ts | 6 +- tsconfig.json | 11 + vite.config.ts | 13 +- 57 files changed, 1787 insertions(+), 97 deletions(-) create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte create mode 100644 src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte create mode 100644 src/lib/components/ui/dropdown-menu/index.ts create mode 100644 src/lib/components/ui/torrentclient/columnheader.svelte create mode 100644 src/lib/components/ui/torrentclient/files/cells/index.ts create mode 100644 src/lib/components/ui/torrentclient/files/cells/name.svelte create mode 100644 src/lib/components/ui/torrentclient/files/cells/progress.svelte create mode 100644 src/lib/components/ui/torrentclient/files/table.svelte create mode 100644 src/lib/components/ui/torrentclient/globe.svelte create mode 100644 src/lib/components/ui/torrentclient/index.ts create mode 100644 src/lib/components/ui/torrentclient/library/cells/date.svelte create mode 100644 src/lib/components/ui/torrentclient/library/cells/index.ts create mode 100644 src/lib/components/ui/torrentclient/library/cells/name.svelte create mode 100644 src/lib/components/ui/torrentclient/library/cells/status.svelte create mode 100644 src/lib/components/ui/torrentclient/library/table.svelte create mode 100644 src/lib/components/ui/torrentclient/overview.svelte create mode 100644 src/lib/components/ui/torrentclient/peers/cells/country.svelte create mode 100644 src/lib/components/ui/torrentclient/peers/cells/flags.svelte create mode 100644 src/lib/components/ui/torrentclient/peers/cells/index.ts create mode 100644 src/lib/components/ui/torrentclient/peers/cells/ip.svelte create mode 100644 src/lib/components/ui/torrentclient/peers/cells/progress.svelte create mode 100644 src/lib/components/ui/torrentclient/peers/cells/speed.svelte create mode 100644 src/lib/components/ui/torrentclient/peers/table.svelte create mode 100644 src/lib/components/ui/torrentclient/status.svelte create mode 100644 src/lib/modules/geoip/index.ts create mode 100644 src/lib/modules/geoip/utils.ts create mode 100644 src/routes/app/client/+layout.svelte create mode 100644 src/routes/app/client/files/+page.svelte create mode 100644 src/routes/app/client/library/+page.svelte create mode 100644 src/routes/app/client/peers/+page.svelte diff --git a/package.json b/package.json index dc5e237..79ec921 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui", - "version": "6.3.69", + "version": "6.3.70", "license": "BUSL-1.1", "private": true, "packageManager": "pnpm@9.14.4", @@ -39,12 +39,14 @@ "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "vaul-svelte": "^0.3.2", - "vite": "^5.4.11" + "vite": "^5.4.11", + "vite-plugin-static-copy": "^3.0.2" }, "type": "module", "dependencies": { "@cloudflare/speedtest": "^1.4.1", "@fontsource-variable/nunito": "^5.2.5", + "@fontsource/geist-mono": "^5.2.6", "@prgm/sveltekit-progress-bar": "2.0.0", "@thaunknown/web-irc": "^1.0.3", "@urql/exchange-auth": "^2.2.1", @@ -58,8 +60,10 @@ "bittorrent-tracker": "10.0.12", "bottleneck": "^2.19.5", "clsx": "^2.1.1", + "cobe": "0.6.3", "date-fns": "^4.1.0", "debug": "^4.4.1", + "doc999tor-fast-geoip": "^1.1.335", "dompurify": "^3.2.5", "events": "^3.3.0", "idb-keyval": "^6.2.2", @@ -70,6 +74,7 @@ "rollup-plugin-license": "^3.6.0", "semver": "^7.7.2", "simple-store-svelte": "^1.0.6", + "svelte-headless-table": "^0.18.3", "svelte-keybinds": "^1.0.9", "svelte-persisted-store": "^0.12.0", "tailwind-merge": "^3.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e5790c..2a38818 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@fontsource-variable/nunito': specifier: ^5.2.5 version: 5.2.5 + '@fontsource/geist-mono': + specifier: ^5.2.6 + version: 5.2.6 '@prgm/sveltekit-progress-bar': 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) @@ -53,12 +56,18 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + cobe: + specifier: 0.6.3 + version: 0.6.3 date-fns: specifier: ^4.1.0 version: 4.1.0 debug: specifier: ^4.4.1 version: 4.4.1 + doc999tor-fast-geoip: + specifier: ^1.1.335 + version: 1.1.335 dompurify: specifier: ^3.2.5 version: 3.2.5 @@ -89,6 +98,9 @@ importers: simple-store-svelte: specifier: ^1.0.6 version: 1.0.6 + svelte-headless-table: + specifier: ^0.18.3 + version: 0.18.3(svelte@4.2.19) svelte-keybinds: specifier: ^1.0.9 version: 1.0.9 @@ -183,6 +195,9 @@ importers: vite: specifier: ^5.4.11 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: @@ -400,6 +415,9 @@ packages: '@fontsource-variable/nunito@5.2.5': 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': resolution: {integrity: sha512-jFFSY8OxYeBxdKi58UzeMXG1tdm4FVjXa8WHIi66Gzu9JWtCE6mqom3a8xkmSw+mVaybFW5EN2WXf1WztJVNyQ==} peerDependencies: @@ -988,6 +1006,9 @@ packages: peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.1 + cobe@0.6.3: + resolution: {integrity: sha512-WHr7X4o1ym94GZ96h7b1pNemZJacbOzd02dZtnVwuC4oWBaLg96PBmp2rIS1SAhUDhhC/QyS9WEqkpZIs/ZBTg==} + code-red@1.0.4: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} @@ -1126,6 +1147,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + doc999tor-fast-geoip@1.1.335: + resolution: {integrity: sha512-/rnNx4yIu84V8i8c0+95fNuVOfSRAGKNJXrSR8PSmrcXDuqzmnLNUsm++8Tqe+c9n1Fsp/3ryATTe5hHgC64hQ==} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1384,6 +1408,10 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fs-extra@11.3.0: + resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} + engines: {node: '>=14.14'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1690,6 +1718,9 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1895,6 +1926,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 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: resolution: {tarball: https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316} version: 1.5.1 @@ -1931,6 +1966,9 @@ packages: periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} + phenomenon@1.6.0: + resolution: {integrity: sha512-7h9/fjPD3qNlgggzm88cY58l9sudZ6Ey+UmZsizfhtawO6E3srZQXywaNm2lBwT72TbpHYRPy7ytIHeBUD/G0A==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2293,6 +2331,11 @@ packages: svelte: 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: resolution: {integrity: sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==} engines: {node: ^12.20 || ^14.13.1 || >= 16} @@ -2302,6 +2345,11 @@ packages: svelte-keybinds@1.0.9: 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: resolution: {integrity: sha512-BdBQr2SGSJ+rDWH8/aEV5GthBJDapVP0GP3fuUCA7TjYG5ctcB+O9Mj9ZC0+Jo1oJMfZUd1y9H68NFRR5MyIJA==} engines: {node: '>=0.14'} @@ -2313,11 +2361,21 @@ packages: peerDependencies: 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: resolution: {integrity: sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==} peerDependencies: 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: resolution: {integrity: sha512-NX8a7eSqF1hr6WKArvXr7TV7DeE+y0kDFD7L5JP7TWqlwFidzGKaG415p992MHREiiEWOv2xIWXJ+mlONofs0A==} peerDependencies: @@ -2367,6 +2425,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2435,6 +2497,10 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} 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: resolution: {integrity: sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw==} @@ -2468,6 +2534,12 @@ packages: video-deband@1.0.7: 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: resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2757,6 +2829,8 @@ snapshots: '@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)': dependencies: '@0no-co/graphqlsp': 1.12.16(graphql@16.10.0)(typescript@5.8.3) @@ -3419,6 +3493,10 @@ snapshots: nanoid: 5.1.5 svelte: 4.2.19 + cobe@0.6.3: + dependencies: + phenomenon: 1.6.0 + code-red@1.0.4: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -3547,6 +3625,8 @@ snapshots: dlv@1.1.3: {} + doc999tor-fast-geoip@1.1.335: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -3940,6 +4020,12 @@ snapshots: fraction.js@4.3.7: {} + fs-extra@11.3.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + fsevents@2.3.3: optional: true @@ -4248,6 +4334,12 @@ snapshots: dependencies: minimist: 1.2.8 + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -4426,6 +4518,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@7.0.3: {} + p2pt@https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316: dependencies: bittorrent-tracker: 10.0.12 @@ -4465,6 +4559,8 @@ snapshots: estree-walker: 3.0.3 is-reference: 3.0.3 + phenomenon@1.6.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -4882,12 +4978,23 @@ snapshots: optionalDependencies: 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): dependencies: svelte: 4.2.19 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): dependencies: svelte: 4.2.19 @@ -4896,10 +5003,19 @@ snapshots: dependencies: 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): dependencies: 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): dependencies: dedent-js: 1.0.1 @@ -4984,6 +5100,11 @@ snapshots: dependencies: any-promise: 1.3.0 + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5069,6 +5190,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + universalify@2.0.1: {} + unordered-array-remove@1.0.2: {} update-browserslist-db@1.1.3(browserslist@4.24.5): @@ -5104,6 +5227,15 @@ snapshots: rvfc-polyfill: 1.0.7 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): dependencies: esbuild: 0.21.5 diff --git a/src/app.css b/src/app.css index 3a4c018..3ea940a 100644 --- a/src/app.css +++ b/src/app.css @@ -14,6 +14,10 @@ --padding-left: unset !important; } + .font-mono { + font-family: "Geist Mono", ui-monospace, SFMono-Regular, Roboto Mono, Menlo, Monaco, Liberation Mono, DejaVu Sans Mono, Courier New, monospace !important; + } + :root { --background: 0 0% 100%; --foreground: 240 10% 3.9%; diff --git a/src/app.d.ts b/src/app.d.ts index 808682f..e3ef482 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -38,17 +38,55 @@ export interface Attachment { } export interface TorrentInfo { - peers: number - progress: number - down: number - up: number 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 - 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 - downloaded: number - eta: number + progress: number + selections: number } export interface TorrentSettings { @@ -93,8 +131,18 @@ export interface Native { subtitles: (hash: string, id: number, cb: (subtitle: { text: string, time: number, duration: number }, trackNumber: number) => void) => Promise errors: (cb: (error: Error) => void) => Promise chapters: (hash: string, id: number) => Promise> - torrentStats: (hash: string) => Promise - torrents: () => Promise + torrentInfo: (hash: string) => Promise + peerInfo: (hash: string) => Promise + fileInfo: (hash: string) => Promise + protocolStatus: (hash: string) => Promise<{ + dht: boolean + lsd: boolean + pex: boolean + nat: boolean + forwarding: boolean + persisting: boolean + streaming: boolean + }> setDOH: (dns: string) => Promise cachedTorrents: () => Promise downloadProgress: (percent: number) => Promise @@ -108,6 +156,7 @@ export interface Native { version: () => Promise navigate: (cb: (data: { target: string, value: string | undefined }) => void) => Promise defaultTransparency: () => boolean + debug: (levels: string) => Promise } declare global { diff --git a/src/lib/components/EpisodesList.svelte b/src/lib/components/EpisodesList.svelte index 32a9ebe..9c35367 100644 --- a/src/lib/components/EpisodesList.svelte +++ b/src/lib/components/EpisodesList.svelte @@ -23,7 +23,7 @@ import { authAggregator, list, progress } from '$lib/modules/auth' import { click, dragScroll } from '$lib/modules/navigate' 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 media: Media @@ -150,7 +150,7 @@ - {#if !$isMobile} + {#if $breakpoints.md} {#each pages as { page, type } (page)} {#if type === 'ellipsis'} ... diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..0f408c6 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..03a5866 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,26 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..b89f5fb --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..43f1527 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..1c74ae1 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..7cdfdca --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..8b16e03 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..d8c7378 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..042398d --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..f207f30 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,32 @@ + + + + + + diff --git a/src/lib/components/ui/dropdown-menu/index.ts b/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..c1749e9 --- /dev/null +++ b/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,48 @@ +import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.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, +}; diff --git a/src/lib/components/ui/forums/Comments.svelte b/src/lib/components/ui/forums/Comments.svelte index 7db9065..b55e10f 100644 --- a/src/lib/components/ui/forums/Comments.svelte +++ b/src/lib/components/ui/forums/Comments.svelte @@ -8,7 +8,7 @@ import { Comment } from './' import { client } from '$lib/modules/anilist' - import { isMobile } from '$lib/utils' + import { breakpoints } from '$lib/utils' export let isLocked = false export let threadId: number @@ -71,7 +71,7 @@ - {#if !$isMobile} + {#if $breakpoints.md} {#each pages as { page, type } (page)} {#if type === 'ellipsis'} ... diff --git a/src/lib/components/ui/forums/Threads.svelte b/src/lib/components/ui/forums/Threads.svelte index 5b406d3..b1a7ec7 100644 --- a/src/lib/components/ui/forums/Threads.svelte +++ b/src/lib/components/ui/forums/Threads.svelte @@ -12,7 +12,7 @@ import * as Avatar from '$lib/components/ui/avatar' import * as Tooltip from '$lib/components/ui/tooltip' import { client, type Media } from '$lib/modules/anilist' - import { isMobile, since } from '$lib/utils' + import { breakpoints, since } from '$lib/utils' export let media: Media @@ -121,7 +121,7 @@ - {#if !$isMobile} + {#if $breakpoints.md} {#each pages as { page, type } (page)} {#if type === 'ellipsis'} ... diff --git a/src/lib/components/ui/player/player.svelte b/src/lib/components/ui/player/player.svelte index c42bbec..17c2634 100644 --- a/src/lib/components/ui/player/player.svelte +++ b/src/lib/components/ui/player/player.svelte @@ -754,15 +754,15 @@
- {$torrentstats.seeders} + {$torrentstats.peers.seeders}
- {fastPrettyBits($torrentstats.down * 8)}/s + {fastPrettyBits($torrentstats.speed.down * 8)}/s
- {fastPrettyBits($torrentstats.up * 8)}/s + {fastPrettyBits($torrentstats.speed.up * 8)}/s
{#if seeking} diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte index dd6230c..a66a199 100644 --- a/src/lib/components/ui/sidebar/sidebar.svelte +++ b/src/lib/components/ui/sidebar/sidebar.svelte @@ -4,12 +4,12 @@ import { Button } from '../button' - import { isMobile } from '$lib/utils' + import { breakpoints } from '$lib/utils' let open = false // 152 x 140 -{#if $isMobile} +{#if !$breakpoints.md}
diff --git a/src/lib/components/ui/table/table.svelte b/src/lib/components/ui/table/table.svelte index 0d1872f..deb7f99 100644 --- a/src/lib/components/ui/table/table.svelte +++ b/src/lib/components/ui/table/table.svelte @@ -9,7 +9,7 @@ export { className as class } -
+
diff --git a/src/lib/components/ui/torrentclient/columnheader.svelte b/src/lib/components/ui/torrentclient/columnheader.svelte new file mode 100644 index 0000000..9699f12 --- /dev/null +++ b/src/lib/components/ui/torrentclient/columnheader.svelte @@ -0,0 +1,65 @@ + + +{#if !props.sort.disabled} +
+ + + + + + + + Asc + + + + Desc + + + +
+{:else} + +{/if} diff --git a/src/lib/components/ui/torrentclient/files/cells/index.ts b/src/lib/components/ui/torrentclient/files/cells/index.ts new file mode 100644 index 0000000..c305774 --- /dev/null +++ b/src/lib/components/ui/torrentclient/files/cells/index.ts @@ -0,0 +1,2 @@ +export { default as NameCell } from './name.svelte' +export { default as ProgressCell } from './progress.svelte' diff --git a/src/lib/components/ui/torrentclient/files/cells/name.svelte b/src/lib/components/ui/torrentclient/files/cells/name.svelte new file mode 100644 index 0000000..42b8a1d --- /dev/null +++ b/src/lib/components/ui/torrentclient/files/cells/name.svelte @@ -0,0 +1,5 @@ + + +
{value}
diff --git a/src/lib/components/ui/torrentclient/files/cells/progress.svelte b/src/lib/components/ui/torrentclient/files/cells/progress.svelte new file mode 100644 index 0000000..eeab5ba --- /dev/null +++ b/src/lib/components/ui/torrentclient/files/cells/progress.svelte @@ -0,0 +1,8 @@ + + +
+
+
+
{(value * 100).toFixed(1)}%
diff --git a/src/lib/components/ui/torrentclient/files/table.svelte b/src/lib/components/ui/torrentclient/files/table.svelte new file mode 100644 index 0000000..ccc104a --- /dev/null +++ b/src/lib/components/ui/torrentclient/files/table.svelte @@ -0,0 +1,97 @@ + + +
+ + + {#each $headerRows as headerRow, i (i)} + + + {#each headerRow.cells as cell (cell.id)} + + + {#if cell.id !== 'flags'} + + + + {:else} +
+ +
+ {/if} +
+
+ {/each} +
+
+ {/each} +
+ + {#if $pageRows.length} + {#each $pageRows as row (row.id)} + + + {#each row.cells as cell (cell.id)} + + + + + + {/each} + + + {/each} + {:else} + + + No files downloaded yet. + + + {/if} + +
+
diff --git a/src/lib/components/ui/torrentclient/globe.svelte b/src/lib/components/ui/torrentclient/globe.svelte new file mode 100644 index 0000000..206c480 --- /dev/null +++ b/src/lib/components/ui/torrentclient/globe.svelte @@ -0,0 +1,104 @@ + + + diff --git a/src/lib/components/ui/torrentclient/index.ts b/src/lib/components/ui/torrentclient/index.ts new file mode 100644 index 0000000..095a7c7 --- /dev/null +++ b/src/lib/components/ui/torrentclient/index.ts @@ -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' diff --git a/src/lib/components/ui/torrentclient/library/cells/date.svelte b/src/lib/components/ui/torrentclient/library/cells/date.svelte new file mode 100644 index 0000000..c8beeaf --- /dev/null +++ b/src/lib/components/ui/torrentclient/library/cells/date.svelte @@ -0,0 +1,5 @@ + + +
{new Date(value).toLocaleDateString()}
diff --git a/src/lib/components/ui/torrentclient/library/cells/index.ts b/src/lib/components/ui/torrentclient/library/cells/index.ts new file mode 100644 index 0000000..2934165 --- /dev/null +++ b/src/lib/components/ui/torrentclient/library/cells/index.ts @@ -0,0 +1,2 @@ +export { default as StatusCell } from './status.svelte' +export { default as NameCell } from './name.svelte' diff --git a/src/lib/components/ui/torrentclient/library/cells/name.svelte b/src/lib/components/ui/torrentclient/library/cells/name.svelte new file mode 100644 index 0000000..42b8a1d --- /dev/null +++ b/src/lib/components/ui/torrentclient/library/cells/name.svelte @@ -0,0 +1,5 @@ + + +
{value}
diff --git a/src/lib/components/ui/torrentclient/library/cells/status.svelte b/src/lib/components/ui/torrentclient/library/cells/status.svelte new file mode 100644 index 0000000..efc137a --- /dev/null +++ b/src/lib/components/ui/torrentclient/library/cells/status.svelte @@ -0,0 +1,7 @@ + + +
+
{value ? 'Completed' : 'In Progress'} +
diff --git a/src/lib/components/ui/torrentclient/library/table.svelte b/src/lib/components/ui/torrentclient/library/table.svelte new file mode 100644 index 0000000..95e1b4f --- /dev/null +++ b/src/lib/components/ui/torrentclient/library/table.svelte @@ -0,0 +1,117 @@ + + +
+ + + {#each $headerRows as headerRow, i (i)} + + + {#each headerRow.cells as cell (cell.id)} + + + {#if cell.id !== 'flags'} + + + + {:else} +
+ +
+ {/if} +
+
+ {/each} +
+
+ {/each} +
+ + {#if $pageRows.length} + {#each $pageRows as row (row.id)} + + + {#each row.cells as cell (cell.id)} + + + + + + {/each} + + + {/each} + {:else} + + + No torrents downloaded yet. + + + {/if} + +
+
diff --git a/src/lib/components/ui/torrentclient/overview.svelte b/src/lib/components/ui/torrentclient/overview.svelte new file mode 100644 index 0000000..d9df599 --- /dev/null +++ b/src/lib/components/ui/torrentclient/overview.svelte @@ -0,0 +1,209 @@ + + +
+
+
+

{torrent.name || 'No Name Provided'}

+
+
+ {completed ? 'Seeding' : 'Downloading'} +
+ {torrent.hash} +
+
+
+
+
+
+
+ + Progress +
+ {(torrent.progress * 100).toFixed(1)}% +
+
+
+
+
+
+ +
+ Downloaded +
{fastPrettyBytes(torrent.size.downloaded)}
+
+
+
+ +
+ Uploaded +
{fastPrettyBytes(torrent.size.uploaded)}
+
+
+
+ +
+ Total Size +
{fastPrettyBytes(torrent.size.total)}
+
+
+
+ +
+ Pieces +
{torrent.pieces.total} × {fastPrettyBytes(torrent.pieces.size)}
+
+
+
+
+
+
+
+

+ + Speed & Transfer +

+
+
+
+
+
+ + Download +
+
{fastPrettyBits(torrent.speed.down * 8)}/s
+
+
+
+ + Upload +
+
{fastPrettyBits(torrent.speed.up * 8)}/s
+
+
+
+
+
+
+

+ + Time Information +

+
+
+
+
+
+ + Remaining +
+
{eta(torrent.time.remaining)}
+
+
+
+ + Elapsed +
+
{eta(torrent.time.elapsed)}
+
+
+
+
+
+
+

+ + Peers & Connections +

+
+
+
+
+
+ + Seeders +
+
{torrent.peers.seeders}
+
+
+
+ + Leechers +
+
{torrent.peers.leechers}
+
+
+
+ + Wires +
+
{torrent.peers.wires}
+
+
+
+
+
+
+
+

+ + Protocol Status +

+
+
+
+
+

Network Discovery

+
+ + + +
+
+
+

Connection

+
+ + +
+
+
+

Storage

+
+ + +
+
+
+
+
+
diff --git a/src/lib/components/ui/torrentclient/peers/cells/country.svelte b/src/lib/components/ui/torrentclient/peers/cells/country.svelte new file mode 100644 index 0000000..bb48633 --- /dev/null +++ b/src/lib/components/ui/torrentclient/peers/cells/country.svelte @@ -0,0 +1,17 @@ + + +
+ {#await lookup(value) then location} +
+ {codeToEmoji(location.country)} +
+
+ {location.country} +
+ {/await} +
diff --git a/src/lib/components/ui/torrentclient/peers/cells/flags.svelte b/src/lib/components/ui/torrentclient/peers/cells/flags.svelte new file mode 100644 index 0000000..e1a6124 --- /dev/null +++ b/src/lib/components/ui/torrentclient/peers/cells/flags.svelte @@ -0,0 +1,48 @@ + + +
+ {#each value as flag (flag)} + + + + {#if flag === 'incoming'} + + {:else if flag === 'outgoing'} + + {:else if flag === 'utp'} + + {:else if flag === 'encrypted'} + + {/if} + + + +

{labels[flag]}

+
+
+ + {/each} +
diff --git a/src/lib/components/ui/torrentclient/peers/cells/index.ts b/src/lib/components/ui/torrentclient/peers/cells/index.ts new file mode 100644 index 0000000..3636f06 --- /dev/null +++ b/src/lib/components/ui/torrentclient/peers/cells/index.ts @@ -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' diff --git a/src/lib/components/ui/torrentclient/peers/cells/ip.svelte b/src/lib/components/ui/torrentclient/peers/cells/ip.svelte new file mode 100644 index 0000000..955c4c0 --- /dev/null +++ b/src/lib/components/ui/torrentclient/peers/cells/ip.svelte @@ -0,0 +1,8 @@ + + +
+
{ip} +
diff --git a/src/lib/components/ui/torrentclient/peers/cells/progress.svelte b/src/lib/components/ui/torrentclient/peers/cells/progress.svelte new file mode 100644 index 0000000..68e17ea --- /dev/null +++ b/src/lib/components/ui/torrentclient/peers/cells/progress.svelte @@ -0,0 +1,8 @@ + + +
+
+
+
{(value * 100).toFixed(1)}%
diff --git a/src/lib/components/ui/torrentclient/peers/cells/speed.svelte b/src/lib/components/ui/torrentclient/peers/cells/speed.svelte new file mode 100644 index 0000000..2288266 --- /dev/null +++ b/src/lib/components/ui/torrentclient/peers/cells/speed.svelte @@ -0,0 +1,18 @@ + + +
+ {#if type === 'download'} + + {:else} + + {/if} + {fastPrettyBits(value * 8) + '/s'} +
diff --git a/src/lib/components/ui/torrentclient/peers/table.svelte b/src/lib/components/ui/torrentclient/peers/table.svelte new file mode 100644 index 0000000..1a77596 --- /dev/null +++ b/src/lib/components/ui/torrentclient/peers/table.svelte @@ -0,0 +1,136 @@ + + +
+ + + {#each $headerRows as headerRow, i (i)} + + + {#each headerRow.cells as cell (cell.id)} + + + {#if cell.id !== 'flags'} + + + + {:else} +
+ +
+ {/if} +
+
+ {/each} +
+
+ {/each} +
+ + {#if $pageRows.length} + {#each $pageRows as row (row.id)} + + + {#each row.cells as cell (cell.id)} + + + + + + {/each} + + + {/each} + {:else} + + + No peers connected yet. + + + {/if} + +
+
diff --git a/src/lib/components/ui/torrentclient/status.svelte b/src/lib/components/ui/torrentclient/status.svelte new file mode 100644 index 0000000..42bcb71 --- /dev/null +++ b/src/lib/components/ui/torrentclient/status.svelte @@ -0,0 +1,13 @@ + + +
+
+
+ {title} +

{description}

+
+
diff --git a/src/lib/modules/geoip/index.ts b/src/lib/modules/geoip/index.ts new file mode 100644 index 0000000..53431cd --- /dev/null +++ b/src/lib/modules/geoip/index.ts @@ -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 (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 = {} +const locationCache = viteFixedImportWeTrullyHateViteWithAPassionForNotSupportingBasicFeatures('locations') + +async function readFile (filename: string): Promise { + if (ipCache[filename] !== undefined) { + return await Promise.resolve(ipCache[filename] as T) + } + const content = await viteFixedImportWeTrullyHateViteWithAPassionForNotSupportingBasicFeatures(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 { + const ip = ipStr2Num(stringifiedIp) + const data = await readFile('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('i' + rootIndex) + const index = binarySearch(data2, ip, identity) + rootIndex * params.NUMBER_NODES_PER_MIDINDEX + nextIp = getNextIp(data2, index, nextIp, identity) + const data3 = await readFile('' + 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(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] + } +} diff --git a/src/lib/modules/geoip/utils.ts b/src/lib/modules/geoip/utils.ts new file mode 100644 index 0000000..e44286d --- /dev/null +++ b/src/lib/modules/geoip/utils.ts @@ -0,0 +1,49 @@ +type extractKeyFunction = (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 (list: recordType[], item: number, extractKey: extractKeyFunction): 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 (data: recordType[], index: number, currentNextIp: number, extractKey: extractKeyFunction): number { + if (index < (data.length - 1)) { + return extractKey(data[index + 1]!) + } else { + return currentNextIp + } +} diff --git a/src/lib/modules/native.ts b/src/lib/modules/native.ts index acf15f5..85fd536 100644 --- a/src/lib/modules/native.ts +++ b/src/lib/modules/native.ts @@ -24,6 +24,29 @@ const dummyFiles = [ // 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>({ authAL: (url: string) => { @@ -86,12 +109,47 @@ export default Object.assign>({ setHideToTray: async () => undefined, transparency: async () => undefined, setZoom: async () => undefined, - // @ts-expect-error yeah - navigate: async (cb) => { globalThis.___navigate = cb }, + navigate: async () => undefined, downloadProgress: async () => undefined, updateProgress: async () => undefined, - torrentStats: async (): Promise => ({ 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() }), - torrents: async (): Promise => [{ 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 => ({ + 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, errors: async () => undefined, debug: async () => undefined diff --git a/src/lib/modules/torrent/client.ts b/src/lib/modules/torrent/client.ts index 235f776..0d6b2f9 100644 --- a/src/lib/modules/torrent/client.ts +++ b/src/lib/modules/torrent/client.ts @@ -5,33 +5,72 @@ import { persisted } from 'svelte-persisted-store' import native from '../native' 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' +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 { last = persisted<{media: Media, id: string, episode: number} | null>('last-torrent', null) active = writable>() downloaded = writable(this.cachedSet()) - stats = readable({ 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 const update = async () => { const id = (await get(this.active))?.id - if (id) set(await native.torrentStats(id)) - listener = setTimeout(update, 1000) + if (id) set(await native.torrentInfo(id)) + listener = setTimeout(update, 200) } update() return () => clearTimeout(listener) }) - list = readable([], set => { + protocol = readable(defaultProtocolStatus, set => { let listener = 0 const update = async () => { - set(await native.torrents()) - listener = setTimeout(update, 1000) + const id = (await get(this.active))?.id + if (id) set(await native.protocolStatus(id)) + listener = setTimeout(update, 5000) + } + + update() + return () => clearTimeout(listener) + }) + + peers = readable([], 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([], 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() diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 83bc568..3e44a99 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -9,6 +9,42 @@ export function cn (...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } +type MediaQuery = Record> = { + [K in keyof Query]?: boolean | string; +} + +function calculateMedia (mqls: Record) { + 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>({}, set => { + const ctrl = new AbortController() + const mqls: Record = {} + 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 { y?: number x?: number @@ -89,15 +125,7 @@ export const debounce = 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 formatterShort = new Intl.RelativeTimeFormat('en', { style: 'short' }) const ranges: Partial> = { years: 3600 * 24 * 365, months: 3600 * 24 * 30, @@ -119,16 +147,33 @@ export function since (date: Date) { } return 'now' } -export function eta (date: Date) { - const secondsElapsed = (date.getTime() - Date.now()) / 1000 - for (const _key in ranges) { - const key = _key as Intl.RelativeTimeFormatUnit - if ((ranges[key] ?? 0) < Math.abs(secondsElapsed)) { - const delta = secondsElapsed / (ranges[key] ?? 0) - return formatterShort.format(Math.round(delta), key) +export function eta (seconds: number): string { + if (!Number.isFinite(seconds) || seconds < 0) return '0s' + + const units = [ + { label: 'y', secs: 31536000 }, + { label: 'mo', secs: 2592000 }, + { 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'] export function fastPrettyBytes (num: number) { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 24e2c66..43e9d26 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,6 +1,7 @@ + +
+
+

Torrent Client

+

+ Monitor your torrents, and configure settings for your torrent client. +

+
+ +
+ +
+ + +
+
+
diff --git a/src/routes/app/client/+page.svelte b/src/routes/app/client/+page.svelte index 814adee..05762ff 100644 --- a/src/routes/app/client/+page.svelte +++ b/src/routes/app/client/+page.svelte @@ -1,41 +1,8 @@ -
- - - - Name - Progress - Size - Done - Download - Upload - ETA - Seeders - Leechers - - - - {#each $list as { name, progress, size, down, up, eta, seeders, leechers, peers }, i (i)} - - {name ?? '?'} - {(progress * 100).toFixed(1)}% - {fastPrettyBytes(size)} - {fastPrettyBytes(size * progress)} - {fastPrettyBits(down * 8)}/s - {fastPrettyBits(up * 8)}/s - {_eta(new Date(Date.now() + eta)) ?? 'Done'} - {seeders}/{peers} - {leechers}/{peers} - - {/each} - - +
+
diff --git a/src/routes/app/client/files/+page.svelte b/src/routes/app/client/files/+page.svelte new file mode 100644 index 0000000..905acf1 --- /dev/null +++ b/src/routes/app/client/files/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/app/client/library/+page.svelte b/src/routes/app/client/library/+page.svelte new file mode 100644 index 0000000..e9b9b52 --- /dev/null +++ b/src/routes/app/client/library/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/app/client/peers/+page.svelte b/src/routes/app/client/peers/+page.svelte new file mode 100644 index 0000000..8170176 --- /dev/null +++ b/src/routes/app/client/peers/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/app/schedule/+page.svelte b/src/routes/app/schedule/+page.svelte index 9f66406..df423bb 100644 --- a/src/routes/app/schedule/+page.svelte +++ b/src/routes/app/schedule/+page.svelte @@ -15,7 +15,7 @@ import { dedupeAiring } from '$lib/modules/anilist' import { authAggregator, list } from '$lib/modules/auth' import { dragScroll } from '$lib/modules/navigate' - import { cn, isMobile } from '$lib/utils' + import { cn, breakpoints } from '$lib/utils' const query = authAggregator.schedule() @@ -127,15 +127,17 @@ {@const sameMonth = isSameMonth(now, day.date)}
- {#if $isMobile} + {#if !$breakpoints.md}
{day.number}
-
- {episodes.length} eps -
+ {#if episodes.length} +
+ {episodes.length} ep{episodes.length > 1 ? 's' : ''} +
+ {/if}
@@ -165,8 +167,6 @@
{day.number}
- {/if} - {#if !$isMobile}
{#each episodes.length > 6 ? episodes.slice(0, 5) : episodes as episode, i (i)} {@const status = _list(episode)} diff --git a/tailwind.config.ts b/tailwind.config.ts index 0c06482..41ebf27 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -34,7 +34,11 @@ const config: Config = { }) ], darkMode: ['class'], - content: ['./src/**/*.{html,js,svelte,ts}'], + content: [ + './src/**/*.{html,js,svelte,ts}', + './node_modules/svelte-ux/**/*.{svelte,js}', + './node_modules/layerchart/**/*.{svelte,js}' + ], safelist: ['dark'], theme: { container: { diff --git a/tsconfig.json b/tsconfig.json index 8cfc52f..fdcb950 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,16 @@ { "compilerOptions": { + "paths": { + "lucide-svelte/dist/Icon.svelte": [ + "./node_modules/lucide-svelte/dist/Icon.svelte" + ], + "$lib": [ + "./src/lib" + ], + "$lib/*": [ + "./src/lib/*" + ] + }, "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 // disabling type acquisition does NOT prevent type acquisition from working, WE LOVE TYPESCRIPT, INDUSTRY LEADING TECHNOLOGY diff --git a/vite.config.ts b/vite.config.ts index d61a4e3..e0814ac 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ import { resolve } from 'node:path' import { sveltekit } from '@sveltejs/kit/vite' import license from 'rollup-plugin-license' import { defineConfig } from 'vite' +import { viteStaticCopy } from 'vite-plugin-static-copy' export default defineConfig({ plugins: [ @@ -13,6 +14,14 @@ export default defineConfig({ output: resolve(import.meta.dirname, './build/LICENSE.txt'), includeSelf: true } + }), + viteStaticCopy({ + targets: [ + { // VITE IS DOG AND DOESNT SUPPORT DYNAMIC JSON IMPORTS + src: 'node_modules/doc999tor-fast-geoip/data/*.json', + dest: 'geoip/' + } + ] }) ], resolve: { @@ -27,7 +36,9 @@ export default defineConfig({ 'bittorrent-tracker/lib/client/websocket-tracker.js': resolve(import.meta.dirname, 'node_modules/bittorrent-tracker/lib/client/websocket-tracker.js') } }, - server: { port: 7344 }, + server: { + port: 7344 + }, build: { target: 'esnext', sourcemap: true