From 2f90805594ae45b9b26ea5471034d89b5a4609b6 Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Sun, 5 Apr 2026 16:19:43 -0700 Subject: [PATCH] Remove manual sync UI; add auto-sync lifecycle and status Remove deprecated sync mode UI and manual sync controls from SettingsScreen and TasksScreen. Implement an always-full sync mode and a new auto-sync lifecycle in the app store: startAutoSync/stopAutoSync, periodic polling (60s), debounced file-change-triggered sync (5s), and window-focus-triggered sync when last sync is older than 30s. Track syncStatus (idle/synced/error/offline), lastSyncTime, and surface status in the Tasks drawer with a status dot and a manual sync button. Wire auto-sync startup/teardown into loadConfig, switchWorkspace, addWebdavWorkspace, and removeWorkspace. These changes simplify sync configuration by removing legacy sync modes and UI elements, provide robust automatic syncing for WebDAV workspaces, and improve user feedback on sync state and errors. --- .../src/lib/screens/SettingsScreen.svelte | 32 --------- apps/tauri/src/lib/screens/TasksScreen.svelte | 28 ++++++++ apps/tauri/src/lib/stores/app.svelte.ts | 66 +++++++++++++++---- 3 files changed, 81 insertions(+), 45 deletions(-) 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,