From 0e93908bb27b90f51d2bac33258a90004cd4ae8d Mon Sep 17 00:00:00 2001 From: Zerebos Date: Sat, 23 May 2026 22:16:40 -0400 Subject: [PATCH] Phase d cleanup --- package.json | 9 + pnpm-lock.yaml | 160 +++- src/app.d.ts | 48 + src/lib/core/actions/selectPortal.ts | 24 +- src/lib/core/algorithms/filter.ts | 2 +- src/lib/core/persistence/credentialVault.ts | 20 +- src/lib/core/reader/navigation.ts | 2 +- src/lib/core/util.ts | 4 +- src/lib/request-manager/chapters.ts | 4 +- src/lib/request-manager/tracking.ts | 2 +- src/lib/server-adapters/moku/index.ts | 2 +- src/lib/server-adapters/suwayomi/index.ts | 6 +- src/lib/server-adapters/suwayomi/types.ts | 2 +- src/lib/server-adapters/types.ts | 2 +- src/lib/state/extensions.svelte.ts | 4 +- src/lib/state/library.svelte.ts | 4 +- src/lib/state/reader.svelte.ts | 2 +- src/lib/state/series.svelte.ts | 2 +- src/lib/state/tracking.svelte.ts | 2 +- src/lib/types/index.ts | 568 +----------- src/lib/types/manga.ts | 2 +- src/lib/ui/chrome/ContextMenu.svelte | 6 +- src/lib/ui/chrome/TitleBar.svelte | 36 +- src/lib/ui/manga/MangaCard.svelte | 1 + src/routes/browse/[sourceId]/+page.svelte | 4 - .../reader/[mangaId]/[chapterId]/+page.svelte | 866 ++---------------- src/routes/settings/keybinds/+page.svelte | 7 +- src/routes/settings/tracking/+page.svelte | 2 +- vite.config.ts | 8 +- 29 files changed, 388 insertions(+), 1413 deletions(-) diff --git a/package.json b/package.json index c333284..ba9558a 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,23 @@ "@sveltejs/kit": "^2.57.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "@tauri-apps/cli": "^2.0.0", + "@types/node": "^25.9.1", "svelte": "^5.55.2", "svelte-check": "^4.4.6", "typescript": "^6.0.2", "vite": "^8.0.7" }, "dependencies": { + "@capacitor/app": "^8.1.0", + "@capacitor/browser": "^8.0.3", + "@capacitor/filesystem": "^8.1.2", "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-fs": "^2.5.1", "@tauri-apps/plugin-os": "^2.3.2", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", + "capacitor-native-biometric": "^4.2.2", "phosphor-svelte": "^3.1.0" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 376cca2..d516e0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,31 +8,58 @@ importers: .: dependencies: + '@capacitor/app': + specifier: ^8.1.0 + version: 8.1.0(@capacitor/core@3.9.0) + '@capacitor/browser': + specifier: ^8.0.3 + version: 8.0.3(@capacitor/core@3.9.0) + '@capacitor/filesystem': + specifier: ^8.1.2 + version: 8.1.2(@capacitor/core@3.9.0) '@tauri-apps/api': specifier: ^2.0.0 version: 2.11.0 + '@tauri-apps/plugin-dialog': + specifier: ^2.7.1 + version: 2.7.1 + '@tauri-apps/plugin-fs': + specifier: ^2.5.1 + version: 2.5.1 '@tauri-apps/plugin-os': specifier: ^2.3.2 version: 2.3.2 + '@tauri-apps/plugin-process': + specifier: ^2.3.1 + version: 2.3.1 + '@tauri-apps/plugin-updater': + specifier: ^2.10.1 + version: 2.10.1 + capacitor-native-biometric: + specifier: ^4.2.2 + version: 4.2.2 phosphor-svelte: specifier: ^3.1.0 - version: 3.1.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10) + version: 3.1.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)) devDependencies: '@sveltejs/adapter-node': specifier: ^5.5.4 - version: 5.5.4(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10)) + version: 5.5.4(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10(@types/node@25.9.1))) '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10)) + version: 3.0.10(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10(@types/node@25.9.1))) '@sveltejs/kit': specifier: ^2.57.0 - version: 2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10) + version: 2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10(@types/node@25.9.1)) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10) + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)) '@tauri-apps/cli': specifier: ^2.0.0 version: 2.11.2 + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 svelte: specifier: ^5.55.2 version: 5.55.5(@typescript-eslint/types@8.57.1) @@ -44,10 +71,31 @@ importers: version: 6.0.3 vite: specifier: ^8.0.7 - version: 8.0.10 + version: 8.0.10(@types/node@25.9.1) packages: + '@capacitor/app@8.1.0': + resolution: {integrity: sha512-MlmttTOWHDedr/G4SrhNRxsXMqY+R75S4MM4eIgzsgCzOYhb/MpCkA5Q3nuOCfL1oHm26xjUzqZ5aupbOwdfYg==} + peerDependencies: + '@capacitor/core': '>=8.0.0' + + '@capacitor/browser@8.0.3': + resolution: {integrity: sha512-WJWPHEPbweiFoHYmVlCbZf5yrqJ2Rchx2Xvbmd+3Lf+Zkpq3nXBThThY2CF69lYEg1NINGF9BcHThIOEU1gZlQ==} + peerDependencies: + '@capacitor/core': '>=8.0.0' + + '@capacitor/core@3.9.0': + resolution: {integrity: sha512-j1lL0+/7stY8YhIq1Lm6xixvUqIn89vtyH5ZpJNNmcZ0kwz6K9eLkcG6fvq1UWMDgSVZg9JrRGSFhb4LLoYOsw==} + + '@capacitor/filesystem@8.1.2': + resolution: {integrity: sha512-doaaMfGoFR2hWU6aV6u83I+5ZsGyJVq+Gz4r9lMpJzUKMm1eMu0hLnFdV1aXZlU9FlK/RndFrVD8oRZfNOqWgQ==} + peerDependencies: + '@capacitor/core': '>=8.0.0' + + '@capacitor/synapse@1.0.4': + resolution: {integrity: sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -477,9 +525,21 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-dialog@2.7.1': + resolution: {integrity: sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==} + + '@tauri-apps/plugin-fs@2.5.1': + resolution: {integrity: sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==} + '@tauri-apps/plugin-os@2.3.2': resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} + '@tauri-apps/plugin-process@2.3.1': + resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==} + + '@tauri-apps/plugin-updater@2.10.1': + resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -489,6 +549,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -512,6 +575,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + capacitor-native-biometric@4.2.2: + resolution: {integrity: sha512-stg0h48UxgkNuNcCAgCXLp2DUspRQs79bCBPntpCBhsDxk2bhDRUu+J/QpFtDQHG4M4DioSUcYaAsVw2N6N7wA==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -785,6 +851,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + vite@8.0.10: resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -841,6 +910,25 @@ packages: snapshots: + '@capacitor/app@8.1.0(@capacitor/core@3.9.0)': + dependencies: + '@capacitor/core': 3.9.0 + + '@capacitor/browser@8.0.3(@capacitor/core@3.9.0)': + dependencies: + '@capacitor/core': 3.9.0 + + '@capacitor/core@3.9.0': + dependencies: + tslib: 2.8.1 + + '@capacitor/filesystem@8.1.2(@capacitor/core@3.9.0)': + dependencies: + '@capacitor/core': 3.9.0 + '@capacitor/synapse': 1.0.4 + + '@capacitor/synapse@1.0.4': {} + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -1055,23 +1143,23 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10))': + '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10(@types/node@25.9.1)))': dependencies: '@rollup/plugin-commonjs': 29.0.2(rollup@4.60.4) '@rollup/plugin-json': 6.1.0(rollup@4.60.4) '@rollup/plugin-node-resolve': 16.0.3(rollup@4.60.4) - '@sveltejs/kit': 2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10) + '@sveltejs/kit': 2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10(@types/node@25.9.1)) rollup: 4.60.4 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10(@types/node@25.9.1)))': dependencies: - '@sveltejs/kit': 2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10) + '@sveltejs/kit': 2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10(@types/node@25.9.1)) - '@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10)': + '@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10(@types/node@25.9.1))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10) + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -1083,18 +1171,18 @@ snapshots: set-cookie-parser: 3.1.0 sirv: 3.0.2 svelte: 5.55.5(@typescript-eslint/types@8.57.1) - vite: 8.0.10 + vite: 8.0.10(@types/node@25.9.1) optionalDependencies: typescript: 6.0.3 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10)': + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 svelte: 5.55.5(@typescript-eslint/types@8.57.1) - vite: 8.0.10 - vitefu: 1.1.3(vite@8.0.10) + vite: 8.0.10(@types/node@25.9.1) + vitefu: 1.1.3(vite@8.0.10(@types/node@25.9.1)) '@tauri-apps/api@2.11.0': {} @@ -1145,10 +1233,26 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 '@tauri-apps/cli-win32-x64-msvc': 2.11.2 + '@tauri-apps/plugin-dialog@2.7.1': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-fs@2.5.1': + dependencies: + '@tauri-apps/api': 2.11.0 + '@tauri-apps/plugin-os@2.3.2': dependencies: '@tauri-apps/api': 2.11.0 + '@tauri-apps/plugin-process@2.3.1': + dependencies: + '@tauri-apps/api': 2.11.0 + + '@tauri-apps/plugin-updater@2.10.1': + dependencies: + '@tauri-apps/api': 2.11.0 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -1158,6 +1262,10 @@ snapshots: '@types/estree@1.0.8': {} + '@types/node@25.9.1': + dependencies: + undici-types: 7.24.6 + '@types/resolve@1.20.2': {} '@types/trusted-types@2.0.7': {} @@ -1171,6 +1279,10 @@ snapshots: axobject-query@4.1.0: {} + capacitor-native-biometric@4.2.2: + dependencies: + '@capacitor/core': 3.9.0 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -1299,13 +1411,13 @@ snapshots: path-parse@1.0.7: {} - phosphor-svelte@3.1.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10): + phosphor-svelte@3.1.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10(@types/node@25.9.1)): dependencies: estree-walker: 3.0.3 magic-string: 0.30.21 svelte: 5.55.5(@typescript-eslint/types@8.57.1) optionalDependencies: - vite: 8.0.10 + vite: 8.0.10(@types/node@25.9.1) picocolors@1.1.1: {} @@ -1434,12 +1546,13 @@ snapshots: totalist@3.0.1: {} - tslib@2.8.1: - optional: true + tslib@2.8.1: {} typescript@6.0.3: {} - vite@8.0.10: + undici-types@7.24.6: {} + + vite@8.0.10(@types/node@25.9.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -1447,10 +1560,11 @@ snapshots: rolldown: 1.0.0-rc.17 tinyglobby: 0.2.16 optionalDependencies: + '@types/node': 25.9.1 fsevents: 2.3.3 - vitefu@1.1.3(vite@8.0.10): + vitefu@1.1.3(vite@8.0.10(@types/node@25.9.1)): optionalDependencies: - vite: 8.0.10 + vite: 8.0.10(@types/node@25.9.1) zimmerframe@1.1.4: {} diff --git a/src/app.d.ts b/src/app.d.ts index 32fa55b..9a79db0 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -2,4 +2,52 @@ declare global { namespace App {} const __APP_VERSION__: string } + +declare module '@capacitor/filesystem' { + export const Filesystem: { + readFile(options: { path: string; directory?: string }): Promise<{ data: string | Blob }>; + writeFile(options: { path: string; data: string | Blob; directory?: string }): Promise; + }; + export const Directory: { + Data: string; + }; +} + +declare module '@capacitor/app' { + export const App: { + getInfo(): Promise<{ version: string }>; + }; +} + +declare module '@capacitor/browser' { + export const Browser: { + open(options: { url: string }): Promise; + }; +} + +declare module 'capacitor-native-biometric' { + export const NativeBiometric: { + verifyIdentity(options: { reason?: string; title?: string }): Promise; + setCredentials(options: { username: string; password: string; server: string }): Promise; + getCredentials(options: { server: string }): Promise<{ username: string; password: string }>; + }; +} + +declare module '@tauri-apps/plugin-dialog' { + export function open(options?: { directory?: boolean; multiple?: boolean }): Promise; +} + +declare module '@tauri-apps/plugin-fs' { + export function readFile(path: string): Promise; + export function writeFile(path: string, data: Uint8Array): Promise; +} + +declare module '@tauri-apps/plugin-updater' { + export function check(): Promise<{ available: boolean; version: string; body?: string; downloadAndInstall(): Promise } | null>; +} + +declare module '@tauri-apps/plugin-process' { + export function relaunch(): Promise; +} + export {} \ No newline at end of file diff --git a/src/lib/core/actions/selectPortal.ts b/src/lib/core/actions/selectPortal.ts index bf0a02e..b856612 100644 --- a/src/lib/core/actions/selectPortal.ts +++ b/src/lib/core/actions/selectPortal.ts @@ -1,28 +1,30 @@ import type {Attachment} from 'svelte/attachments'; -export function selectPortal(triggerEl: HTMLElement & {__selectMenuEl?: HTMLElement | null;}): Attachment { - return (menuEl: HTMLElement) => { +export function selectPortal(triggerEl: HTMLElement & {__selectMenuEl?: HTMLElement | null;}): Attachment { + return (menuEl: Element) => { + const menu = menuEl as HTMLElement; + function position() { const zoom = parseFloat(document.documentElement.style.zoom) / 100 || 1; const rect = triggerEl.getBoundingClientRect(); const top = rect.bottom / zoom + 4; const right = rect.right / zoom; - const width = menuEl.offsetWidth; + const width = menu.offsetWidth; const left = Math.max(8, right - width); - menuEl.style.position = 'fixed'; - menuEl.style.top = `${top}px`; - menuEl.style.left = `${left}px`; + menu.style.position = 'fixed'; + menu.style.top = `${top}px`; + menu.style.left = `${left}px`; } - menuEl.style.visibility = 'hidden'; - document.body.appendChild(menuEl); - triggerEl.__selectMenuEl = menuEl; + menu.style.visibility = 'hidden'; + document.body.appendChild(menu); + triggerEl.__selectMenuEl = menu; requestAnimationFrame(() => { position(); - menuEl.style.visibility = ''; + menu.style.visibility = ''; }); window.addEventListener('scroll', position, true); @@ -32,7 +34,7 @@ export function selectPortal(triggerEl: HTMLElement & {__selectMenuEl?: HTMLElem window.removeEventListener('scroll', position, true); window.removeEventListener('resize', position); triggerEl.__selectMenuEl = null; - menuEl.remove(); + menu.remove(); }; }; } diff --git a/src/lib/core/algorithms/filter.ts b/src/lib/core/algorithms/filter.ts index f5ecd05..be6c324 100644 --- a/src/lib/core/algorithms/filter.ts +++ b/src/lib/core/algorithms/filter.ts @@ -1,4 +1,4 @@ -export { shouldHideNsfw, shouldHideSource, dedupeSourcesByLang } from "@core/util"; +export { shouldHideNsfw, shouldHideSource, dedupeSourcesByLang } from '$lib/core/util'; export function buildFilter(...predicates: ((item: T) => boolean)[]): (item: T) => boolean { return (item) => predicates.every((p) => p(item)); diff --git a/src/lib/core/persistence/credentialVault.ts b/src/lib/core/persistence/credentialVault.ts index 3289193..797c41c 100644 --- a/src/lib/core/persistence/credentialVault.ts +++ b/src/lib/core/persistence/credentialVault.ts @@ -15,15 +15,17 @@ interface StoredVault { data: string; } -function toB64(buf: ArrayBuffer): string { - return btoa(String.fromCharCode(...new Uint8Array(buf))); +function toB64(data: ArrayBuffer | Uint8Array): string { + const bytes = data instanceof Uint8Array ? data : new Uint8Array(data); + return btoa(String.fromCharCode(...bytes)); } -function fromB64(s: string): Uint8Array { - return Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); +function fromB64(s: string): ArrayBuffer { + const bytes = Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); + return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); } -async function deriveKey(pin: string, salt: Uint8Array): Promise { +async function deriveKey(pin: string, salt: ArrayBuffer): Promise { const enc = new TextEncoder(); const keyMat = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]); return crypto.subtle.deriveKey( @@ -42,7 +44,7 @@ export function vaultExists(): boolean { export async function lockVault(pin: string, payload: VaultPayload): Promise { const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); - const key = await deriveKey(pin, salt); + const key = await deriveKey(pin, salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength)); const enc = new TextEncoder(); const cipher = await crypto.subtle.encrypt( @@ -65,10 +67,12 @@ export async function unlockVault(pin: string): Promise { try { const stored = JSON.parse(raw) as StoredVault; const key = await deriveKey(pin, fromB64(stored.salt)); + const iv = new Uint8Array(fromB64(stored.iv)); + const cipher = fromB64(stored.data); const plain = await crypto.subtle.decrypt( - { name: "AES-GCM", iv: fromB64(stored.iv) }, + { name: "AES-GCM", iv }, key, - fromB64(stored.data), + cipher, ); return JSON.parse(new TextDecoder().decode(plain)) as VaultPayload; } catch { diff --git a/src/lib/core/reader/navigation.ts b/src/lib/core/reader/navigation.ts index 236fe3e..e70ab1e 100644 --- a/src/lib/core/reader/navigation.ts +++ b/src/lib/core/reader/navigation.ts @@ -1,7 +1,7 @@ import {getAdapter} from '$lib/request-manager'; import {loadChapterPages, updateProgress} from '$lib/request-manager/chapters'; import {readerState} from '$lib/state/reader.svelte'; -import type {Chapter} from '$lib/types'; +import type {Chapter} from '$lib/types/index'; export function sortChapters(chapters: Chapter[]): Chapter[] { return [...chapters].sort((a, b) => a.sourceOrder - b.sourceOrder); diff --git a/src/lib/core/util.ts b/src/lib/core/util.ts index 1b2276f..8899a4f 100644 --- a/src/lib/core/util.ts +++ b/src/lib/core/util.ts @@ -1,5 +1,5 @@ -import type {Manga, Source} from "$lib/types"; -import type {Settings} from "$lib/types/settings"; +import type {Manga, Source} from '$lib/types/index'; +import type {Settings} from '$lib/types/settings'; export {clsx as cn} from "clsx"; diff --git a/src/lib/request-manager/chapters.ts b/src/lib/request-manager/chapters.ts index 64a347b..2116541 100644 --- a/src/lib/request-manager/chapters.ts +++ b/src/lib/request-manager/chapters.ts @@ -54,13 +54,13 @@ export async function updateProgress(chapterId: string, lastPageRead: number, re export async function markRead(id: string, read: boolean) { await getAdapter().markChapterRead(id, read); - const chapter = seriesState.chapters.find(c => c.id === id); + const chapter = seriesState.chapters.find(c => String(c.id) === id); if (chapter) chapter.read = read; } export async function markManyRead(ids: string[], read: boolean) { await getAdapter().markChaptersRead(ids, read); for (const c of seriesState.chapters) { - if (ids.includes(c.id)) c.read = read; + if (ids.includes(String(c.id))) c.read = read; } } diff --git a/src/lib/request-manager/tracking.ts b/src/lib/request-manager/tracking.ts index f7ccb85..09450d5 100644 --- a/src/lib/request-manager/tracking.ts +++ b/src/lib/request-manager/tracking.ts @@ -1,6 +1,6 @@ import {getAdapter} from '$lib/request-manager'; import {trackingState} from '$lib/state/tracking.svelte'; -import type {TrackRecord} from '$lib/types'; +import type {TrackRecord} from '$lib/types/index'; export async function loadTrackers() { trackingState.loading = true; diff --git a/src/lib/server-adapters/moku/index.ts b/src/lib/server-adapters/moku/index.ts index 2c87251..249624d 100644 --- a/src/lib/server-adapters/moku/index.ts +++ b/src/lib/server-adapters/moku/index.ts @@ -9,7 +9,7 @@ import type { DownloadItem, UpdateResult, } from '$lib/server-adapters/types'; -import type {Manga, Chapter, Extension, Source, Tracker} from '$lib/types'; +import type {Manga, Chapter, Extension, Source, Tracker} from '$lib/types/index'; import type {TrackRecord} from '$lib/types/tracking'; function notImplemented(): never { diff --git a/src/lib/server-adapters/suwayomi/index.ts b/src/lib/server-adapters/suwayomi/index.ts index 9474d5d..51692a7 100644 --- a/src/lib/server-adapters/suwayomi/index.ts +++ b/src/lib/server-adapters/suwayomi/index.ts @@ -9,7 +9,7 @@ import type { DownloadItem, UpdateResult, } from '$lib/server-adapters/types'; -import type {Manga, Chapter, Extension, Source, Tracker} from '$lib/types'; +import type {Manga, Chapter, Extension, Source, Tracker} from '$lib/types/index'; import type {TrackRecord} from '$lib/types/tracking'; import { GET_LIBRARY, @@ -19,7 +19,6 @@ import { UPDATE_MANGA, SET_MANGA_META, UPDATE_LIBRARY, - FETCH_SOURCE_MANGA, } from './manga'; import { GET_CHAPTERS, @@ -34,6 +33,7 @@ import { ENQUEUE_DOWNLOAD, DEQUEUE_DOWNLOAD, CLEAR_DOWNLOADER, + FETCH_SOURCE_MANGA, } from './downloads'; import { GET_EXTENSIONS, @@ -50,12 +50,12 @@ import { LOGOUT_TRACKER, } from './tracking'; import { - GQLResponse, mapManga, mapChapter, mapExtension, mapDownloadItem, } from './types'; +import type {GQLResponse} from './types'; const GET_CHAPTER = ` query GetChapter($id: Int!) { diff --git a/src/lib/server-adapters/suwayomi/types.ts b/src/lib/server-adapters/suwayomi/types.ts index af9576e..9779cc9 100644 --- a/src/lib/server-adapters/suwayomi/types.ts +++ b/src/lib/server-adapters/suwayomi/types.ts @@ -1,4 +1,4 @@ -import type { Manga, Chapter, Extension } from '$lib/types' +import type { Manga, Chapter, Extension } from '$lib/types/index' import type { DownloadItem } from '$lib/server-adapters/types' export interface GQLResponse { diff --git a/src/lib/server-adapters/types.ts b/src/lib/server-adapters/types.ts index 9e9c087..80d1350 100644 --- a/src/lib/server-adapters/types.ts +++ b/src/lib/server-adapters/types.ts @@ -4,7 +4,7 @@ import type { Extension, Source, Tracker, -} from '$lib/types'; +} from '$lib/types/index'; import type {TrackRecord} from '$lib/types/tracking'; export interface ServerConfig { diff --git a/src/lib/state/extensions.svelte.ts b/src/lib/state/extensions.svelte.ts index a5bd83d..cffc018 100644 --- a/src/lib/state/extensions.svelte.ts +++ b/src/lib/state/extensions.svelte.ts @@ -1,4 +1,4 @@ -import type {Extension, Source, Manga} from '$lib/types'; +import type {Extension, Source, Manga} from '$lib/types/index'; import {shouldHideSource} from '$lib/core/util'; import {settingsState} from '$lib/state/settings.svelte'; @@ -24,7 +24,7 @@ export const filteredExtensions = $derived.by(() => { let result = extensionsState.items; if (extensionsState.filter.installed) { - result = result.filter(e => e.installed); + result = result.filter(e => e.isInstalled); } if (extensionsState.filter.language !== 'all') { result = result.filter(e => e.lang === extensionsState.filter.language); diff --git a/src/lib/state/library.svelte.ts b/src/lib/state/library.svelte.ts index 19c12b2..1959d26 100644 --- a/src/lib/state/library.svelte.ts +++ b/src/lib/state/library.svelte.ts @@ -1,4 +1,4 @@ -import type {Manga} from '$lib/types'; +import type {Manga} from '$lib/types/index'; import type {MangaStatus} from '$lib/server-adapters/types'; import {shouldHideNsfw} from '$lib/core/util'; import {settingsState} from '$lib/state/settings.svelte'; @@ -28,7 +28,7 @@ export const filteredItems = $derived.by(() => { result = result.filter(m => !shouldHideNsfw(m, settingsState)); if (libraryState.filter.unread) { - result = result.filter(m => m.unreadCount > 0); + result = result.filter(m => (m.unreadCount ?? 0) > 0); } if (libraryState.filter.status !== 'all') { result = result.filter(m => m.status === libraryState.filter.status); diff --git a/src/lib/state/reader.svelte.ts b/src/lib/state/reader.svelte.ts index e23aed1..315d14c 100644 --- a/src/lib/state/reader.svelte.ts +++ b/src/lib/state/reader.svelte.ts @@ -1,4 +1,4 @@ -import type {Manga, Chapter} from '$lib/types'; +import type {Manga, Chapter} from '$lib/types/index'; import type {Page} from '$lib/server-adapters/types'; export type ReadMode = 'single' | 'strip'; diff --git a/src/lib/state/series.svelte.ts b/src/lib/state/series.svelte.ts index c6cecba..5088649 100644 --- a/src/lib/state/series.svelte.ts +++ b/src/lib/state/series.svelte.ts @@ -1,4 +1,4 @@ -import type { Manga, Chapter } from '$lib/types' +import type { Manga, Chapter } from '$lib/types/index' export const seriesState = $state({ current: null as Manga | null, diff --git a/src/lib/state/tracking.svelte.ts b/src/lib/state/tracking.svelte.ts index c3a6440..42da4db 100644 --- a/src/lib/state/tracking.svelte.ts +++ b/src/lib/state/tracking.svelte.ts @@ -1,4 +1,4 @@ -import type { Tracker } from '$lib/types' +import type { Tracker } from '$lib/types/index' export const trackingState = $state({ trackers: [] as Tracker[], diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 47ffcaa..a8a83ba 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -1,540 +1,28 @@ -import type { - ServerAdapter, - ServerConfig, - ServerStatus, - MangaFilters, - MangaMeta, - PaginatedResult, - Page, - DownloadItem, - UpdateResult, -} from '$lib/server-adapters/types'; -import type {Manga, Chapter, Extension, Source, Tracker} from '$lib/types'; -export type {Settings} from './settings'; - -// ─── GQL client ──────────────────────────────────────────────────────────── - -interface GQLResponse { - data: T; - errors?: {message: string;}[]; -} - -// ─── Queries ──────────────────────────────────────────────────────────────── - -const GET_LIBRARY = ` - query GetLibrary { - mangas(condition: { inLibrary: true }) { - nodes { - id title thumbnailUrl inLibrary downloadCount unreadCount bookmarkCount - description status author artist genre inLibraryAt lastFetchedAt - source { id name displayName } - chapters { totalCount } - lastReadChapter { id chapterNumber } - firstUnreadChapter { id chapterNumber } - } - } - } -`; - -const GET_MANGA = ` - query GetManga($id: Int!) { - manga(id: $id) { - id title description thumbnailUrl status author artist genre inLibrary realUrl - inLibraryAt lastFetchedAt updateStrategy - source { id name displayName } - lastReadChapter { id chapterNumber lastPageRead } - firstUnreadChapter { id chapterNumber } - highestNumberedChapter { id chapterNumber } - } - } -`; - -const GET_CHAPTERS = ` - query GetChapters($mangaId: Int!) { - chapters(condition: { mangaId: $mangaId }) { - nodes { - id name chapterNumber sourceOrder isRead isDownloaded isBookmarked - pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator - } - } - } -`; - -const GET_DOWNLOAD_STATUS = ` - query GetDownloadStatus { - downloadStatus { - state - queue { - progress state tries - chapter { - id name pageCount mangaId - manga { id title thumbnailUrl } - } - } - } - } -`; - -const GET_EXTENSIONS = ` - query GetExtensions { - extensions { - nodes { - apkName pkgName name lang versionName - isInstalled isObsolete hasUpdate iconUrl - } - } - } -`; - -const GET_SOURCES = ` - query GetSources { - sources { - nodes { - id name lang displayName iconUrl isNsfw - isConfigurable supportsLatest - } - } - } -`; - -const GET_TRACKERS = ` - query GetTrackers { - trackers { - nodes { - id name icon isLoggedIn isTokenExpired authUrl - supportsPrivateTracking supportsReadingDates supportsTrackDeletion - scores - statuses { value name } - } - } - } -`; - -// ─── Mutations ────────────────────────────────────────────────────────────── - -const FETCH_MANGA = ` - mutation FetchManga($id: Int!) { - fetchManga(input: { id: $id }) { - manga { - id title description thumbnailUrl status author artist genre inLibrary realUrl - source { id name displayName } - } - } - } -`; - -const FETCH_SOURCE_MANGA = ` - mutation FetchSourceManga($source: LongString!, $type: FetchSourceMangaType!, $page: Int!, $query: String) { - fetchSourceManga(input: { source: $source, type: $type, page: $page, query: $query }) { - mangas { id title thumbnailUrl inLibrary } - hasNextPage - } - } -`; - -const UPDATE_MANGA = ` - mutation UpdateManga($id: Int!, $inLibrary: Boolean) { - updateManga(input: { id: $id, patch: { inLibrary: $inLibrary } }) { - manga { id inLibrary } - } - } -`; - -const SET_MANGA_META = ` - mutation SetMangaMeta($mangaId: Int!, $key: String!, $value: String!) { - setMangaMeta(input: { meta: { mangaId: $mangaId, key: $key, value: $value } }) { - meta { key value } - } - } -`; - -const FETCH_CHAPTERS = ` - mutation FetchChapters($mangaId: Int!) { - fetchChapters(input: { mangaId: $mangaId }) { - chapters { - id name chapterNumber sourceOrder isRead isDownloaded isBookmarked - pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator - } - } - } -`; - -const FETCH_CHAPTER_PAGES = ` - mutation FetchChapterPages($chapterId: Int!) { - fetchChapterPages(input: { chapterId: $chapterId }) { pages } - } -`; - -const MARK_CHAPTER_READ = ` - mutation MarkChapterRead($id: Int!, $isRead: Boolean!) { - updateChapter(input: { id: $id, patch: { isRead: $isRead } }) { - chapter { id isRead } - } - } -`; - -const MARK_CHAPTERS_READ = ` - mutation MarkChaptersRead($ids: [Int!]!, $isRead: Boolean!) { - updateChapters(input: { ids: $ids, patch: { isRead: $isRead } }) { - chapters { id isRead } - } - } -`; - -const ENQUEUE_DOWNLOAD = ` - mutation EnqueueDownload($chapterId: Int!) { - enqueueChapterDownload(input: { id: $chapterId }) { - downloadStatus { state } - } - } -`; - -const DEQUEUE_DOWNLOAD = ` - mutation DequeueDownload($chapterId: Int!) { - dequeueChapterDownload(input: { id: $chapterId }) { - downloadStatus { state } - } - } -`; - -const CLEAR_DOWNLOADER = ` - mutation ClearDownloader { - clearDownloader(input: {}) { - downloadStatus { state } - } - } -`; - -const FETCH_EXTENSIONS = ` - mutation FetchExtensions { - fetchExtensions(input: {}) { - extensions { - apkName pkgName name lang versionName - isInstalled isObsolete hasUpdate iconUrl - } - } - } -`; - -const UPDATE_EXTENSION = ` - mutation UpdateExtension($id: String!, $install: Boolean, $uninstall: Boolean, $update: Boolean) { - updateExtension(input: { id: $id, patch: { install: $install, uninstall: $uninstall, update: $update } }) { - extension { apkName pkgName name isInstalled hasUpdate } - } - } -`; - -const BIND_TRACK = ` - mutation BindTrack($mangaId: Int!, $trackerId: Int!, $remoteId: LongString!) { - bindTrack(input: { mangaId: $mangaId, trackerId: $trackerId, remoteId: $remoteId }) { - trackRecord { id trackerId remoteId } - } - } -`; - -const TRACK_PROGRESS = ` - mutation TrackProgress($mangaId: Int!) { - trackProgress(input: { mangaId: $mangaId }) { - trackRecords { id trackerId lastChapterRead status } - } - } -`; - -const UPDATE_LIBRARY = ` - mutation UpdateLibrary { - updateLibrary(input: {}) { - updateStatus { jobsInfo { isRunning finishedJobs totalJobs } } - } - } -`; - -// ─── Mappers ──────────────────────────────────────────────────────────────── - -function mapChapter(raw: Record): Chapter { - return { - id: raw.id as number, - name: raw.name as string, - chapterNumber: raw.chapterNumber as number, - sourceOrder: raw.sourceOrder as number, - read: (raw.isRead as boolean) ?? false, - downloaded: (raw.isDownloaded as boolean) ?? false, - bookmarked: (raw.isBookmarked as boolean) ?? false, - pageCount: (raw.pageCount as number) ?? 0, - mangaId: raw.mangaId as number, - fetchedAt: raw.fetchedAt as string | undefined, - uploadDate: raw.uploadDate as string | null | undefined, - realUrl: raw.realUrl as string | null | undefined, - lastPageRead: raw.lastPageRead as number | undefined, - lastReadAt: raw.lastReadAt as string | undefined, - scanlator: raw.scanlator as string | null | undefined, - manga: raw.manga as Chapter['manga'], - }; -} - -function mapManga(raw: Record): Manga { - const inLibraryAt = raw.inLibraryAt as string | null | undefined; - return { - ...(raw as unknown as Manga), - tags: raw.genre as string[] | undefined, - addedAt: inLibraryAt ? new Date(inLibraryAt).getTime() : undefined, - lastReadAt: raw.lastReadChapter - ? Date.now() - : undefined, - }; -} - -function mapExtension(raw: Record): Extension { - return { - ...(raw as unknown as Extension), - id: raw.pkgName as string, - }; -} - -function mapDownloadItem(raw: Record): DownloadItem { - const chapter = raw.chapter as Record; - const manga = chapter?.manga as Record; - return { - chapterId: String(chapter?.id), - mangaId: String(chapter?.mangaId ?? manga?.id), - chapterName: chapter?.name as string, - mangaTitle: manga?.title as string, - progress: (raw.progress as number) ?? 0, - state: mapDownloadState(raw.state as string), - }; -} - -function mapDownloadState(state: string): DownloadItem['state'] { - switch (state) { - case 'DOWNLOADING': return 'downloading'; - case 'FINISHED': return 'finished'; - case 'ERROR': return 'error'; - default: return 'queued'; - } -} - -// ─── Adapter ──────────────────────────────────────────────────────────────── - -export class SuwayomiAdapter implements ServerAdapter { - private baseUrl = 'http://127.0.0.1:4567'; - private authHeader: string | null = null; - - async connect(config: ServerConfig) { - this.baseUrl = config.baseUrl.replace(/\/$/, ''); - if (config.credentials) { - const {username, password} = config.credentials; - this.authHeader = 'Basic ' + btoa(`${username}:${password}`); - } - } - - async getStatus(): Promise { - try { - const res = await fetch(`${this.baseUrl}/api/graphql`, { - method: 'POST', - headers: this.headers(), - body: JSON.stringify({query: '{ aboutServer { name } }'}), - }); - return res.ok ? 'connected' : 'error'; - } catch { - return 'disconnected'; - } - } - - private headers(): Record { - const h: Record = {'Content-Type': 'application/json'}; - if (this.authHeader) h['Authorization'] = this.authHeader; - return h; - } - - private async gql(query: string, variables?: Record): Promise { - const res = await fetch(`${this.baseUrl}/api/graphql`, { - method: 'POST', - headers: this.headers(), - body: JSON.stringify({query, variables}), - }); - if (!res.ok) throw new Error(`Suwayomi HTTP ${res.status}`); - const json: GQLResponse = await res.json(); - if (json.errors?.length) throw new Error(json.errors[0].message); - return json.data; - } - - // ── Manga ────────────────────────────────────────────────────────────── - - async getManga(id: string): Promise { - const data = await this.gql<{manga: Record;}>( - GET_MANGA, {id: Number(id)} - ); - return mapManga(data.manga); - } - - async getMangaList(filters: MangaFilters): Promise> { - if (filters.inLibrary) { - const data = await this.gql<{mangas: {nodes: Record[];};}>(GET_LIBRARY); - return {items: data.mangas.nodes.map(mapManga), hasNextPage: false}; - } - const data = await this.gql<{mangas: {nodes: Record[];};}>(GET_LIBRARY); - return {items: data.mangas.nodes.map(mapManga), hasNextPage: false}; - } - - async searchManga(query: string, sourceId?: string): Promise { - if (!sourceId) return []; - const data = await this.gql<{ - fetchSourceManga: {mangas: Record[];}; - }>(FETCH_SOURCE_MANGA, { - source: sourceId, - type: 'SEARCH', - page: 1, - query, - }); - return data.fetchSourceManga.mangas.map(mapManga); - } - - async addToLibrary(mangaId: string) { - await this.gql(UPDATE_MANGA, {id: Number(mangaId), inLibrary: true}); - } - - async removeFromLibrary(mangaId: string) { - await this.gql(UPDATE_MANGA, {id: Number(mangaId), inLibrary: false}); - } - - async updateMangaMeta(id: string, meta: Partial) { - for (const [key, value] of Object.entries(meta)) { - if (value === undefined) continue; - await this.gql(SET_MANGA_META, { - mangaId: Number(id), - key, - value: String(value), - }); - } - } - - // ── Chapters ─────────────────────────────────────────────────────────── - - async getChapters(mangaId: string): Promise { - const data = await this.gql<{chapters: {nodes: Record[];};}>( - GET_CHAPTERS, {mangaId: Number(mangaId)} - ); - return data.chapters.nodes.map(mapChapter); - } - - async getChapter(id: string): Promise { - const chapters = await this.gql<{chapters: {nodes: Record[];};}>( - GET_CHAPTERS, {mangaId: 0} - ); - const found = chapters.chapters.nodes.find(c => String(c.id) === id); - if (!found) throw new Error(`Chapter ${id} not found`); - return mapChapter(found); - } - - async getChapterPages(id: string): Promise { - const data = await this.gql<{fetchChapterPages: {pages: string[];};}>( - FETCH_CHAPTER_PAGES, {chapterId: Number(id)} - ); - return data.fetchChapterPages.pages.map((url, index) => ({index, url})); - } - - async markChapterRead(id: string, read: boolean) { - await this.gql(MARK_CHAPTER_READ, {id: Number(id), isRead: read}); - } - - async markChaptersRead(ids: string[], read: boolean) { - await this.gql(MARK_CHAPTERS_READ, {ids: ids.map(Number), isRead: read}); - } - - // ── Downloads ────────────────────────────────────────────────────────── - - async getDownloads(): Promise { - const data = await this.gql<{ - downloadStatus: {queue: Record[];}; - }>(GET_DOWNLOAD_STATUS); - return data.downloadStatus.queue.map(mapDownloadItem); - } - - async enqueueDownload(chapterId: string) { - await this.gql(ENQUEUE_DOWNLOAD, {chapterId: Number(chapterId)}); - } - - async dequeueDownload(chapterId: string) { - await this.gql(DEQUEUE_DOWNLOAD, {chapterId: Number(chapterId)}); - } - - async clearDownloads() { - await this.gql(CLEAR_DOWNLOADER); - } - - // ── Extensions ───────────────────────────────────────────────────────── - - async getExtensions(): Promise { - await this.gql(FETCH_EXTENSIONS); - const data = await this.gql<{extensions: {nodes: Record[];};}>( - GET_EXTENSIONS - ); - return data.extensions.nodes.map(mapExtension); - } - - async installExtension(id: string) { - await this.gql(UPDATE_EXTENSION, {id, install: true}); - } - - async uninstallExtension(id: string) { - await this.gql(UPDATE_EXTENSION, {id, uninstall: true}); - } - - async updateExtension(id: string) { - await this.gql(UPDATE_EXTENSION, {id, update: true}); - } - - async getSources(): Promise { - const data = await this.gql<{sources: {nodes: Source[];};}>(GET_SOURCES); - return data.sources.nodes; - } - - async browseSource(sourceId: string, page: number): Promise> { - const data = await this.gql<{ - fetchSourceManga: {mangas: Record[]; hasNextPage: boolean;}; - }>(FETCH_SOURCE_MANGA, { - source: sourceId, - type: 'LATEST', - page, - }); - return { - items: data.fetchSourceManga.mangas.map(mapManga), - hasNextPage: data.fetchSourceManga.hasNextPage, - }; - } - - // ── Tracking ─────────────────────────────────────────────────────────── - - async getTrackers(): Promise { - const data = await this.gql<{trackers: {nodes: Tracker[];};}>(GET_TRACKERS); - return data.trackers.nodes; - } - - async linkTracker(mangaId: string, trackerId: string, remoteId: string) { - await this.gql(BIND_TRACK, { - mangaId: Number(mangaId), - trackerId: Number(trackerId), - remoteId, - }); - } - - async syncTracking(mangaId: string) { - await this.gql(TRACK_PROGRESS, {mangaId: Number(mangaId)}); - } - - // ── Updates ──────────────────────────────────────────────────────────── - - async checkForUpdates(mangaIds?: string[]): Promise { - if (mangaIds?.length) { - const results: UpdateResult[] = []; - for (const id of mangaIds) { - const before = await this.getChapters(id); - await this.gql(FETCH_CHAPTERS, {mangaId: Number(id)}); - const after = await this.getChapters(id); - results.push({mangaId: id, newChapters: after.length - before.length}); - } - return results; - } - await this.gql(UPDATE_LIBRARY); - return []; - } -} +export type { Settings, MangaPrefs } from './settings'; + +export type { Manga, MangaDetail, Category, ChapterRef } from './manga'; +export type { Chapter } from './chapter'; +export type { Extension, Source } from './extension'; +export type { Tracker, TrackRecord, TrackerStatus } from './tracking'; + +export type { + DownloadQueueItem, + DownloadStatus, + Connection, + PageInfo, + PaginatedConnection, + MetaEntry, + UpdaterJobsInfo, + UpdateStatus, + AboutServer, + ServerUpdateEntry, +} from './api'; +export type { + HistoryEntry, + BookmarkEntry, + MarkerColor, + MarkerEntry, + ReadLogEntry, + ReadingStats, + LibraryUpdateEntry, +} from './history'; diff --git a/src/lib/types/manga.ts b/src/lib/types/manga.ts index 693408a..29e8827 100644 --- a/src/lib/types/manga.ts +++ b/src/lib/types/manga.ts @@ -50,7 +50,7 @@ export interface Manga { lastReadChapter?: ChapterRef | null firstUnreadChapter?: ChapterRef | null highestNumberedChapter?: ChapterRef | null - source?: { id: string; name: string; displayName: string } | null + source?: { id: string; name: string; displayName: string; isNsfw?: boolean } | null } export interface MangaDetail extends Manga { diff --git a/src/lib/ui/chrome/ContextMenu.svelte b/src/lib/ui/chrome/ContextMenu.svelte index ec19664..85f45f1 100644 --- a/src/lib/ui/chrome/ContextMenu.svelte +++ b/src/lib/ui/chrome/ContextMenu.svelte @@ -1,5 +1,5 @@ - - - -
-
-
- - -
-

