From 5dfbc80bbe269ed0075066693ee530d4079dd967 Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Fri, 5 Jun 2026 17:42:32 -0500 Subject: [PATCH] Fix: App Pin & Downloads (Filesystem Changes) --- src-tauri/capabilities/default.json | 4 +- src-tauri/src/commands/storage.rs | 50 +- src-tauri/src/lib.rs | 4 + src/hooks.client.ts | 12 +- src/lib/components/chrome/AuthGate.svelte | 31 +- src/lib/components/chrome/SplashScreen.svelte | 650 ++++++++---------- src/lib/components/library/Library.svelte | 22 +- .../settings/sections/SecuritySettings.svelte | 65 ++ .../settings/sections/StorageSettings.svelte | 12 +- src/lib/core/filesystem.ts | 57 +- src/lib/platform-adapters/tauri/adapter.ts | 6 +- src/lib/state/app.svelte.ts | 8 +- src/lib/state/boot.svelte.ts | 13 +- src/lib/state/settings.svelte.ts | 8 +- src/routes/+layout.svelte | 123 ++-- src/routes/+layout.ts | 2 +- 16 files changed, 577 insertions(+), 490 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index df276b5..7c02da2 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -43,6 +43,8 @@ "discord-rpc:allow-disconnect", "discord-rpc:allow-set-activity", "discord-rpc:allow-clear-activity", - "discord-rpc:allow-is-running" + "discord-rpc:allow-is-running", + "dialog:default", + "dialog:allow-open" ] } \ No newline at end of file diff --git a/src-tauri/src/commands/storage.rs b/src-tauri/src/commands/storage.rs index 65289a7..ace7277 100644 --- a/src-tauri/src/commands/storage.rs +++ b/src-tauri/src/commands/storage.rs @@ -2,10 +2,58 @@ use serde::Serialize; use std::path::PathBuf; use sysinfo::Disks; use tauri::Emitter; +use tauri_plugin_store::StoreExt; use walkdir::WalkDir; use crate::server::resolve::suwayomi_data_dir; +// ── Key-value store (used by the frontend via platformService) ──────────────── + +#[tauri::command] +pub fn load_store(app: tauri::AppHandle, key: String) -> Result, String> { + let store = app + .store(format!("{}.json", key)) + .map_err(|e| e.to_string())?; + let value = store.get(&key); + Ok(value.map(|v| v.to_string())) +} + +#[tauri::command] +pub fn save_store(app: tauri::AppHandle, key: String, value: String) -> Result<(), String> { + let store = app + .store(format!("{}.json", key)) + .map_err(|e| e.to_string())?; + let parsed: serde_json::Value = + serde_json::from_str(&value).map_err(|e| e.to_string())?; + store.set(key, parsed); + store.save().map_err(|e| e.to_string()) +} + +// ── Credential store (PIN-encrypted vault, auth tokens) ────────────────────── + +#[tauri::command] +pub fn store_credential(app: tauri::AppHandle, key: String, value: String) -> Result<(), String> { + let store = app + .store("credentials.json") + .map_err(|e| e.to_string())?; + if value.is_empty() { + store.delete(&key); + } else { + store.set(&key, serde_json::Value::String(value)); + } + store.save().map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn get_credential(app: tauri::AppHandle, key: String) -> Result, String> { + let store = app + .store("credentials.json") + .map_err(|e| e.to_string())?; + Ok(store.get(&key).and_then(|v| v.as_str().map(|s| s.to_owned()))) +} + +// ── Disk / downloads storage ───────────────────────────────────────────────── + #[derive(Serialize)] pub struct StorageInfo { pub manga_bytes: u64, @@ -127,4 +175,4 @@ pub async fn migrate_downloads( fs::remove_dir_all(&src_path).map_err(|e| e.to_string())?; Ok(()) -} +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 03c3b2f..3fe8206 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -108,6 +108,10 @@ pub fn run() { commands::backup::auto_backup_app_data, commands::backup::get_auto_backup_dir, commands::backup::read_store_files, + commands::storage::load_store, + commands::storage::save_store, + commands::storage::store_credential, + commands::storage::get_credential, commands::updater::list_releases, commands::updater::download_and_install_update, commands::biometric::windows_hello_authenticate, diff --git a/src/hooks.client.ts b/src/hooks.client.ts index 079934c..b80e60f 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -17,21 +17,17 @@ interface SavedAuth { pass?: string } -async function resolveServerAdapter() { - const { SuwayomiAdapter } = await import('$lib/server-adapters/suwayomi') - return new SuwayomiAdapter() -} - async function boot() { try { const platformAdapter = detectAdapter() initPlatformService(platformAdapter) - await platformAdapter.init() - - const serverAdapter = await resolveServerAdapter() + const { SuwayomiAdapter } = await import('$lib/server-adapters/suwayomi') + const serverAdapter = new SuwayomiAdapter() initRequestManager(serverAdapter) + await platformAdapter.init() + appState.platform = platformAdapter.platform appState.version = await platformAdapter.getVersion() diff --git a/src/lib/components/chrome/AuthGate.svelte b/src/lib/components/chrome/AuthGate.svelte index 44b4efe..48f09ae 100644 --- a/src/lib/components/chrome/AuthGate.svelte +++ b/src/lib/components/chrome/AuthGate.svelte @@ -1,6 +1,6 @@ {#if appState.status === 'auth'} -
+

