diff --git a/apps/tauri/src/lib/screens/SettingsScreen.svelte b/apps/tauri/src/lib/screens/SettingsScreen.svelte index c21ee35..32b7c98 100644 --- a/apps/tauri/src/lib/screens/SettingsScreen.svelte +++ b/apps/tauri/src/lib/screens/SettingsScreen.svelte @@ -202,38 +202,6 @@ -
- - -
- {#if app.error} -

{app.error}

- {/if} - {#if ws?.last_sync} - {@const lastSync = new Date(ws.last_sync)} - {@const secsAgo = Math.floor((Date.now() - lastSync.getTime()) / 1000)} - {@const relTime = secsAgo < 60 ? "just now" : secsAgo < 3600 ? `${Math.floor(secsAgo / 60)}m ago` : `${Math.floor(secsAgo / 3600)}h ago`} -

- Last sync: {relTime} - {#if app.lastSyncResult} -  · ↑{app.lastSyncResult.uploaded} ↓{app.lastSyncResult.downloaded} - {/if} -

- {/if} {/if} diff --git a/apps/tauri/src/lib/screens/TasksScreen.svelte b/apps/tauri/src/lib/screens/TasksScreen.svelte index 11a3774..47464be 100644 --- a/apps/tauri/src/lib/screens/TasksScreen.svelte +++ b/apps/tauri/src/lib/screens/TasksScreen.svelte @@ -326,6 +326,34 @@ + +
+ {#if app.isWebdav} +
+ + + + {app.syncing ? "Syncing..." : app.syncStatus === "synced" || app.syncStatus === "idle" ? "Synced" : app.syncStatus === "error" ? "Sync error" : "Offline"} + + + +
+ {:else} + Local workspace + {/if} +
+ diff --git a/apps/tauri/src/lib/stores/app.svelte.ts b/apps/tauri/src/lib/stores/app.svelte.ts index 0bd7158..b10812f 100644 --- a/apps/tauri/src/lib/stores/app.svelte.ts +++ b/apps/tauri/src/lib/stores/app.svelte.ts @@ -1,5 +1,6 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; +import { getCurrentWindow } from "@tauri-apps/api/window"; import type { AppConfig, Task, @@ -11,6 +12,8 @@ import type { // Listen for file system changes from the backend watcher. listen("fs-changed", () => { loadLists(); + // Debounced sync for WebDAV workspaces on local file changes + if (isWebdav) debouncedSync(); }); // ── Reactive state ─────────────────────────────────────────────────── @@ -22,10 +25,17 @@ let activeListId = $state(null); let tasks = $state([]); let osDark = globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false; let syncing = $state(false); -let syncMode = $state<"full" | "push" | "pull">("full"); +let syncStatus = $state<"idle" | "synced" | "error" | "offline">("idle"); let lastSyncResult = $state(null); let error = $state(null); let missingWorkspace = $state(null); +let lastSyncTime = 0; +let _syncInterval: ReturnType | null = null; +let _syncDebounce: ReturnType | null = null; +let _focusUnlisten: (() => void) | null = null; +const SYNC_POLL_MS = 60_000; +const SYNC_DEBOUNCE_MS = 5_000; +const SYNC_FOCUS_THRESHOLD_MS = 30_000; // ── Derived ────────────────────────────────────────────────────────── @@ -64,6 +74,11 @@ let currentTheme = $derived( let isDark = $derived( currentTheme ? DARK_THEMES.has(currentTheme) : osDark, ); +let isWebdav = $derived( + config?.current_workspace + ? config.workspaces[config.current_workspace]?.mode === "webdav" + : false, +); // ── Actions ────────────────────────────────────────────────────────── @@ -83,6 +98,7 @@ async function loadConfig() { if (lists.length > 0 && !activeListId) activeListId = lists[0].id; if (activeListId) await loadTasks(); screen = "tasks"; + if (isWebdav) startAutoSync(); } else { screen = "setup"; } @@ -114,6 +130,7 @@ async function switchWorkspace(id: string) { await loadLists(); const ws = config?.workspaces[id]; if (ws) invoke("watch_workspace", { path: ws.path }).catch((e) => console.warn("File watcher failed:", e)); + if (isWebdav) startAutoSync(); else stopAutoSync(); error = null; } catch (e) { error = String(e); @@ -131,6 +148,7 @@ async function renameWorkspace(id: string, newName: string) { } async function removeWorkspace(id: string) { + stopAutoSync(); try { await invoke("remove_workspace", { id }); config = await invoke("get_config"); @@ -302,29 +320,46 @@ async function setGroupByDueDate(listId: string, enabled: boolean) { } async function triggerSync() { - if (!config?.current_workspace) return; + if (!config?.current_workspace || syncing) return; syncing = true; - error = null; try { const result = await invoke("sync_workspace", { workspaceId: config.current_workspace, - mode: syncMode, + mode: "full", }); lastSyncResult = result; - if (result.errors.length > 0) { - error = result.errors.join("; "); - } + lastSyncTime = Date.now(); + syncStatus = result.errors.length > 0 ? "error" : "synced"; + if (result.errors.length > 0) error = result.errors.join("; "); config = await invoke("get_config"); await loadLists(); } catch (e) { - error = String(e); + const msg = String(e); + syncStatus = msg.includes("timeout") || msg.includes("connect") || msg.includes("network") ? "offline" : "error"; + error = msg; } finally { syncing = false; } } -function setSyncMode(mode: "full" | "push" | "pull") { - syncMode = mode; +function debouncedSync() { + if (_syncDebounce) clearTimeout(_syncDebounce); + _syncDebounce = setTimeout(() => { _syncDebounce = null; triggerSync(); }, SYNC_DEBOUNCE_MS); +} + +function startAutoSync() { + stopAutoSync(); + triggerSync(); + _syncInterval = setInterval(triggerSync, SYNC_POLL_MS); + getCurrentWindow().onFocusChanged(({ payload: focused }) => { + if (focused && Date.now() - lastSyncTime > SYNC_FOCUS_THRESHOLD_MS) triggerSync(); + }).then((unlisten) => { _focusUnlisten = unlisten; }); +} + +function stopAutoSync() { + if (_syncInterval) { clearInterval(_syncInterval); _syncInterval = null; } + if (_syncDebounce) { clearTimeout(_syncDebounce); _syncDebounce = null; } + if (_focusUnlisten) { _focusUnlisten(); _focusUnlisten = null; } } async function setTheme(theme: string | null) { @@ -351,6 +386,7 @@ async function addWebdavWorkspace(name: string, webdavUrl: string, webdavPath: s } screen = "tasks"; error = null; + if (isWebdav) startAutoSync(); } catch (e) { error = String(e); } @@ -420,8 +456,11 @@ export const app = { get syncing() { return syncing; }, - get syncMode() { - return syncMode; + get syncStatus() { + return syncStatus; + }, + get isWebdav() { + return isWebdav; }, get lastSyncResult() { return lastSyncResult; @@ -455,7 +494,8 @@ export const app = { renameList, setGroupByDueDate, triggerSync, - setSyncMode, + startAutoSync, + stopAutoSync, setTheme, addWebdavWorkspace, forgetMissingWorkspace,