{readerState.manga?.title ?? 'Reader'}

-

{readerState.chapter?.name ?? 'Loading chapter'}

-

{chapterLabel} · {pageLabel}

-
-
- -
-
- - -
- - - -
- - - -
-
-
- -
-
- {progressPercent}% read - {pageLabel} -
- -
- -
- {#if initializing && readerState.pages.length === 0} -
- -

Loading chapter pages...

-
- {:else if routeError || readerState.pagesError} -
-

{routeError ?? readerState.pagesError}

- -
- {:else if totalPages === 0} -
-

No pages were returned for this chapter.

-
- {:else if readerState.mode === 'strip'} -
- {#each readerState.pages as pageData, index (pageData.index)} - - {/each} -
- {:else} -
1 - ? `cursor: grab; overflow: hidden;` - : ''} - > - - - {#if currentPageData} - {`Page 1 - ? `transform: scale(${readerState.inspectScale}) translate(${readerState.inspectPanX}px, ${readerState.inspectPanY}px); transform-origin: center; transition: transform 0.1s ease;` - : `zoom: ${readerState.zoom}`} - /> - {/if} - - -
- {/if} -
- -
- - - -
-
- - - let initializing = $state(true) let routeError = $state(null) let requestVersion = 0 @@ -652,11 +23,8 @@ const totalPages = $derived(readerState.pages.length) const progressPercent = $derived(Math.round(progress * 100)) const pageLabel = $derived(totalPages > 0 ? `${currentPageNumber} / ${totalPages}` : '0 / 0') - const chapterLabel = $derived( - readerState.chapter - ? `Ch. ${Number.isInteger(readerState.chapter.chapterNumber) ? readerState.chapter.chapterNumber : readerState.chapter.chapterNumber}` - : 'Chapter' - ) + const chapterLabel = $derived(readerState.chapter ? `Ch. ${readerState.chapter.chapterNumber}` : 'Chapter') + const zoomPct = $derived(Math.round(readerState.zoom * 100)) $effect(() => { const activeMangaId = mangaId @@ -679,6 +47,10 @@ }) }) + $effect(() => { + preloadPages(readerState.pages, readerState.currentPage, settingsState.preloadPages ?? 3) + }) + async function stepForward() { const advanced = await goToNextReaderPage() if (advanced) return @@ -720,100 +92,67 @@ await goto(`/series/${readerState.manga.id}`) } - function handleKeydown(event: KeyboardEvent) { - const binds = settingsState.keybinds + function cycleMode() { + readerState.mode = readerState.mode === 'single' ? 'strip' : 'single' + } - if (matchesKeybind(event, binds.turnPageRight)) { - event.preventDefault() - void (readerState.direction === 'rtl' ? stepBackward() : stepForward()) + function toggleBookmarkAction() { + if (!readerState.chapter || !readerState.manga) return + const currentChapterId = readerState.chapter.id + + if (getBookmark(currentChapterId)) { + removeBookmark(currentChapterId) return } - if (matchesKeybind(event, binds.turnPageLeft)) { - event.preventDefault() - void (readerState.direction === 'rtl' ? stepForward() : stepBackward()) - return - } + addBookmark({ + mangaId: readerState.manga.id, + chapterId: currentChapterId, + pageNumber: readerState.currentPage, + mangaTitle: readerState.manga.title, + chapterName: readerState.chapter.name, + thumbnailUrl: readerState.manga.thumbnailUrl, + }) + } - if (matchesKeybind(event, binds.firstPage)) { - event.preventDefault() - void setCurrentReaderPage(0) - return - } - - if (matchesKeybind(event, binds.lastPage)) { - event.preventDefault() - void setCurrentReaderPage(readerState.pages.length - 1) - return - } - - if (matchesKeybind(event, binds.turnChapterRight)) { - event.preventDefault() + const handleKeydown = createReaderKeyHandler({ + goNext: () => void (readerState.direction === 'rtl' ? stepBackward() : stepForward()), + goPrev: () => void (readerState.direction === 'rtl' ? stepForward() : stepBackward()), + goToPage: (idx) => void setCurrentReaderPage(idx), + lastPage: () => readerState.pages.length - 1, + exitReader: () => void returnToSeries(), + chapterNext: () => { const neighbors = getAdjacentChapters() if (readerState.manga && neighbors.next) { void goto(`/reader/${readerState.manga.id}/${neighbors.next.id}`) } - return - } - - if (matchesKeybind(event, binds.turnChapterLeft)) { - event.preventDefault() + }, + chapterPrev: () => { const neighbors = getAdjacentChapters() if (readerState.manga && neighbors.previous) { void goto(`/reader/${readerState.manga.id}/${neighbors.previous.id}`) } - return - } - - if (matchesKeybind(event, binds.exitReader)) { - event.preventDefault() - void returnToSeries() - return - } - - if (matchesKeybind(event, binds.toggleReadingDirection)) { - event.preventDefault() + }, + adjustZoom: (delta) => { + readerState.zoom = adjustZoom(readerState.zoom, delta) + }, + resetZoom: () => { + readerState.zoom = 1 + readerState.inspectScale = 1 + readerState.inspectPanX = 0 + readerState.inspectPanY = 0 + }, + cycleMode, + toggleDirection: () => { readerState.direction = readerState.direction === 'ltr' ? 'rtl' : 'ltr' - return - } - - if (matchesKeybind(event, binds.togglePageStyle)) { - event.preventDefault() - readerState.mode = readerState.mode === 'single' ? 'strip' : 'single' - return - } - - if (matchesKeybind(event, binds.toggleFullscreen)) { - event.preventDefault() - void toggleFullscreen() - return - } - - if (matchesKeybind(event, binds.toggleBookmark)) { - event.preventDefault() - if (!readerState.chapter || !readerState.manga) return - const chapterId = readerState.chapter.id - if (getBookmark(chapterId)) { - removeBookmark(chapterId) - } else { - addBookmark({ - mangaId: readerState.manga.id, - chapterId, - pageNumber: readerState.currentPage, - mangaTitle: readerState.manga.title, - chapterName: readerState.chapter.name, - thumbnailUrl: readerState.manga.thumbnailUrl, - }) - } - return - } - - // legacy Escape key fallback - if (event.key === 'Escape') { - event.preventDefault() - void returnToSeries() - } - } + }, + openSettings: () => void goto('/settings/general'), + toggleBookmark: toggleBookmarkAction, + toggleAutoScroll: () => { + readerState.autoScrollActive = !readerState.autoScrollActive + }, + getKeybinds: () => settingsState.keybinds, + }) @@ -835,34 +174,29 @@
- -
- + +
+ + + +
@@ -898,16 +232,10 @@

No pages were returned for this chapter.

{:else if readerState.mode === 'strip'} -
+
{#each readerState.pages as pageData, index (pageData.index)} - {/each} @@ -919,11 +247,7 @@ {#if currentPageData} - {`Page + {`Page {/if}
- {#each Object.keys(KEYBIND_LABELS) as key} + {#each Object.keys(KEYBIND_LABELS) as key (key)} {@const bindKey = key as keyof Keybinds} {@const isListening = listeningKey === bindKey} {@const isDefault = settingsState.keybinds[bindKey] === DEFAULT_KEYBINDS[bindKey]} diff --git a/src/routes/settings/tracking/+page.svelte b/src/routes/settings/tracking/+page.svelte index 161a5a5..b2b9eb8 100644 --- a/src/routes/settings/tracking/+page.svelte +++ b/src/routes/settings/tracking/+page.svelte @@ -8,7 +8,7 @@ logoutTracker, syncTracking, } from '$lib/request-manager/tracking' - import type { Tracker } from '$lib/types' + import type { Tracker } from '$lib/types/index' let oauthTrackerId = $state(null) let oauthCallback = $state('') diff --git a/vite.config.ts b/vite.config.ts index ebcb9c2..a65ae89 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,13 @@ import { sveltekit } from '@sveltejs/kit/vite' import { defineConfig } from 'vite' +const env = (globalThis as { process?: { env?: Record } }).process?.env ?? {} + export default defineConfig({ plugins: [sveltekit()], clearScreen: false, define: { - __APP_VERSION__: JSON.stringify(process.env.npm_package_version ?? '0.0.0'), + __APP_VERSION__: JSON.stringify(env.npm_package_version ?? '0.0.0'), }, server: { port: 1420, @@ -17,7 +19,7 @@ export default defineConfig({ envPrefix: ['VITE_', 'TAURI_'], build: { target: ['es2021', 'chrome100', 'safari13'], - minify: !process.env.TAURI_DEBUG ? 'oxc' : false, - sourcemap: !!process.env.TAURI_DEBUG, + minify: !env.TAURI_DEBUG ? 'oxc' : false, + sourcemap: !!env.TAURI_DEBUG, }, }) \ No newline at end of file