diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index f4230b3..997687e 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -75,6 +75,24 @@ fn remove_dir_best_effort(path: &std::path::Path) { } } +fn wait_until_deletable(path: &std::path::Path, timeout_secs: u64) -> bool { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs); + while std::time::Instant::now() < deadline { + let locked = if path.is_file() { + std::fs::OpenOptions::new().write(true).open(path).is_err() + } else if path.is_dir() { + std::fs::read_dir(path).is_err() + } else { + return true; + }; + if !locked { + return true; + } + std::thread::sleep(std::time::Duration::from_millis(200)); + } + false +} + #[tauri::command] pub async fn clear_moku_cache(app: tauri::AppHandle) -> Result<(), String> { let window = app.get_webview_window("main").ok_or("no main window")?; @@ -92,10 +110,17 @@ pub async fn clear_moku_cache(app: tauri::AppHandle) -> Result<(), String> { pub fn clear_suwayomi_cache() -> Result<(), String> { use crate::server::resolve::suwayomi_data_dir; let data_dir = suwayomi_data_dir(); - for dir in &["cache", "bin/kcef", "cache/kcef"] { + for dir in &["cache/kcef", "logs"] { let p = data_dir.join(dir); if p.exists() { - std::fs::remove_dir_all(&p).map_err(|e| e.to_string())?; + remove_dir_best_effort(&p); + } + } + for dir in &["downloads/thumbnails"] { + let p = data_dir.join(dir); + if p.exists() { + remove_dir_best_effort(&p); + let _ = std::fs::create_dir_all(&p); } } Ok(()) @@ -106,10 +131,19 @@ pub fn reset_suwayomi_data(app: tauri::AppHandle) -> Result<(), String> { use crate::server::resolve::suwayomi_data_dir; crate::server::kill_tachidesk(&app); - std::thread::sleep(std::time::Duration::from_millis(500)); let data_dir = suwayomi_data_dir(); - for entry_name in &["database.mv.db", "extensions", "settings", "logs", "local"] { + let targets = ["database.mv.db", "extensions", "settings", "logs", "local"]; + + // Wait up to 10s for the JVM to release file locks + for entry_name in &targets { + let p = data_dir.join(entry_name); + if p.exists() { + wait_until_deletable(&p, 10); + } + } + + for entry_name in &targets { let p = data_dir.join(entry_name); if p.is_dir() { std::fs::remove_dir_all(&p).map_err(|e| format!("{entry_name}: {e}"))?; diff --git a/src/App.svelte b/src/App.svelte index 8422487..291fb5e 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -91,6 +91,13 @@ return () => clearTimeout(timer); }); + $effect(() => { + if (!appReady) return; + downloadStore.poll(); + const dlInterval = setInterval(() => downloadStore.poll(), 2000); + return () => clearInterval(dlInterval); + }); + $effect(() => { if (store.settings.discordRpc) { initRpc(); @@ -125,9 +132,11 @@ applyZoom(); }); - const unlistenClose = await win.listen("tauri://close-requested", handleCloseRequested); + await initStore(); + startProbe(); + if (store.settings.autoStartServer) { invoke("spawn_server", { binary: store.settings.serverBinary }).catch((err: any) => { if (err?.kind === "NotConfigured") boot.notConfigured = true; @@ -135,20 +144,13 @@ }); } - await initStore(); - startProbe(); - const unlistenDownload = await listen<{ chapterId: number; mangaId: number; progress: number }[]>( "download-progress", e => setActiveDownloads(e.payload), ); - await downloadStore.poll(); - const dlInterval = setInterval(() => downloadStore.poll(), 2000); - return () => { stopProbe(); - clearInterval(dlInterval); unlistenResize(); unlistenScale(); unlistenDownload(); diff --git a/src/features/reader/components/PageView.svelte b/src/features/reader/components/PageView.svelte index 1d3cc1f..e6bb22c 100644 --- a/src/features/reader/components/PageView.svelte +++ b/src/features/reader/components/PageView.svelte @@ -559,15 +559,15 @@ .page-loader-single { width: min(100%, var(--effective-width, 100%)); max-width: var(--effective-width, 100%); - max-height: calc(100vh - 80px); + max-height: calc(var(--visual-vh, 100vh) - 80px); aspect-ratio: 2 / 3; } .img { display: block; user-select: none; image-rendering: auto; } .img:global(.optimize-contrast) { image-rendering: -webkit-optimize-contrast; } :global(.fit-width) { max-width: var(--effective-width, 100%); width: 100%; height: auto; } - :global(.fit-height) { max-height: calc(100vh - 80px); width: auto; max-width: var(--effective-width, 100%); height: auto; } - :global(.fit-screen) { max-width: var(--effective-width, 100%); max-height: calc(100vh - 80px); object-fit: contain; height: auto; } + :global(.fit-height) { max-height: calc(var(--visual-vh, 100vh) - 80px); width: auto; max-width: var(--effective-width, 100%); height: auto; } + :global(.fit-screen) { max-width: var(--effective-width, 100%); max-height: calc(var(--visual-vh, 100vh) - 80px); object-fit: contain; height: auto; } :global(.fit-original) { max-width: 100%; width: auto; height: auto; } :global(.strip-gap) { margin-bottom: 8px; } diff --git a/src/store/boot.svelte.ts b/src/store/boot.svelte.ts index 893dba1..494d77c 100644 --- a/src/store/boot.svelte.ts +++ b/src/store/boot.svelte.ts @@ -4,7 +4,8 @@ import { trackingState } from "@features/tracking/store/tracki import { loadAllStores } from "@core/persistence/persist"; import { notifyReauthSuccess } from "@api/client"; -const MAX_ATTEMPTS = 40; +const MAX_ATTEMPTS = 15; +const BG_MAX_ATTEMPTS = 60; export const boot = $state({ serverProbeOk: false, @@ -26,6 +27,44 @@ export async function initStore() { store.hydrate(saved); } +function handleProbeSuccess(gen: number) { + if (gen !== probeGeneration) return; + boot.serverProbeOk = true; + boot.failed = false; + boot.skipped = false; + trackingState.bootSync().catch(() => {}); +} + +function handleAuthRequired(gen: number) { + if (gen !== probeGeneration) return; + boot.serverProbeOk = true; + boot.failed = false; + const mode = store.settings.serverAuthMode ?? "NONE"; + if (mode === "BASIC_AUTH") { + const user = store.settings.serverAuthUser?.trim() ?? ""; + const pass = store.settings.serverAuthPass?.trim() ?? ""; + if (user && pass) { + loginBasic(user, pass) + .then(() => { if (gen === probeGeneration) trackingState.bootSync().catch(() => {}); }) + .catch(() => { + if (gen !== probeGeneration) return; + boot.loginUser = store.settings.serverAuthUser ?? ""; + boot.loginRequired = true; + }); + return; + } + boot.loginUser = store.settings.serverAuthUser ?? ""; + boot.loginRequired = true; + return; + } + if (mode === "UI_LOGIN") { + boot.loginUser = store.settings.serverAuthUser ?? ""; + boot.loginRequired = true; + return; + } + trackingState.bootSync().catch(() => {}); +} + export function startProbe() { const gen = ++probeGeneration; boot.failed = false; @@ -36,51 +75,36 @@ export function startProbe() { async function probe() { if (gen !== probeGeneration) return; tries++; - const result = await probeServer(); if (gen !== probeGeneration) return; - if (result === "ok") { - boot.serverProbeOk = true; - trackingState.bootSync().catch(() => {}); - return; - } + if (result === "ok") { handleProbeSuccess(gen); return; } + if (result === "auth_required") { handleAuthRequired(gen); return; } + if (tries >= MAX_ATTEMPTS) { boot.failed = true; startBackgroundProbe(gen); return; } - if (result === "auth_required") { - boot.serverProbeOk = true; - const mode = store.settings.serverAuthMode ?? "NONE"; - - if (mode === "BASIC_AUTH") { - const user = store.settings.serverAuthUser?.trim() ?? ""; - const pass = store.settings.serverAuthPass?.trim() ?? ""; - if (user && pass) { - try { - await loginBasic(user, pass); - if (gen !== probeGeneration) return; - trackingState.bootSync().catch(() => {}); - return; - } catch {} - } - boot.loginUser = store.settings.serverAuthUser ?? ""; - boot.loginRequired = true; - return; - } - - if (mode === "UI_LOGIN") { - boot.loginUser = store.settings.serverAuthUser ?? ""; - boot.loginRequired = true; - return; - } - - trackingState.bootSync().catch(() => {}); - return; - } - - if (tries >= MAX_ATTEMPTS) { boot.failed = true; return; } - setTimeout(probe, Math.min(750 + tries * 250, 3000)); + setTimeout(probe, Math.min(300 + tries * 150, 1500)); } - setTimeout(probe, 2000); + setTimeout(probe, 100); +} + +function startBackgroundProbe(gen: number) { + let bgTries = 0; + + async function bgProbe() { + if (gen !== probeGeneration) return; + bgTries++; + const result = await probeServer(); + if (gen !== probeGeneration) return; + + if (result === "ok") { handleProbeSuccess(gen); return; } + if (result === "auth_required") { handleAuthRequired(gen); return; } + if (bgTries >= BG_MAX_ATTEMPTS) return; + + setTimeout(bgProbe, 2000); + } + + setTimeout(bgProbe, 2000); } export function stopProbe() { @@ -94,7 +118,6 @@ export async function submitLogin(onSuccess: () => void): Promise { } boot.loginBusy = true; boot.loginError = null; - try { const mode = store.settings.serverAuthMode ?? "NONE"; if (mode === "UI_LOGIN") { @@ -102,7 +125,6 @@ export async function submitLogin(onSuccess: () => void): Promise { } else { await loginBasic(boot.loginUser.trim(), boot.loginPass.trim()); } - boot.loginRequired = false; boot.sessionExpired = false; boot.skipped = false; @@ -128,10 +150,11 @@ export function retryBoot() { } export function bypassBoot(onReady: () => void) { - probeGeneration++; + const gen = probeGeneration; boot.serverProbeOk = true; boot.loginRequired = false; boot.sessionExpired = false; boot.skipped = true; onReady(); + startBackgroundProbe(gen); } \ No newline at end of file