moku

@@ -56,10 +56,14 @@ {/if} \ No newline at end of file diff --git a/src/lib/components/chrome/SplashScreen.svelte b/src/lib/components/chrome/SplashScreen.svelte index 6722090..ac81fa8 100644 --- a/src/lib/components/chrome/SplashScreen.svelte +++ b/src/lib/components/chrome/SplashScreen.svelte @@ -1,384 +1,336 @@ -
+
{#if showCards} {#if showFps} @@ -386,26 +338,7 @@ {/if} {/if} - {#if mode === "idle" && lockEnabled} -
-
-
- Moku -
-
-

Enter PIN

-
-
- {#each Array(settingsState.settings.appLockPin?.length ?? 4) as _, i} -
- {/each} -
- -
-
-
- - {:else if mode === "idle"} + {#if mode === 'idle'}
@@ -414,60 +347,53 @@

press any key to continue

+ {:else if mode === 'locked'} +
+
+
+ Moku +
+

Enter PIN

+
+ {#each Array(pinLen) as _, i} +
+ {/each} +
+
+ {:else}
{#if !failed && !notConfigured} - + + style="transition:stroke-dasharray 0.4s cubic-bezier(0.4,0,0.2,1)" /> {/if} Moku
-
- {#if failed || notConfigured} -
-

{failed ? "Could not reach server" : "Server not configured"}

-
- - -
-
- {:else} -

{ringFull ? "" : `Initializing server${dots}`}

- {/if} -
- - {#if lockEnabled} -
-
-

Enter PIN

-
-
- {#each Array(settingsState.settings.appLockPin?.length ?? 4) as _, i} -
- {/each} -
- -
+ {#if failed || notConfigured} +
+

{failed ? 'Could not reach server' : 'Server not configured'}

+
+ +
+ {:else} +

{ringFull ? '' : `Initializing server${dots}`}

{/if}
{/if}
\ No newline at end of file diff --git a/src/lib/components/library/Library.svelte b/src/lib/components/library/Library.svelte index 87a751e..26bbece 100644 --- a/src/lib/components/library/Library.svelte +++ b/src/lib/components/library/Library.svelte @@ -6,7 +6,6 @@ import { addToast } from '$lib/state/notifications.svelte' import { updateSettings, settingsState } from '$lib/state/settings.svelte' import { goto } from '$app/navigation' - import { invoke } from '@tauri-apps/api/core' import LibraryToolbar from '$lib/components/library/LibraryToolbar.svelte' import LibraryGrid from '$lib/components/library/LibraryGrid.svelte' import ContextMenu from '$lib/components/shared/ui/ContextMenu.svelte' @@ -16,6 +15,7 @@ Books, Folder, FolderSimple, FolderSimplePlus, Trash, CheckSquare, ArrowSquareOut, ArrowsClockwise, } from 'phosphor-svelte' + import { openMangaFolder, openDownloadsFolder } from '$lib/core/filesystem' const SIDEBAR_W = 52 const TITLEBAR_H = 36 @@ -115,26 +115,6 @@ } catch (e) { console.error(e) } } - async function openMangaFolder(m: Manga) { - let base: string | undefined - try { base = await invoke('get_default_downloads_path') } catch {} - if (!base) { addToast({ kind: 'error', title: 'No downloads path set', body: 'Configure it in Settings → Storage' }); return } - const sanitize = (s: string) => s.replace(/[\/\\?%*:|"<>]/g, '_') - const source = (m as any).source?.displayName ?? (m as any).source?.name ?? '' - const path = source - ? `${base}/mangas/${sanitize(source)}/${sanitize(m.title)}` - : `${base}/mangas/${sanitize(m.title)}` - try { await invoke('open_path', { path }) } - catch (e: any) { addToast({ kind: 'error', title: 'Could not open folder', body: e?.toString?.() ?? path }) } - } - - async function openDownloadsFolder() { - let path: string | undefined - try { path = await invoke('get_default_downloads_path') } catch {} - if (!path) { addToast({ kind: 'error', title: 'No downloads path set', body: 'Configure it in Settings → Storage' }); return } - try { await invoke('open_path', { path }) } - catch (e: any) { addToast({ kind: 'error', title: 'Could not open folder', body: e?.toString?.() ?? path }) } - } async function refreshSingleManga(m: Manga) { if (libraryState.refreshingMangaId !== null) return diff --git a/src/lib/components/settings/sections/SecuritySettings.svelte b/src/lib/components/settings/sections/SecuritySettings.svelte index ed285be..994b513 100644 --- a/src/lib/components/settings/sections/SecuritySettings.svelte +++ b/src/lib/components/settings/sections/SecuritySettings.svelte @@ -31,6 +31,35 @@ let flareTtl = $state(settingsState.settings.flareSolverrSessionTtl ?? 15) let flareFallback = $state(settingsState.settings.flareSolverrAsResponseFallback ?? false) + let lockEnabled = $state(settingsState.settings.appLockEnabled ?? false) + let lockPin = $state(settingsState.settings.appLockEnabled ? (settingsState.settings.appLockPin ?? '') : '') + let lockPinVis = $state(false) + let lockError = $state(null) + let lockSaved = $state(false) + + function onLockToggle() { + lockEnabled = !lockEnabled + lockError = null + lockSaved = false + if (!lockEnabled) { + lockPin = '' + updateSettings({ appLockEnabled: false, appLockPin: '' }) + } + } + + function onLockPinInput() { + lockPin = lockPin.replace(/\D/g, '') + lockError = null + lockSaved = false + } + + function saveLockPin() { + if (lockPin.length < 4) { lockError = 'PIN must be at least 4 digits'; return } + updateSettings({ appLockEnabled: true, appLockPin: lockPin }) + lockSaved = true + setTimeout(() => lockSaved = false, 2000) + } + function normalizeAuthMode(mode: string): 'NONE' | 'BASIC_AUTH' | 'UI_LOGIN' { if (mode === 'BASIC_AUTH' || mode === 'UI_LOGIN' || mode === 'NONE') return mode return 'NONE' @@ -283,6 +312,41 @@
+
+

App Lock

+
+ + {#if lockEnabled} + {#if lockError} +
{lockError}
+ {/if} +
+
+ PIN + Minimum 4 digits +
+
+
+ + +
+ +
+
+ {/if} +
+
+

FlareSolverr

@@ -337,4 +401,5 @@ .s-ghost-btn { display: inline-flex; align-items: center; gap: 5px; background: none; border: none; color: var(--text-faint); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); cursor: pointer; padding: 2px 0; transition: color 0.15s; } .s-ghost-btn:hover:not(:disabled) { color: var(--color-error); } .s-ghost-btn:disabled { opacity: 0.35; cursor: default; } + .s-pin-row { display: flex; align-items: center; gap: 8px; } \ No newline at end of file diff --git a/src/lib/components/settings/sections/StorageSettings.svelte b/src/lib/components/settings/sections/StorageSettings.svelte index 8ff9d58..ecac1fe 100644 --- a/src/lib/components/settings/sections/StorageSettings.svelte +++ b/src/lib/components/settings/sections/StorageSettings.svelte @@ -168,11 +168,11 @@ if (!supportsFilesystem) return storageLoading = true; storageError = null try { - const pathData = await gql<{ downloadsPath: string | null; localSourcePath: string | null }>( - `{ downloadsPath localSourcePath }` + const pathData = await gql<{ settings: { downloadsPath: string | null; localSourcePath: string | null } }>( + `{ settings { downloadsPath localSourcePath } }` ) - const dl = pathData.downloadsPath ?? '' - const loc = pathData.localSourcePath ?? '' + const dl = pathData.settings.downloadsPath ?? '' + const loc = pathData.settings.localSourcePath ?? '' downloadsPathInput = dl; localSourcePathInput = loc confirmedDownloadsPath = dl; confirmedLocalSourcePath = loc updateSettings({ serverDownloadsPath: dl, serverLocalSourcePath: loc }) @@ -218,8 +218,8 @@ if (dlErr || locErr) { pathsFieldError = { ...(dlErr ? { dl: dlErr } : {}), ...(locErr ? { loc: locErr } : {}) }; return } pathsSaving = true try { - await gql(`mutation($path: String!) { setDownloadsPath(input: { location: $path }) { location } }`, { path: dl }) - if (loc) await gql(`mutation($path: String!) { setLocalSourcePath(input: { location: $path }) { location } }`, { path: loc }) + await gql(`mutation($path: String!) { setSettings(input: { settings: { downloadsPath: $path } }) { settings { downloadsPath } } }`, { path: dl }) + if (loc) await gql(`mutation($path: String!) { setSettings(input: { settings: { localSourcePath: $path } }) { settings { localSourcePath } } }`, { path: loc }) updateSettings({ serverDownloadsPath: dl, serverLocalSourcePath: loc }) if (supportsFilesystem && !isExternalServer) { const oldDl = confirmedDownloadsPath || defaultDownloadsPath diff --git a/src/lib/core/filesystem.ts b/src/lib/core/filesystem.ts index a1c2852..28d546f 100644 --- a/src/lib/core/filesystem.ts +++ b/src/lib/core/filesystem.ts @@ -1,16 +1,14 @@ import { platformService } from '$lib/platform-service' -import { seriesState } from '$lib/state/series.svelte' +import { settingsState } from '$lib/state/settings.svelte' import { addToast } from '$lib/state/notifications.svelte' import type { Manga } from '$lib/types' -function sanitizeTitle(title: string): string { - return title.replace(/[\\/:*?"<>|]/g, '').replace(/\s+/g, ' ').trim() +function sanitize(s: string): string { + return s.replace(/[\\/:*?"<>|]/g, '').replace(/\s+/g, ' ').trim() } -async function getDownloadsRoot(): Promise { - let root = (seriesState.settings as any).downloadsPath?.trim() ?? '' - if (!root) root = await platformService.getDefaultDownloadsPath().catch(() => '') - return root +function getDownloadsRoot(): string { + return settingsState.settings?.serverDownloadsPath?.trim() ?? '' } function join(root: string, ...parts: string[]): string { @@ -18,31 +16,42 @@ function join(root: string, ...parts: string[]): string { return [root.replace(/[/\\]$/, ''), ...parts].join(sep) } -export async function openMangaFolder(manga: Manga): Promise { +function checkSupported(): boolean { if (!platformService.isSupported('filesystem')) { addToast({ kind: 'info', title: 'Desktop only', body: 'Opening folders requires the desktop app.' }) - return + return false } - const root = await getDownloadsRoot() - if (!root) return - await platformService.openPath(join(root, 'mangas', sanitizeTitle(manga.title))).catch(console.error) + return true } -export async function openCustomFolder(path: string): Promise { - if (!platformService.isSupported('filesystem')) { - addToast({ kind: 'info', title: 'Desktop only', body: 'Opening folders requires the desktop app.' }) - return +function checkRoot(root: string): boolean { + if (!root) { + addToast({ kind: 'error', title: 'No downloads path set', body: 'Configure it in Settings → Storage' }) + return false } - if (!path?.trim()) return + return true +} + +export async function openMangaFolder(manga: Manga): Promise { + if (!checkSupported()) return + const root = getDownloadsRoot() + if (!checkRoot(root)) return + const source = (manga as any).source?.displayName ?? (manga as any).source?.name ?? '' + const path = source + ? join(root, 'mangas', sanitize(source), sanitize(manga.title)) + : join(root, 'mangas', sanitize(manga.title)) await platformService.openPath(path).catch(console.error) } export async function openDownloadsFolder(): Promise { - if (!platformService.isSupported('filesystem')) { - addToast({ kind: 'info', title: 'Desktop only', body: 'Opening folders requires the desktop app.' }) - return - } - const root = await getDownloadsRoot() - if (!root) return - await platformService.openPath(join(root, 'mangas')).catch(console.error) + if (!checkSupported()) return + const root = getDownloadsRoot() + if (!checkRoot(root)) return + await platformService.openPath(root).catch(console.error) +} + +export async function openCustomFolder(path: string): Promise { + if (!checkSupported()) return + if (!path?.trim()) return + await platformService.openPath(path).catch(console.error) } \ No newline at end of file diff --git a/src/lib/platform-adapters/tauri/adapter.ts b/src/lib/platform-adapters/tauri/adapter.ts index f45d004..65efcb1 100644 --- a/src/lib/platform-adapters/tauri/adapter.ts +++ b/src/lib/platform-adapters/tauri/adapter.ts @@ -40,7 +40,11 @@ export class TauriAdapter implements PlatformAdapter { async loadStore(key: string): Promise { try { - return await invoke('load_store', { key }) + const raw = await invoke('load_store', { key }) + if (typeof raw === 'string') { + try { return JSON.parse(raw) } catch { return null } + } + return raw } catch { return null } diff --git a/src/lib/state/app.svelte.ts b/src/lib/state/app.svelte.ts index 41547e4..114f272 100644 --- a/src/lib/state/app.svelte.ts +++ b/src/lib/state/app.svelte.ts @@ -1,11 +1,11 @@ import type { Platform } from '$lib/platform-adapters/types' -export type AppStatus = 'booting' | 'not-configured' | 'auth' | 'ready' | 'error' +export type AppStatus = 'booting' | 'not-configured' | 'auth' | 'locked' | 'ready' | 'error' class AppStore { - settingsOpen: boolean = $state(false) - navPage: string = $state('') - scrollPositions: Map = $state(new Map()) + settingsOpen: boolean = $state(false) + navPage: string = $state('') + scrollPositions: Map = $state(new Map()) setSettingsOpen(next: boolean) { this.settingsOpen = next } setNavPage(next: string) { this.navPage = next } diff --git a/src/lib/state/boot.svelte.ts b/src/lib/state/boot.svelte.ts index 5d231e6..c34bdeb 100644 --- a/src/lib/state/boot.svelte.ts +++ b/src/lib/state/boot.svelte.ts @@ -3,6 +3,7 @@ import { initPlatformService } from '$lib/platform-service' import { platformService } from '$lib/platform-service' import { probeServer, loginBasic, loginUI } from '$lib/core/auth' import { appState } from '$lib/state/app.svelte' +import { settingsState } from '$lib/state/settings.svelte' const MAX_ATTEMPTS = 40 const BG_MAX_ATTEMPTS = 120 @@ -31,13 +32,21 @@ export async function initPlatform(): Promise { appState.appDir = await platformService.getAppDir() } +function pinLockEnabled(): boolean { + return ( + settingsState.settings.appLockEnabled === true && + typeof settingsState.settings.appLockPin === 'string' && + settingsState.settings.appLockPin.length >= 4 + ) +} + function handleProbeSuccess(gen: number) { if (gen !== probeGeneration) return boot.failed = false boot.skipped = false boot.serverProbeOk = true appState.authenticated = true - appState.status = 'ready' + appState.status = pinLockEnabled() ? 'locked' : 'ready' } function handleAuthRequired( @@ -144,7 +153,7 @@ export async function submitLogin(): Promise { boot.loginError = null boot.serverProbeOk = true appState.authenticated = true - appState.status = 'ready' + appState.status = pinLockEnabled() ? 'locked' : 'ready' } catch (e: unknown) { boot.loginError = e instanceof Error ? e.message : 'Login failed' } finally { diff --git a/src/lib/state/settings.svelte.ts b/src/lib/state/settings.svelte.ts index b4fef17..0394ae8 100644 --- a/src/lib/state/settings.svelte.ts +++ b/src/lib/state/settings.svelte.ts @@ -2,12 +2,16 @@ import type { Settings } from '$lib/types/settings' import { DEFAULT_SETTINGS } from '$lib/types/settings' import { saveSettings } from '$lib/core/persistence/persist' -export const settingsState = $state({ settings: { ...DEFAULT_SETTINGS } as Settings }) +export const settingsState = $state({ + settings: { ...DEFAULT_SETTINGS } as Settings, + loaded: false, +}) export async function loadSettingsIntoState(raw: unknown) { if (raw && typeof raw === 'object') { Object.assign(settingsState.settings, raw) } + settingsState.loaded = true if (typeof document !== 'undefined') { document.documentElement.style.zoom = String(settingsState.settings.uiZoom ?? 1.0) } @@ -16,7 +20,6 @@ export async function loadSettingsIntoState(raw: unknown) { export function updateSettings(patch: Partial) { Object.assign(settingsState.settings, patch) void saveSettings({ storeVersion: 2, settings: settingsState.settings }) - if (typeof document !== 'undefined' && patch.uiZoom !== undefined) { document.documentElement.style.zoom = String(patch.uiZoom) } @@ -24,5 +27,6 @@ export function updateSettings(patch: Partial) { export function resetSettings() { settingsState.settings = { ...DEFAULT_SETTINGS } + settingsState.loaded = true void saveSettings({ storeVersion: 2, settings: settingsState.settings }) } \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9959683..4df4102 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,22 +1,22 @@