mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Phase d cleanup
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+137
-23
@@ -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: {}
|
||||||
|
|||||||
Vendored
+48
@@ -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 {}
|
||||||
@@ -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,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));
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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> {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,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,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,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
@@ -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 [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
|
||||||
const win = getCurrentWindow()
|
let disposed = false
|
||||||
os = await detectOs()
|
let unlisten: (() => void) | null = null
|
||||||
isFullscreen = await win.isFullscreen()
|
|
||||||
const unlisten = await win.onResized(async () => {
|
void (async () => {
|
||||||
|
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
||||||
|
const win = getCurrentWindow()
|
||||||
|
os = await detectOs()
|
||||||
isFullscreen = await win.isFullscreen()
|
isFullscreen = await win.isFullscreen()
|
||||||
})
|
|
||||||
return unlisten
|
const stop = await win.onResized(async () => {
|
||||||
|
isFullscreen = await win.isFullscreen()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (disposed) {
|
||||||
|
stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unlisten = stop
|
||||||
|
})()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disposed = true
|
||||||
|
unlisten?.()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const isMac = $derived(os === 'macos')
|
const isMac = $derived(os === 'macos')
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -1,646 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { page } from '$app/stores'
|
import { page } from '$app/stores'
|
||||||
import { onMount } from 'svelte'
|
|
||||||
import { ArrowArcLeft, CaretLeft, CaretRight, Columns, List, MagnifyingGlass, SpinnerGap, TextAlignRight } from 'phosphor-svelte'
|
import { ArrowArcLeft, CaretLeft, CaretRight, Columns, List, MagnifyingGlass, SpinnerGap, TextAlignRight } from 'phosphor-svelte'
|
||||||
import { currentPageData, progress, readerState } from '$lib/state/reader.svelte'
|
import { currentPageData, progress, readerState } from '$lib/state/reader.svelte'
|
||||||
import { ensureReaderSession } from '$lib/core/reader/chapterLoader'
|
import { ensureReaderSession } from '$lib/core/reader/chapterLoader'
|
||||||
import { getAdjacentChapters, goToNextReaderPage, goToPreviousReaderPage, setCurrentReaderPage } from '$lib/core/reader/navigation'
|
import { getAdjacentChapters, goToNextReaderPage, goToPreviousReaderPage, setCurrentReaderPage } from '$lib/core/reader/navigation'
|
||||||
import { createReaderKeyHandler } from '$lib/core/reader/readerKeybinds'
|
import { createReaderKeyHandler } from '$lib/core/reader/readerKeybinds'
|
||||||
import { createPinchTracker } from '$lib/core/reader/pinchZoom'
|
|
||||||
import { setupScrollTracking } from '$lib/core/reader/scrollHandler'
|
|
||||||
import { adjustZoom, ZOOM_STEP } from '$lib/core/reader/zoomHelpers'
|
import { adjustZoom, ZOOM_STEP } from '$lib/core/reader/zoomHelpers'
|
||||||
import { preloadPages } from '$lib/core/reader/pageLoader'
|
import { preloadPages } from '$lib/core/reader/pageLoader'
|
||||||
import { settingsState } from '$lib/state/settings.svelte'
|
import { settingsState } from '$lib/state/settings.svelte'
|
||||||
import { addBookmark, getBookmark, removeBookmark } from '$lib/state/history.svelte'
|
import { addBookmark, getBookmark, removeBookmark } from '$lib/state/history.svelte'
|
||||||
import Button from '$lib/ui/primitives/Button.svelte'
|
import Button from '$lib/ui/primitives/Button.svelte'
|
||||||
|
|
||||||
let initializing = $state(true)
|
|
||||||
let routeError = $state<string | null>(null)
|
|
||||||
let requestVersion = 0
|
|
||||||
|
|
||||||
let stageEl = $state<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
const mangaId = $derived($page.params.mangaId ?? '')
|
|
||||||
const chapterId = $derived($page.params.chapterId ?? '')
|
|
||||||
const chapterNeighbors = $derived.by(() => getAdjacentChapters())
|
|
||||||
const currentPageNumber = $derived(readerState.currentPage + 1)
|
|
||||||
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. ${readerState.chapter.chapterNumber}`
|
|
||||||
: 'Chapter'
|
|
||||||
)
|
|
||||||
const zoomPct = $derived(Math.round(readerState.zoom * 100))
|
|
||||||
|
|
||||||
// ---- Session loading ----
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const activeMangaId = mangaId
|
|
||||||
const activeChapterId = chapterId
|
|
||||||
|
|
||||||
if (!activeMangaId || !activeChapterId) return
|
|
||||||
|
|
||||||
const version = ++requestVersion
|
|
||||||
initializing = true
|
|
||||||
routeError = null
|
|
||||||
|
|
||||||
void ensureReaderSession(activeMangaId, activeChapterId)
|
|
||||||
.catch((error) => {
|
|
||||||
if (version !== requestVersion) return
|
|
||||||
routeError = error instanceof Error ? error.message : String(error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (version !== requestVersion) return
|
|
||||||
initializing = false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- Preload upcoming pages ----
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
preloadPages(readerState.pages, readerState.currentPage, settingsState.preloadPages ?? 3)
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- Auto-scroll in strip mode ----
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!readerState.autoScrollActive || readerState.mode !== 'strip' || !stageEl) return
|
|
||||||
const speed = settingsState.autoScrollSpeed ?? 5
|
|
||||||
let id: ReturnType<typeof setInterval>
|
|
||||||
id = setInterval(() => {
|
|
||||||
if (!stageEl) return
|
|
||||||
stageEl.scrollTop += speed
|
|
||||||
}, 16)
|
|
||||||
return () => clearInterval(id)
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- Longstrip scroll tracking ----
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const el = stageEl
|
|
||||||
if (!el || readerState.mode !== 'strip') return
|
|
||||||
return setupScrollTracking(el, {
|
|
||||||
onPageChange: (idx) => { readerState.currentPage = idx },
|
|
||||||
onChapterChange: (_id) => {},
|
|
||||||
onMarkRead: (_id) => {},
|
|
||||||
onAppend: () => {},
|
|
||||||
getStripChapters: () => [],
|
|
||||||
shouldAutoMark: () => settingsState.autoMarkRead ?? true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- Pinch zoom ----
|
|
||||||
|
|
||||||
const pinchTracker = createPinchTracker({
|
|
||||||
getZoom: () => readerState.zoom,
|
|
||||||
setZoom: (v) => { readerState.zoom = v },
|
|
||||||
getInspectScale: () => readerState.inspectScale,
|
|
||||||
setInspectScale: (v) => { readerState.inspectScale = v },
|
|
||||||
resetInspectPan: () => { readerState.inspectPanX = 0; readerState.inspectPanY = 0 },
|
|
||||||
isLongstrip: () => readerState.mode === 'strip',
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- Navigation helpers ----
|
|
||||||
|
|
||||||
async function stepForward() {
|
|
||||||
if (readerState.mode === 'strip') {
|
|
||||||
if (chapterNeighbors.next && readerState.manga) {
|
|
||||||
await goto(`/reader/${readerState.manga.id}/${chapterNeighbors.next.id}`)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const advanced = await goToNextReaderPage()
|
|
||||||
if (advanced) return
|
|
||||||
if (chapterNeighbors.next && readerState.manga) {
|
|
||||||
await goto(`/reader/${readerState.manga.id}/${chapterNeighbors.next.id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stepBackward() {
|
|
||||||
if (readerState.mode === 'strip') {
|
|
||||||
if (chapterNeighbors.previous && readerState.manga) {
|
|
||||||
await goto(`/reader/${readerState.manga.id}/${chapterNeighbors.previous.id}`)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const moved = await goToPreviousReaderPage()
|
|
||||||
if (moved) return
|
|
||||||
if (chapterNeighbors.previous && readerState.manga) {
|
|
||||||
await goto(`/reader/${readerState.manga.id}/${chapterNeighbors.previous.id}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRangeInput(event: Event) {
|
|
||||||
const target = event.currentTarget as HTMLInputElement
|
|
||||||
await setCurrentReaderPage(Number(target.value) - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function retryLoad() {
|
|
||||||
requestVersion += 1
|
|
||||||
initializing = true
|
|
||||||
routeError = null
|
|
||||||
void ensureReaderSession(mangaId, chapterId)
|
|
||||||
.catch((error) => { routeError = error instanceof Error ? error.message : String(error) })
|
|
||||||
.finally(() => { initializing = false })
|
|
||||||
}
|
|
||||||
|
|
||||||
async function returnToSeries() {
|
|
||||||
if (!readerState.manga) return
|
|
||||||
await goto(`/series/${readerState.manga.id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function cycleMode() {
|
|
||||||
readerState.mode = readerState.mode === 'single' ? 'strip' : 'single'
|
|
||||||
if (readerState.mode === 'single') {
|
|
||||||
readerState.inspectScale = 1
|
|
||||||
readerState.inspectPanX = 0
|
|
||||||
readerState.inspectPanY = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleBookmarkAction() {
|
|
||||||
if (!readerState.chapter || !readerState.manga) return
|
|
||||||
const cid = readerState.chapter.id
|
|
||||||
if (getBookmark(cid)) {
|
|
||||||
removeBookmark(cid)
|
|
||||||
} else {
|
|
||||||
addBookmark({
|
|
||||||
mangaId: readerState.manga.id,
|
|
||||||
chapterId: cid,
|
|
||||||
pageNumber: readerState.currentPage,
|
|
||||||
mangaTitle: readerState.manga.title,
|
|
||||||
chapterName: readerState.chapter.name,
|
|
||||||
thumbnailUrl: readerState.manga.thumbnailUrl,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Keybind handler (via shared factory) ----
|
|
||||||
|
|
||||||
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 n = getAdjacentChapters()
|
|
||||||
if (readerState.manga && n.next) void goto(`/reader/${readerState.manga.id}/${n.next.id}`)
|
|
||||||
},
|
|
||||||
chapterPrev: () => {
|
|
||||||
const n = getAdjacentChapters()
|
|
||||||
if (readerState.manga && n.previous) void goto(`/reader/${readerState.manga.id}/${n.previous.id}`)
|
|
||||||
},
|
|
||||||
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' },
|
|
||||||
openSettings: () => void goto('/settings/general'),
|
|
||||||
toggleBookmark: toggleBookmarkAction,
|
|
||||||
toggleAutoScroll: () => { readerState.autoScrollActive = !readerState.autoScrollActive },
|
|
||||||
getKeybinds: () => settingsState.keybinds,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window onkeydown={handleKeydown} />
|
|
||||||
|
|
||||||
<section class="reader-page">
|
|
||||||
<header class="reader-toolbar">
|
|
||||||
<div class="reader-meta">
|
|
||||||
<Button variant="ghost" size="sm" onclick={returnToSeries}>
|
|
||||||
<ArrowArcLeft size={16} weight="bold" />
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div class="reader-titles">
|
|
||||||
<p class="eyebrow">{readerState.manga?.title ?? 'Reader'}</p>
|
|
||||||
<h1>{readerState.chapter?.name ?? 'Loading chapter'}</h1>
|
|
||||||
<p class="subcopy">{chapterLabel} · {pageLabel}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="reader-actions">
|
|
||||||
<div class="toggle-group">
|
|
||||||
<button
|
|
||||||
class:active={readerState.mode === 'single'}
|
|
||||||
type="button"
|
|
||||||
onclick={() => (readerState.mode = 'single')}
|
|
||||||
aria-pressed={readerState.mode === 'single'}
|
|
||||||
>
|
|
||||||
<Columns size={16} weight="bold" />
|
|
||||||
Single
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class:active={readerState.mode === 'strip'}
|
|
||||||
type="button"
|
|
||||||
onclick={() => (readerState.mode = 'strip')}
|
|
||||||
aria-pressed={readerState.mode === 'strip'}
|
|
||||||
>
|
|
||||||
<List size={16} weight="bold" />
|
|
||||||
Strip
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="direction-toggle"
|
|
||||||
type="button"
|
|
||||||
onclick={() => (readerState.direction = readerState.direction === 'ltr' ? 'rtl' : 'ltr')}
|
|
||||||
>
|
|
||||||
<TextAlignRight size={16} weight="bold" />
|
|
||||||
{readerState.direction.toUpperCase()}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="zoom-controls">
|
|
||||||
<button
|
|
||||||
class="zoom-btn"
|
|
||||||
type="button"
|
|
||||||
onclick={() => { readerState.zoom = adjustZoom(readerState.zoom, -ZOOM_STEP) }}
|
|
||||||
aria-label="Zoom out"
|
|
||||||
title="Zoom out (Ctrl -)"
|
|
||||||
>−</button>
|
|
||||||
<button
|
|
||||||
class="zoom-label"
|
|
||||||
type="button"
|
|
||||||
onclick={() => { readerState.zoom = 1; readerState.inspectScale = 1 }}
|
|
||||||
title="Reset zoom"
|
|
||||||
aria-label="Reset zoom"
|
|
||||||
>
|
|
||||||
<MagnifyingGlass size={12} weight="bold" />
|
|
||||||
{zoomPct}%
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="zoom-btn"
|
|
||||||
type="button"
|
|
||||||
onclick={() => { readerState.zoom = adjustZoom(readerState.zoom, ZOOM_STEP) }}
|
|
||||||
aria-label="Zoom in"
|
|
||||||
title="Zoom in (Ctrl +)"
|
|
||||||
>+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="reader-progress">
|
|
||||||
<div class="progress-copy">
|
|
||||||
<span>{progressPercent}% read</span>
|
|
||||||
<span>{pageLabel}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1"
|
|
||||||
max={Math.max(totalPages, 1)}
|
|
||||||
value={Math.min(Math.max(currentPageNumber, 1), Math.max(totalPages, 1))}
|
|
||||||
oninput={handleRangeInput}
|
|
||||||
disabled={totalPages === 0}
|
|
||||||
aria-label="Reader progress"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="reader-stage"
|
|
||||||
bind:this={stageEl}
|
|
||||||
onpointerdown={pinchTracker.onPointerDown}
|
|
||||||
onpointermove={pinchTracker.onPointerMove}
|
|
||||||
onpointerup={pinchTracker.onPointerUp}
|
|
||||||
onpointercancel={pinchTracker.onPointerUp}
|
|
||||||
>
|
|
||||||
{#if initializing && readerState.pages.length === 0}
|
|
||||||
<div class="reader-status">
|
|
||||||
<span class="spin"><SpinnerGap size={22} weight="bold" /></span>
|
|
||||||
<p>Loading chapter pages...</p>
|
|
||||||
</div>
|
|
||||||
{:else if routeError || readerState.pagesError}
|
|
||||||
<div class="reader-status error">
|
|
||||||
<p>{routeError ?? readerState.pagesError}</p>
|
|
||||||
<Button onclick={retryLoad}>Retry</Button>
|
|
||||||
</div>
|
|
||||||
{:else if totalPages === 0}
|
|
||||||
<div class="reader-status">
|
|
||||||
<p>No pages were returned for this chapter.</p>
|
|
||||||
</div>
|
|
||||||
{:else if readerState.mode === 'strip'}
|
|
||||||
<div class="strip-view" style="zoom: {readerState.zoom}">
|
|
||||||
{#each readerState.pages as pageData, index (pageData.index)}
|
|
||||||
<button
|
|
||||||
class="strip-page"
|
|
||||||
class:current={index === readerState.currentPage}
|
|
||||||
type="button"
|
|
||||||
onclick={() => void setCurrentReaderPage(index)}
|
|
||||||
aria-label={`Open page ${index + 1}`}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={pageData.imageData ?? pageData.url}
|
|
||||||
alt={`Page ${index + 1}`}
|
|
||||||
loading="lazy"
|
|
||||||
data-page-index={index}
|
|
||||||
/>
|
|
||||||
<span>Page {index + 1}</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div
|
|
||||||
class="single-view"
|
|
||||||
style={readerState.inspectScale > 1
|
|
||||||
? `cursor: grab; overflow: hidden;`
|
|
||||||
: ''}
|
|
||||||
>
|
|
||||||
<button class="edge-nav left" type="button" onclick={() => void stepBackward()} aria-label="Previous page">
|
|
||||||
<CaretLeft size={28} weight="bold" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{#if currentPageData}
|
|
||||||
<img
|
|
||||||
class="single-page"
|
|
||||||
src={currentPageData.imageData ?? currentPageData.url}
|
|
||||||
alt={`Page ${currentPageNumber}`}
|
|
||||||
style={readerState.inspectScale > 1
|
|
||||||
? `transform: scale(${readerState.inspectScale}) translate(${readerState.inspectPanX}px, ${readerState.inspectPanY}px); transform-origin: center; transition: transform 0.1s ease;`
|
|
||||||
: `zoom: ${readerState.zoom}`}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<button class="edge-nav right" type="button" onclick={() => void stepForward()} aria-label="Next page">
|
|
||||||
<CaretRight size={28} weight="bold" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="reader-footer">
|
|
||||||
<Button variant="ghost" onclick={() => void stepBackward()}>
|
|
||||||
<CaretLeft size={16} weight="bold" />
|
|
||||||
{chapterNeighbors.previous && readerState.currentPage === 0 ? 'Prev chapter' : 'Prev page'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button onclick={() => void stepForward()}>
|
|
||||||
{readerState.currentPage >= totalPages - 1 && chapterNeighbors.next ? 'Next chapter' : 'Next page'}
|
|
||||||
<CaretRight size={16} weight="bold" />
|
|
||||||
</Button>
|
|
||||||
</footer>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.reader-page {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto auto 1fr auto;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
height: 100%;
|
|
||||||
padding: var(--sp-4);
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-toolbar,
|
|
||||||
.reader-progress,
|
|
||||||
.reader-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-3) var(--sp-4);
|
|
||||||
border: 1px solid var(--border-dim);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
background: color-mix(in srgb, var(--bg-raised) 88%, transparent);
|
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-meta,
|
|
||||||
.reader-actions,
|
|
||||||
.reader-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-titles {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-titles h1 {
|
|
||||||
margin: 2px 0;
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: var(--text-xl);
|
|
||||||
line-height: var(--leading-tight);
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow,
|
|
||||||
.subcopy,
|
|
||||||
.progress-copy,
|
|
||||||
.strip-page span {
|
|
||||||
color: var(--text-faint);
|
|
||||||
font-family: var(--font-ui);
|
|
||||||
font-size: var(--text-2xs);
|
|
||||||
letter-spacing: var(--tracking-wide);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-actions {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-group,
|
|
||||||
.direction-toggle {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
border: 1px solid var(--border-dim);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
background: var(--bg-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-group button,
|
|
||||||
.direction-toggle {
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-family: var(--font-ui);
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-group button.active,
|
|
||||||
.direction-toggle:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoom-controls {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid var(--border-dim);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
background: var(--bg-base);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoom-btn,
|
|
||||||
.zoom-label {
|
|
||||||
height: 34px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-family: var(--font-ui);
|
|
||||||
font-size: var(--text-sm);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoom-btn:hover,
|
|
||||||
.zoom-label:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoom-label {
|
|
||||||
min-width: 62px;
|
|
||||||
justify-content: center;
|
|
||||||
border-left: 1px solid var(--border-dim);
|
|
||||||
border-right: 1px solid var(--border-dim);
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-progress {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-copy {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-progress input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-stage {
|
|
||||||
min-height: 0;
|
|
||||||
border: 1px solid var(--border-dim);
|
|
||||||
border-radius: var(--radius-2xl);
|
|
||||||
background: color-mix(in srgb, var(--bg-base) 92%, black 8%);
|
|
||||||
overflow: auto;
|
|
||||||
touch-action: pan-x pan-y;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-status,
|
|
||||||
.single-view,
|
|
||||||
.strip-view {
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-status {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-6);
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-status.error {
|
|
||||||
color: var(--color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spin {
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-view {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(56px, 96px) 1fr minmax(56px, 96px);
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-page {
|
|
||||||
width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edge-nav {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 0;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-faint);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edge-nav:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: color-mix(in srgb, var(--bg-overlay) 44%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.strip-view {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--sp-3);
|
|
||||||
padding: var(--sp-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.strip-page {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--sp-2);
|
|
||||||
justify-items: center;
|
|
||||||
padding: var(--sp-3);
|
|
||||||
border: 1px solid var(--border-dim);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
background: var(--bg-raised);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strip-page.current {
|
|
||||||
border-color: var(--accent);
|
|
||||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 28%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.strip-page img {
|
|
||||||
width: min(100%, 1100px);
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.reader-page {
|
|
||||||
padding: var(--sp-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-toolbar,
|
|
||||||
.reader-footer {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-meta,
|
|
||||||
.reader-actions,
|
|
||||||
.reader-footer {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-view {
|
|
||||||
grid-template-columns: 56px 1fr 56px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
let initializing = $state(true)
|
let initializing = $state(true)
|
||||||
let routeError = $state<string | null>(null)
|
let routeError = $state<string | null>(null)
|
||||||
let requestVersion = 0
|
let requestVersion = 0
|
||||||
@@ -652,11 +23,8 @@
|
|||||||
const totalPages = $derived(readerState.pages.length)
|
const totalPages = $derived(readerState.pages.length)
|
||||||
const progressPercent = $derived(Math.round(progress * 100))
|
const progressPercent = $derived(Math.round(progress * 100))
|
||||||
const pageLabel = $derived(totalPages > 0 ? `${currentPageNumber} / ${totalPages}` : '0 / 0')
|
const pageLabel = $derived(totalPages > 0 ? `${currentPageNumber} / ${totalPages}` : '0 / 0')
|
||||||
const chapterLabel = $derived(
|
const chapterLabel = $derived(readerState.chapter ? `Ch. ${readerState.chapter.chapterNumber}` : 'Chapter')
|
||||||
readerState.chapter
|
const zoomPct = $derived(Math.round(readerState.zoom * 100))
|
||||||
? `Ch. ${Number.isInteger(readerState.chapter.chapterNumber) ? readerState.chapter.chapterNumber : readerState.chapter.chapterNumber}`
|
|
||||||
: 'Chapter'
|
|
||||||
)
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const activeMangaId = mangaId
|
const activeMangaId = mangaId
|
||||||
@@ -679,6 +47,10 @@
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
preloadPages(readerState.pages, readerState.currentPage, settingsState.preloadPages ?? 3)
|
||||||
|
})
|
||||||
|
|
||||||
async function stepForward() {
|
async function stepForward() {
|
||||||
const advanced = await goToNextReaderPage()
|
const advanced = await goToNextReaderPage()
|
||||||
if (advanced) return
|
if (advanced) return
|
||||||
@@ -720,100 +92,67 @@
|
|||||||
await goto(`/series/${readerState.manga.id}`)
|
await goto(`/series/${readerState.manga.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(event: KeyboardEvent) {
|
function cycleMode() {
|
||||||
const binds = settingsState.keybinds
|
readerState.mode = readerState.mode === 'single' ? 'strip' : 'single'
|
||||||
|
}
|
||||||
|
|
||||||
if (matchesKeybind(event, binds.turnPageRight)) {
|
function toggleBookmarkAction() {
|
||||||
event.preventDefault()
|
if (!readerState.chapter || !readerState.manga) return
|
||||||
void (readerState.direction === 'rtl' ? stepBackward() : stepForward())
|
const currentChapterId = readerState.chapter.id
|
||||||
|
|
||||||
|
if (getBookmark(currentChapterId)) {
|
||||||
|
removeBookmark(currentChapterId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchesKeybind(event, binds.turnPageLeft)) {
|
addBookmark({
|
||||||
event.preventDefault()
|
mangaId: readerState.manga.id,
|
||||||
void (readerState.direction === 'rtl' ? stepForward() : stepBackward())
|
chapterId: currentChapterId,
|
||||||
return
|
pageNumber: readerState.currentPage,
|
||||||
}
|
mangaTitle: readerState.manga.title,
|
||||||
|
chapterName: readerState.chapter.name,
|
||||||
|
thumbnailUrl: readerState.manga.thumbnailUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (matchesKeybind(event, binds.firstPage)) {
|
const handleKeydown = createReaderKeyHandler({
|
||||||
event.preventDefault()
|
goNext: () => void (readerState.direction === 'rtl' ? stepBackward() : stepForward()),
|
||||||
void setCurrentReaderPage(0)
|
goPrev: () => void (readerState.direction === 'rtl' ? stepForward() : stepBackward()),
|
||||||
return
|
goToPage: (idx) => void setCurrentReaderPage(idx),
|
||||||
}
|
lastPage: () => readerState.pages.length - 1,
|
||||||
|
exitReader: () => void returnToSeries(),
|
||||||
if (matchesKeybind(event, binds.lastPage)) {
|
chapterNext: () => {
|
||||||
event.preventDefault()
|
|
||||||
void setCurrentReaderPage(readerState.pages.length - 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchesKeybind(event, binds.turnChapterRight)) {
|
|
||||||
event.preventDefault()
|
|
||||||
const neighbors = getAdjacentChapters()
|
const neighbors = getAdjacentChapters()
|
||||||
if (readerState.manga && neighbors.next) {
|
if (readerState.manga && neighbors.next) {
|
||||||
void goto(`/reader/${readerState.manga.id}/${neighbors.next.id}`)
|
void goto(`/reader/${readerState.manga.id}/${neighbors.next.id}`)
|
||||||
}
|
}
|
||||||
return
|
},
|
||||||
}
|
chapterPrev: () => {
|
||||||
|
|
||||||
if (matchesKeybind(event, binds.turnChapterLeft)) {
|
|
||||||
event.preventDefault()
|
|
||||||
const neighbors = getAdjacentChapters()
|
const neighbors = getAdjacentChapters()
|
||||||
if (readerState.manga && neighbors.previous) {
|
if (readerState.manga && neighbors.previous) {
|
||||||
void goto(`/reader/${readerState.manga.id}/${neighbors.previous.id}`)
|
void goto(`/reader/${readerState.manga.id}/${neighbors.previous.id}`)
|
||||||
}
|
}
|
||||||
return
|
},
|
||||||
}
|
adjustZoom: (delta) => {
|
||||||
|
readerState.zoom = adjustZoom(readerState.zoom, delta)
|
||||||
if (matchesKeybind(event, binds.exitReader)) {
|
},
|
||||||
event.preventDefault()
|
resetZoom: () => {
|
||||||
void returnToSeries()
|
readerState.zoom = 1
|
||||||
return
|
readerState.inspectScale = 1
|
||||||
}
|
readerState.inspectPanX = 0
|
||||||
|
readerState.inspectPanY = 0
|
||||||
if (matchesKeybind(event, binds.toggleReadingDirection)) {
|
},
|
||||||
event.preventDefault()
|
cycleMode,
|
||||||
|
toggleDirection: () => {
|
||||||
readerState.direction = readerState.direction === 'ltr' ? 'rtl' : 'ltr'
|
readerState.direction = readerState.direction === 'ltr' ? 'rtl' : 'ltr'
|
||||||
return
|
},
|
||||||
}
|
openSettings: () => void goto('/settings/general'),
|
||||||
|
toggleBookmark: toggleBookmarkAction,
|
||||||
if (matchesKeybind(event, binds.togglePageStyle)) {
|
toggleAutoScroll: () => {
|
||||||
event.preventDefault()
|
readerState.autoScrollActive = !readerState.autoScrollActive
|
||||||
readerState.mode = readerState.mode === 'single' ? 'strip' : 'single'
|
},
|
||||||
return
|
getKeybinds: () => settingsState.keybinds,
|
||||||
}
|
})
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onkeydown={handleKeydown} />
|
<svelte:window onkeydown={handleKeydown} />
|
||||||
@@ -835,34 +174,29 @@
|
|||||||
|
|
||||||
<div class="reader-actions">
|
<div class="reader-actions">
|
||||||
<div class="toggle-group">
|
<div class="toggle-group">
|
||||||
<button
|
<button class:active={readerState.mode === 'single'} type="button" onclick={() => (readerState.mode = 'single')}>
|
||||||
class:active={readerState.mode === 'single'}
|
|
||||||
type="button"
|
|
||||||
onclick={() => (readerState.mode = 'single')}
|
|
||||||
aria-pressed={readerState.mode === 'single'}
|
|
||||||
>
|
|
||||||
<Columns size={16} weight="bold" />
|
<Columns size={16} weight="bold" />
|
||||||
Single
|
Single
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class:active={readerState.mode === 'strip'} type="button" onclick={() => (readerState.mode = 'strip')}>
|
||||||
class:active={readerState.mode === 'strip'}
|
|
||||||
type="button"
|
|
||||||
onclick={() => (readerState.mode = 'strip')}
|
|
||||||
aria-pressed={readerState.mode === 'strip'}
|
|
||||||
>
|
|
||||||
<List size={16} weight="bold" />
|
<List size={16} weight="bold" />
|
||||||
Strip
|
Strip
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button class="direction-toggle" type="button" onclick={() => (readerState.direction = readerState.direction === 'ltr' ? 'rtl' : 'ltr')}>
|
||||||
class="direction-toggle"
|
|
||||||
type="button"
|
|
||||||
onclick={() => (readerState.direction = readerState.direction === 'ltr' ? 'rtl' : 'ltr')}
|
|
||||||
>
|
|
||||||
<TextAlignRight size={16} weight="bold" />
|
<TextAlignRight size={16} weight="bold" />
|
||||||
{readerState.direction.toUpperCase()}
|
{readerState.direction.toUpperCase()}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="zoom-controls">
|
||||||
|
<button class="zoom-btn" type="button" onclick={() => { readerState.zoom = adjustZoom(readerState.zoom, -ZOOM_STEP) }}>−</button>
|
||||||
|
<button class="zoom-label" type="button" onclick={() => { readerState.zoom = 1; readerState.inspectScale = 1 }}>
|
||||||
|
<MagnifyingGlass size={12} weight="bold" />
|
||||||
|
{zoomPct}%
|
||||||
|
</button>
|
||||||
|
<button class="zoom-btn" type="button" onclick={() => { readerState.zoom = adjustZoom(readerState.zoom, ZOOM_STEP) }}>+</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -898,16 +232,10 @@
|
|||||||
<p>No pages were returned for this chapter.</p>
|
<p>No pages were returned for this chapter.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if readerState.mode === 'strip'}
|
{:else if readerState.mode === 'strip'}
|
||||||
<div class="strip-view">
|
<div class="strip-view" style="zoom: {readerState.zoom}">
|
||||||
{#each readerState.pages as pageData, index (pageData.index)}
|
{#each readerState.pages as pageData, index (pageData.index)}
|
||||||
<button
|
<button class="strip-page" class:current={index === readerState.currentPage} type="button" onclick={() => void setCurrentReaderPage(index)}>
|
||||||
class="strip-page"
|
<img src={pageData.imageData ?? pageData.url} alt={`Page ${index + 1}`} loading="lazy" data-page-index={index} />
|
||||||
class:current={index === readerState.currentPage}
|
|
||||||
type="button"
|
|
||||||
onclick={() => void setCurrentReaderPage(index)}
|
|
||||||
aria-label={`Open page ${index + 1}`}
|
|
||||||
>
|
|
||||||
<img src={pageData.imageData ?? pageData.url} alt={`Page ${index + 1}`} loading="lazy" />
|
|
||||||
<span>Page {index + 1}</span>
|
<span>Page {index + 1}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -919,11 +247,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if currentPageData}
|
{#if currentPageData}
|
||||||
<img
|
<img class="single-page" src={currentPageData.imageData ?? currentPageData.url} alt={`Page ${currentPageNumber}`} />
|
||||||
class="single-page"
|
|
||||||
src={currentPageData.imageData ?? currentPageData.url}
|
|
||||||
alt={`Page ${currentPageNumber}`}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button class="edge-nav right" type="button" onclick={() => void stepForward()} aria-label="Next page">
|
<button class="edge-nav right" type="button" onclick={() => void stepForward()} aria-label="Next page">
|
||||||
@@ -967,7 +291,6 @@
|
|||||||
border: 1px solid var(--border-dim);
|
border: 1px solid var(--border-dim);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
background: color-mix(in srgb, var(--bg-raised) 88%, transparent);
|
background: color-mix(in srgb, var(--bg-raised) 88%, transparent);
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-meta,
|
.reader-meta,
|
||||||
@@ -978,18 +301,11 @@
|
|||||||
gap: var(--sp-3);
|
gap: var(--sp-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-titles {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-titles h1 {
|
.reader-titles h1 {
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: var(--text-xl);
|
font-size: var(--text-xl);
|
||||||
line-height: var(--leading-tight);
|
line-height: var(--leading-tight);
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow,
|
.eyebrow,
|
||||||
@@ -1009,19 +325,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toggle-group,
|
.toggle-group,
|
||||||
.direction-toggle {
|
.direction-toggle,
|
||||||
|
.zoom-controls {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
|
||||||
border: 1px solid var(--border-dim);
|
border: 1px solid var(--border-dim);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
background: var(--bg-base);
|
background: var(--bg-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-group button,
|
.toggle-group button,
|
||||||
.direction-toggle {
|
.direction-toggle,
|
||||||
|
.zoom-btn,
|
||||||
|
.zoom-label {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
padding: 0 12px;
|
padding: 0 10px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -1029,12 +347,11 @@
|
|||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-group button.active,
|
.toggle-group button.active {
|
||||||
.direction-toggle:hover {
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||||
}
|
}
|
||||||
@@ -1110,11 +427,6 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edge-nav:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: color-mix(in srgb, var(--bg-overlay) 44%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.strip-view {
|
.strip-view {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--sp-3);
|
gap: var(--sp-3);
|
||||||
@@ -1141,26 +453,4 @@
|
|||||||
width: min(100%, 1100px);
|
width: min(100%, 1100px);
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.reader-page {
|
|
||||||
padding: var(--sp-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-toolbar,
|
|
||||||
.reader-footer {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reader-meta,
|
|
||||||
.reader-actions,
|
|
||||||
.reader-footer {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-view {
|
|
||||||
grid-template-columns: 56px 1fr 56px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -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]}
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user