Phase d cleanup

This commit is contained in:
Zerebos
2026-05-23 22:16:40 -04:00
parent 074147f64f
commit 0e93908bb2
29 changed files with 388 additions and 1413 deletions
+9
View File
@@ -22,14 +22,23 @@
"@sveltejs/kit": "^2.57.0", "@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tauri-apps/cli": "^2.0.0", "@tauri-apps/cli": "^2.0.0",
"@types/node": "^25.9.1",
"svelte": "^5.55.2", "svelte": "^5.55.2",
"svelte-check": "^4.4.6", "svelte-check": "^4.4.6",
"typescript": "^6.0.2", "typescript": "^6.0.2",
"vite": "^8.0.7" "vite": "^8.0.7"
}, },
"dependencies": { "dependencies": {
"@capacitor/app": "^8.1.0",
"@capacitor/browser": "^8.0.3",
"@capacitor/filesystem": "^8.1.2",
"@tauri-apps/api": "^2.0.0", "@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-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" "phosphor-svelte": "^3.1.0"
} }
} }
+137 -23
View File
@@ -8,31 +8,58 @@ importers:
.: .:
dependencies: 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': '@tauri-apps/api':
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.11.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': '@tauri-apps/plugin-os':
specifier: ^2.3.2 specifier: ^2.3.2
version: 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: phosphor-svelte:
specifier: ^3.1.0 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: devDependencies:
'@sveltejs/adapter-node': '@sveltejs/adapter-node':
specifier: ^5.5.4 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': '@sveltejs/adapter-static':
specifier: ^3.0.10 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': '@sveltejs/kit':
specifier: ^2.57.0 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': '@sveltejs/vite-plugin-svelte':
specifier: ^7.0.0 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': '@tauri-apps/cli':
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.11.2 version: 2.11.2
'@types/node':
specifier: ^25.9.1
version: 25.9.1
svelte: svelte:
specifier: ^5.55.2 specifier: ^5.55.2
version: 5.55.5(@typescript-eslint/types@8.57.1) version: 5.55.5(@typescript-eslint/types@8.57.1)
@@ -44,10 +71,31 @@ importers:
version: 6.0.3 version: 6.0.3
vite: vite:
specifier: ^8.0.7 specifier: ^8.0.7
version: 8.0.10 version: 8.0.10(@types/node@25.9.1)
packages: 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': '@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
@@ -477,9 +525,21 @@ packages:
engines: {node: '>= 10'} engines: {node: '>= 10'}
hasBin: true 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': '@tauri-apps/plugin-os@2.3.2':
resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} 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': '@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -489,6 +549,9 @@ packages:
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/node@25.9.1':
resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==}
'@types/resolve@1.20.2': '@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -512,6 +575,9 @@ packages:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
capacitor-native-biometric@4.2.2:
resolution: {integrity: sha512-stg0h48UxgkNuNcCAgCXLp2DUspRQs79bCBPntpCBhsDxk2bhDRUu+J/QpFtDQHG4M4DioSUcYaAsVw2N6N7wA==}
chokidar@4.0.3: chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
@@ -785,6 +851,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
vite@8.0.10: vite@8.0.10:
resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -841,6 +910,25 @@ packages:
snapshots: 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': '@emnapi/core@1.10.0':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.2.1 '@emnapi/wasi-threads': 1.2.1
@@ -1055,23 +1143,23 @@ snapshots:
dependencies: dependencies:
acorn: 8.16.0 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: dependencies:
'@rollup/plugin-commonjs': 29.0.2(rollup@4.60.4) '@rollup/plugin-commonjs': 29.0.2(rollup@4.60.4)
'@rollup/plugin-json': 6.1.0(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) '@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 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: 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: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
'@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.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 '@types/cookie': 0.6.0
acorn: 8.16.0 acorn: 8.16.0
cookie: 0.6.0 cookie: 0.6.0
@@ -1083,18 +1171,18 @@ snapshots:
set-cookie-parser: 3.1.0 set-cookie-parser: 3.1.0
sirv: 3.0.2 sirv: 3.0.2
svelte: 5.55.5(@typescript-eslint/types@8.57.1) svelte: 5.55.5(@typescript-eslint/types@8.57.1)
vite: 8.0.10 vite: 8.0.10(@types/node@25.9.1)
optionalDependencies: optionalDependencies:
typescript: 6.0.3 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: dependencies:
deepmerge: 4.3.1 deepmerge: 4.3.1
magic-string: 0.30.21 magic-string: 0.30.21
obug: 2.1.1 obug: 2.1.1
svelte: 5.55.5(@typescript-eslint/types@8.57.1) svelte: 5.55.5(@typescript-eslint/types@8.57.1)
vite: 8.0.10 vite: 8.0.10(@types/node@25.9.1)
vitefu: 1.1.3(vite@8.0.10) vitefu: 1.1.3(vite@8.0.10(@types/node@25.9.1))
'@tauri-apps/api@2.11.0': {} '@tauri-apps/api@2.11.0': {}
@@ -1145,10 +1233,26 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.11.2 '@tauri-apps/cli-win32-ia32-msvc': 2.11.2
'@tauri-apps/cli-win32-x64-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': '@tauri-apps/plugin-os@2.3.2':
dependencies: dependencies:
'@tauri-apps/api': 2.11.0 '@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': '@tybys/wasm-util@0.10.1':
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@@ -1158,6 +1262,10 @@ snapshots:
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/node@25.9.1':
dependencies:
undici-types: 7.24.6
'@types/resolve@1.20.2': {} '@types/resolve@1.20.2': {}
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
@@ -1171,6 +1279,10 @@ snapshots:
axobject-query@4.1.0: {} axobject-query@4.1.0: {}
capacitor-native-biometric@4.2.2:
dependencies:
'@capacitor/core': 3.9.0
chokidar@4.0.3: chokidar@4.0.3:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
@@ -1299,13 +1411,13 @@ snapshots:
path-parse@1.0.7: {} 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: dependencies:
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
svelte: 5.55.5(@typescript-eslint/types@8.57.1) svelte: 5.55.5(@typescript-eslint/types@8.57.1)
optionalDependencies: optionalDependencies:
vite: 8.0.10 vite: 8.0.10(@types/node@25.9.1)
picocolors@1.1.1: {} picocolors@1.1.1: {}
@@ -1434,12 +1546,13 @@ snapshots:
totalist@3.0.1: {} totalist@3.0.1: {}
tslib@2.8.1: tslib@2.8.1: {}
optional: true
typescript@6.0.3: {} typescript@6.0.3: {}
vite@8.0.10: undici-types@7.24.6: {}
vite@8.0.10(@types/node@25.9.1):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
picomatch: 4.0.4 picomatch: 4.0.4
@@ -1447,10 +1560,11 @@ snapshots:
rolldown: 1.0.0-rc.17 rolldown: 1.0.0-rc.17
tinyglobby: 0.2.16 tinyglobby: 0.2.16
optionalDependencies: optionalDependencies:
'@types/node': 25.9.1
fsevents: 2.3.3 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: optionalDependencies:
vite: 8.0.10 vite: 8.0.10(@types/node@25.9.1)
zimmerframe@1.1.4: {} zimmerframe@1.1.4: {}
+48
View File
@@ -2,4 +2,52 @@ declare global {
namespace App {} namespace App {}
const __APP_VERSION__: string 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<void>;
};
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<void>;
};
}
declare module 'capacitor-native-biometric' {
export const NativeBiometric: {
verifyIdentity(options: { reason?: string; title?: string }): Promise<void>;
setCredentials(options: { username: string; password: string; server: string }): Promise<void>;
getCredentials(options: { server: string }): Promise<{ username: string; password: string }>;
};
}
declare module '@tauri-apps/plugin-dialog' {
export function open(options?: { directory?: boolean; multiple?: boolean }): Promise<string | string[] | null>;
}
declare module '@tauri-apps/plugin-fs' {
export function readFile(path: string): Promise<Uint8Array>;
export function writeFile(path: string, data: Uint8Array): Promise<void>;
}
declare module '@tauri-apps/plugin-updater' {
export function check(): Promise<{ available: boolean; version: string; body?: string; downloadAndInstall(): Promise<void> } | null>;
}
declare module '@tauri-apps/plugin-process' {
export function relaunch(): Promise<void>;
}
export {} export {}
+13 -11
View File
@@ -1,28 +1,30 @@
import type {Attachment} from 'svelte/attachments'; import type {Attachment} from 'svelte/attachments';
export function selectPortal(triggerEl: HTMLElement & {__selectMenuEl?: HTMLElement | null;}): Attachment { export function selectPortal(triggerEl: HTMLElement & {__selectMenuEl?: HTMLElement | null;}): Attachment<Element> {
return (menuEl: HTMLElement) => { return (menuEl: Element) => {
const menu = menuEl as HTMLElement;
function position() { function position() {
const zoom = parseFloat(document.documentElement.style.zoom) / 100 || 1; const zoom = parseFloat(document.documentElement.style.zoom) / 100 || 1;
const rect = triggerEl.getBoundingClientRect(); const rect = triggerEl.getBoundingClientRect();
const top = rect.bottom / zoom + 4; const top = rect.bottom / zoom + 4;
const right = rect.right / zoom; const right = rect.right / zoom;
const width = menuEl.offsetWidth; const width = menu.offsetWidth;
const left = Math.max(8, right - width); const left = Math.max(8, right - width);
menuEl.style.position = 'fixed'; menu.style.position = 'fixed';
menuEl.style.top = `${top}px`; menu.style.top = `${top}px`;
menuEl.style.left = `${left}px`; menu.style.left = `${left}px`;
} }
menuEl.style.visibility = 'hidden'; menu.style.visibility = 'hidden';
document.body.appendChild(menuEl); document.body.appendChild(menu);
triggerEl.__selectMenuEl = menuEl; triggerEl.__selectMenuEl = menu;
requestAnimationFrame(() => { requestAnimationFrame(() => {
position(); position();
menuEl.style.visibility = ''; menu.style.visibility = '';
}); });
window.addEventListener('scroll', position, true); window.addEventListener('scroll', position, true);
@@ -32,7 +34,7 @@ export function selectPortal(triggerEl: HTMLElement & {__selectMenuEl?: HTMLElem
window.removeEventListener('scroll', position, true); window.removeEventListener('scroll', position, true);
window.removeEventListener('resize', position); window.removeEventListener('resize', position);
triggerEl.__selectMenuEl = null; triggerEl.__selectMenuEl = null;
menuEl.remove(); menu.remove();
}; };
}; };
} }
+1 -1
View File
@@ -1,4 +1,4 @@
export { shouldHideNsfw, shouldHideSource, dedupeSourcesByLang } from "@core/util"; export { shouldHideNsfw, shouldHideSource, dedupeSourcesByLang } from '$lib/core/util';
export function buildFilter<T>(...predicates: ((item: T) => boolean)[]): (item: T) => boolean { export function buildFilter<T>(...predicates: ((item: T) => boolean)[]): (item: T) => boolean {
return (item) => predicates.every((p) => p(item)); return (item) => predicates.every((p) => p(item));
+12 -8
View File
@@ -15,15 +15,17 @@ interface StoredVault {
data: string; data: string;
} }
function toB64(buf: ArrayBuffer): string { function toB64(data: ArrayBuffer | Uint8Array): string {
return btoa(String.fromCharCode(...new Uint8Array(buf))); const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
return btoa(String.fromCharCode(...bytes));
} }
function fromB64(s: string): Uint8Array { function fromB64(s: string): ArrayBuffer {
return Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); 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<CryptoKey> { async function deriveKey(pin: string, salt: ArrayBuffer): Promise<CryptoKey> {
const enc = new TextEncoder(); const enc = new TextEncoder();
const keyMat = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]); const keyMat = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]);
return crypto.subtle.deriveKey( return crypto.subtle.deriveKey(
@@ -42,7 +44,7 @@ export function vaultExists(): boolean {
export async function lockVault(pin: string, payload: VaultPayload): Promise<void> { export async function lockVault(pin: string, payload: VaultPayload): Promise<void> {
const salt = crypto.getRandomValues(new Uint8Array(16)); const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12)); 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 enc = new TextEncoder();
const cipher = await crypto.subtle.encrypt( const cipher = await crypto.subtle.encrypt(
@@ -65,10 +67,12 @@ export async function unlockVault(pin: string): Promise<VaultPayload | null> {
try { try {
const stored = JSON.parse(raw) as StoredVault; const stored = JSON.parse(raw) as StoredVault;
const key = await deriveKey(pin, fromB64(stored.salt)); 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( const plain = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: fromB64(stored.iv) }, { name: "AES-GCM", iv },
key, key,
fromB64(stored.data), cipher,
); );
return JSON.parse(new TextDecoder().decode(plain)) as VaultPayload; return JSON.parse(new TextDecoder().decode(plain)) as VaultPayload;
} catch { } catch {
+1 -1
View File
@@ -1,7 +1,7 @@
import {getAdapter} from '$lib/request-manager'; import {getAdapter} from '$lib/request-manager';
import {loadChapterPages, updateProgress} from '$lib/request-manager/chapters'; import {loadChapterPages, updateProgress} from '$lib/request-manager/chapters';
import {readerState} from '$lib/state/reader.svelte'; 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[] { export function sortChapters(chapters: Chapter[]): Chapter[] {
return [...chapters].sort((a, b) => a.sourceOrder - b.sourceOrder); return [...chapters].sort((a, b) => a.sourceOrder - b.sourceOrder);
+2 -2
View File
@@ -1,5 +1,5 @@
import type {Manga, Source} from "$lib/types"; import type {Manga, Source} from '$lib/types/index';
import type {Settings} from "$lib/types/settings"; import type {Settings} from '$lib/types/settings';
export {clsx as cn} from "clsx"; export {clsx as cn} from "clsx";
+2 -2
View File
@@ -54,13 +54,13 @@ export async function updateProgress(chapterId: string, lastPageRead: number, re
export async function markRead(id: string, read: boolean) { export async function markRead(id: string, read: boolean) {
await getAdapter().markChapterRead(id, read); 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; if (chapter) chapter.read = read;
} }
export async function markManyRead(ids: string[], read: boolean) { export async function markManyRead(ids: string[], read: boolean) {
await getAdapter().markChaptersRead(ids, read); await getAdapter().markChaptersRead(ids, read);
for (const c of seriesState.chapters) { for (const c of seriesState.chapters) {
if (ids.includes(c.id)) c.read = read; if (ids.includes(String(c.id))) c.read = read;
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
import {getAdapter} from '$lib/request-manager'; import {getAdapter} from '$lib/request-manager';
import {trackingState} from '$lib/state/tracking.svelte'; import {trackingState} from '$lib/state/tracking.svelte';
import type {TrackRecord} from '$lib/types'; import type {TrackRecord} from '$lib/types/index';
export async function loadTrackers() { export async function loadTrackers() {
trackingState.loading = true; trackingState.loading = true;
+1 -1
View File
@@ -9,7 +9,7 @@ import type {
DownloadItem, DownloadItem,
UpdateResult, UpdateResult,
} from '$lib/server-adapters/types'; } 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 type {TrackRecord} from '$lib/types/tracking';
function notImplemented(): never { function notImplemented(): never {
+3 -3
View File
@@ -9,7 +9,7 @@ import type {
DownloadItem, DownloadItem,
UpdateResult, UpdateResult,
} from '$lib/server-adapters/types'; } 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 type {TrackRecord} from '$lib/types/tracking';
import { import {
GET_LIBRARY, GET_LIBRARY,
@@ -19,7 +19,6 @@ import {
UPDATE_MANGA, UPDATE_MANGA,
SET_MANGA_META, SET_MANGA_META,
UPDATE_LIBRARY, UPDATE_LIBRARY,
FETCH_SOURCE_MANGA,
} from './manga'; } from './manga';
import { import {
GET_CHAPTERS, GET_CHAPTERS,
@@ -34,6 +33,7 @@ import {
ENQUEUE_DOWNLOAD, ENQUEUE_DOWNLOAD,
DEQUEUE_DOWNLOAD, DEQUEUE_DOWNLOAD,
CLEAR_DOWNLOADER, CLEAR_DOWNLOADER,
FETCH_SOURCE_MANGA,
} from './downloads'; } from './downloads';
import { import {
GET_EXTENSIONS, GET_EXTENSIONS,
@@ -50,12 +50,12 @@ import {
LOGOUT_TRACKER, LOGOUT_TRACKER,
} from './tracking'; } from './tracking';
import { import {
GQLResponse,
mapManga, mapManga,
mapChapter, mapChapter,
mapExtension, mapExtension,
mapDownloadItem, mapDownloadItem,
} from './types'; } from './types';
import type {GQLResponse} from './types';
const GET_CHAPTER = ` const GET_CHAPTER = `
query GetChapter($id: Int!) { query GetChapter($id: Int!) {
+1 -1
View File
@@ -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' import type { DownloadItem } from '$lib/server-adapters/types'
export interface GQLResponse<T> { export interface GQLResponse<T> {
+1 -1
View File
@@ -4,7 +4,7 @@ import type {
Extension, Extension,
Source, Source,
Tracker, Tracker,
} from '$lib/types'; } from '$lib/types/index';
import type {TrackRecord} from '$lib/types/tracking'; import type {TrackRecord} from '$lib/types/tracking';
export interface ServerConfig { export interface ServerConfig {
+2 -2
View File
@@ -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 {shouldHideSource} from '$lib/core/util';
import {settingsState} from '$lib/state/settings.svelte'; import {settingsState} from '$lib/state/settings.svelte';
@@ -24,7 +24,7 @@ export const filteredExtensions = $derived.by(() => {
let result = extensionsState.items; let result = extensionsState.items;
if (extensionsState.filter.installed) { if (extensionsState.filter.installed) {
result = result.filter(e => e.installed); result = result.filter(e => e.isInstalled);
} }
if (extensionsState.filter.language !== 'all') { if (extensionsState.filter.language !== 'all') {
result = result.filter(e => e.lang === extensionsState.filter.language); result = result.filter(e => e.lang === extensionsState.filter.language);
+2 -2
View File
@@ -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 type {MangaStatus} from '$lib/server-adapters/types';
import {shouldHideNsfw} from '$lib/core/util'; import {shouldHideNsfw} from '$lib/core/util';
import {settingsState} from '$lib/state/settings.svelte'; import {settingsState} from '$lib/state/settings.svelte';
@@ -28,7 +28,7 @@ export const filteredItems = $derived.by(() => {
result = result.filter(m => !shouldHideNsfw(m, settingsState)); result = result.filter(m => !shouldHideNsfw(m, settingsState));
if (libraryState.filter.unread) { if (libraryState.filter.unread) {
result = result.filter(m => m.unreadCount > 0); result = result.filter(m => (m.unreadCount ?? 0) > 0);
} }
if (libraryState.filter.status !== 'all') { if (libraryState.filter.status !== 'all') {
result = result.filter(m => m.status === libraryState.filter.status); result = result.filter(m => m.status === libraryState.filter.status);
+1 -1
View File
@@ -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'; import type {Page} from '$lib/server-adapters/types';
export type ReadMode = 'single' | 'strip'; export type ReadMode = 'single' | 'strip';
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Manga, Chapter } from '$lib/types' import type { Manga, Chapter } from '$lib/types/index'
export const seriesState = $state({ export const seriesState = $state({
current: null as Manga | null, current: null as Manga | null,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Tracker } from '$lib/types' import type { Tracker } from '$lib/types/index'
export const trackingState = $state({ export const trackingState = $state({
trackers: [] as Tracker[], trackers: [] as Tracker[],
+28 -540
View File
@@ -1,540 +1,28 @@
import type { export type { Settings, MangaPrefs } from './settings';
ServerAdapter,
ServerConfig, export type { Manga, MangaDetail, Category, ChapterRef } from './manga';
ServerStatus, export type { Chapter } from './chapter';
MangaFilters, export type { Extension, Source } from './extension';
MangaMeta, export type { Tracker, TrackRecord, TrackerStatus } from './tracking';
PaginatedResult,
Page, export type {
DownloadItem, DownloadQueueItem,
UpdateResult, DownloadStatus,
} from '$lib/server-adapters/types'; Connection,
import type {Manga, Chapter, Extension, Source, Tracker} from '$lib/types'; PageInfo,
export type {Settings} from './settings'; PaginatedConnection,
MetaEntry,
// ─── GQL client ──────────────────────────────────────────────────────────── UpdaterJobsInfo,
UpdateStatus,
interface GQLResponse<T> { AboutServer,
data: T; ServerUpdateEntry,
errors?: {message: string;}[]; } from './api';
} export type {
HistoryEntry,
// ─── Queries ──────────────────────────────────────────────────────────────── BookmarkEntry,
MarkerColor,
const GET_LIBRARY = ` MarkerEntry,
query GetLibrary { ReadLogEntry,
mangas(condition: { inLibrary: true }) { ReadingStats,
nodes { LibraryUpdateEntry,
id title thumbnailUrl inLibrary downloadCount unreadCount bookmarkCount } from './history';
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<string, unknown>): 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<string, unknown>): 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<string, unknown>): Extension {
return {
...(raw as unknown as Extension),
id: raw.pkgName as string,
};
}
function mapDownloadItem(raw: Record<string, unknown>): DownloadItem {
const chapter = raw.chapter as Record<string, unknown>;
const manga = chapter?.manga as Record<string, unknown>;
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<ServerStatus> {
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<string, string> {
const h: Record<string, string> = {'Content-Type': 'application/json'};
if (this.authHeader) h['Authorization'] = this.authHeader;
return h;
}
private async gql<T>(query: string, variables?: Record<string, unknown>): Promise<T> {
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<T> = await res.json();
if (json.errors?.length) throw new Error(json.errors[0].message);
return json.data;
}
// ── Manga ──────────────────────────────────────────────────────────────
async getManga(id: string): Promise<Manga> {
const data = await this.gql<{manga: Record<string, unknown>;}>(
GET_MANGA, {id: Number(id)}
);
return mapManga(data.manga);
}
async getMangaList(filters: MangaFilters): Promise<PaginatedResult<Manga>> {
if (filters.inLibrary) {
const data = await this.gql<{mangas: {nodes: Record<string, unknown>[];};}>(GET_LIBRARY);
return {items: data.mangas.nodes.map(mapManga), hasNextPage: false};
}
const data = await this.gql<{mangas: {nodes: Record<string, unknown>[];};}>(GET_LIBRARY);
return {items: data.mangas.nodes.map(mapManga), hasNextPage: false};
}
async searchManga(query: string, sourceId?: string): Promise<Manga[]> {
if (!sourceId) return [];
const data = await this.gql<{
fetchSourceManga: {mangas: Record<string, unknown>[];};
}>(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<MangaMeta>) {
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<Chapter[]> {
const data = await this.gql<{chapters: {nodes: Record<string, unknown>[];};}>(
GET_CHAPTERS, {mangaId: Number(mangaId)}
);
return data.chapters.nodes.map(mapChapter);
}
async getChapter(id: string): Promise<Chapter> {
const chapters = await this.gql<{chapters: {nodes: Record<string, unknown>[];};}>(
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<Page[]> {
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<DownloadItem[]> {
const data = await this.gql<{
downloadStatus: {queue: Record<string, unknown>[];};
}>(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<Extension[]> {
await this.gql(FETCH_EXTENSIONS);
const data = await this.gql<{extensions: {nodes: Record<string, unknown>[];};}>(
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<Source[]> {
const data = await this.gql<{sources: {nodes: Source[];};}>(GET_SOURCES);
return data.sources.nodes;
}
async browseSource(sourceId: string, page: number): Promise<PaginatedResult<Manga>> {
const data = await this.gql<{
fetchSourceManga: {mangas: Record<string, unknown>[]; 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<Tracker[]> {
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<UpdateResult[]> {
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 [];
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ export interface Manga {
lastReadChapter?: ChapterRef | null lastReadChapter?: ChapterRef | null
firstUnreadChapter?: ChapterRef | null firstUnreadChapter?: ChapterRef | null
highestNumberedChapter?: 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 { export interface MangaDetail extends Manga {
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
export interface MenuItem { interface MenuItem {
label: string label: string
icon?: any icon?: any
onClick: () => void onClick: () => void
@@ -9,11 +9,11 @@
children?: MenuEntry[] children?: MenuEntry[]
} }
export interface MenuSeparator { interface MenuSeparator {
separator: true separator: true
} }
export type MenuEntry = MenuItem | MenuSeparator type MenuEntry = MenuItem | MenuSeparator
interface Props { interface Props {
x: number x: number
+22 -4
View File
@@ -7,19 +7,37 @@
const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window const isTauri = typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
let os: OsKind = $state('unknown') let os = $state<OsKind>('unknown')
let isFullscreen = $state(false) let isFullscreen = $state(false)
onMount(async () => { onMount(() => {
if (!isTauri) return if (!isTauri) return
let disposed = false
let unlisten: (() => void) | null = null
void (async () => {
const { getCurrentWindow } = await import('@tauri-apps/api/window') const { getCurrentWindow } = await import('@tauri-apps/api/window')
const win = getCurrentWindow() const win = getCurrentWindow()
os = await detectOs() os = await detectOs()
isFullscreen = await win.isFullscreen() isFullscreen = await win.isFullscreen()
const unlisten = await win.onResized(async () => {
const stop = await win.onResized(async () => {
isFullscreen = await win.isFullscreen() isFullscreen = await win.isFullscreen()
}) })
return unlisten
if (disposed) {
stop()
return
}
unlisten = stop
})()
return () => {
disposed = true
unlisten?.()
}
}) })
const isMac = $derived(os === 'macos') const isMac = $derived(os === 'macos')
+1
View File
@@ -105,6 +105,7 @@
font-weight: var(--weight-medium); font-weight: var(--weight-medium);
line-height: var(--leading-snug); line-height: var(--leading-snug);
display: -webkit-box; display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
@@ -241,10 +241,6 @@
cursor: pointer; cursor: pointer;
} }
.spin {
animation: spin 0.8s linear infinite;
}
@keyframes spin { @keyframes spin {
from { transform: rotate(0deg); } from { transform: rotate(0deg); }
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -20,10 +20,13 @@
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
const activeKey = listeningKey
if (!activeKey) return
const binding = eventToKeybind(event) const binding = eventToKeybind(event)
if (!binding) return if (!binding) return
setBinding(listeningKey, binding) setBinding(activeKey, binding)
listeningKey = null listeningKey = null
} }
@@ -52,7 +55,7 @@
<button class="settings-button" type="button" onclick={() => updateSettings({keybinds: {...DEFAULT_KEYBINDS}})}>Reset all</button> <button class="settings-button" type="button" onclick={() => updateSettings({keybinds: {...DEFAULT_KEYBINDS}})}>Reset all</button>
</div> </div>
{#each Object.keys(KEYBIND_LABELS) as key} {#each Object.keys(KEYBIND_LABELS) as key (key)}
{@const bindKey = key as keyof Keybinds} {@const bindKey = key as keyof Keybinds}
{@const isListening = listeningKey === bindKey} {@const isListening = listeningKey === bindKey}
{@const isDefault = settingsState.keybinds[bindKey] === DEFAULT_KEYBINDS[bindKey]} {@const isDefault = settingsState.keybinds[bindKey] === DEFAULT_KEYBINDS[bindKey]}
+1 -1
View File
@@ -8,7 +8,7 @@
logoutTracker, logoutTracker,
syncTracking, syncTracking,
} from '$lib/request-manager/tracking' } from '$lib/request-manager/tracking'
import type { Tracker } from '$lib/types' import type { Tracker } from '$lib/types/index'
let oauthTrackerId = $state<number | null>(null) let oauthTrackerId = $state<number | null>(null)
let oauthCallback = $state('') let oauthCallback = $state('')
+5 -3
View File
@@ -1,11 +1,13 @@
import { sveltekit } from '@sveltejs/kit/vite' import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()],
clearScreen: false, clearScreen: false,
define: { 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: { server: {
port: 1420, port: 1420,
@@ -17,7 +19,7 @@ export default defineConfig({
envPrefix: ['VITE_', 'TAURI_'], envPrefix: ['VITE_', 'TAURI_'],
build: { build: {
target: ['es2021', 'chrome100', 'safari13'], target: ['es2021', 'chrome100', 'safari13'],
minify: !process.env.TAURI_DEBUG ? 'oxc' : false, minify: !env.TAURI_DEBUG ? 'oxc' : false,
sourcemap: !!process.env.TAURI_DEBUG, sourcemap: !!env.TAURI_DEBUG,
}, },
}) })