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.
This commit is contained in:
parent
e33fb9dd0b
commit
2f90805594
|
|
@ -202,38 +202,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<select
|
||||
value={app.syncMode}
|
||||
onchange={(e) => app.setSyncMode((e.target as HTMLSelectElement).value as "full" | "push" | "pull")}
|
||||
class="appearance-none rounded-lg border border-border-light bg-surface-light px-3 py-2 text-sm text-text-light outline-none focus:border-primary dark:border-border-dark dark:bg-surface-dark dark:text-text-dark"
|
||||
>
|
||||
<option value="full">Sync both ways</option>
|
||||
<option value="push">Push only</option>
|
||||
<option value="pull">Pull only</option>
|
||||
</select>
|
||||
<button
|
||||
onclick={() => app.triggerSync()}
|
||||
disabled={app.syncing}
|
||||
class="flex-1 rounded-lg bg-primary py-2 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
||||
>
|
||||
{app.syncing ? "Syncing..." : "Sync Now"}
|
||||
</button>
|
||||
</div>
|
||||
{#if app.error}
|
||||
<p class="mt-1.5 text-xs text-danger">{app.error}</p>
|
||||
{/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`}
|
||||
<p class="mt-1.5 text-xs opacity-40">
|
||||
Last sync: {relTime}
|
||||
{#if app.lastSyncResult}
|
||||
· ↑{app.lastSyncResult.uploaded} ↓{app.lastSyncResult.downloaded}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -326,6 +326,34 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drawer footer: sync status -->
|
||||
<div class="shrink-0 border-t border-border-light px-4 py-2.5 dark:border-border-dark">
|
||||
{#if app.isWebdav}
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Status dot -->
|
||||
<span
|
||||
class="inline-block h-2 w-2 rounded-full {app.syncing ? 'animate-pulse bg-primary' : app.syncStatus === 'synced' || app.syncStatus === 'idle' ? 'bg-green-500' : app.syncStatus === 'error' ? 'bg-red-500' : 'bg-gray-400'}"
|
||||
></span>
|
||||
<span class="flex-1 text-xs opacity-60">
|
||||
{app.syncing ? "Syncing..." : app.syncStatus === "synced" || app.syncStatus === "idle" ? "Synced" : app.syncStatus === "error" ? "Sync error" : "Offline"}
|
||||
</span>
|
||||
<!-- Manual sync button -->
|
||||
<button
|
||||
onclick={() => app.triggerSync()}
|
||||
disabled={app.syncing}
|
||||
class="rounded-lg p-1.5 hover:bg-black/5 disabled:opacity-30 dark:hover:bg-white/10"
|
||||
title="Sync now"
|
||||
>
|
||||
<svg class="h-4 w-4 {app.syncing ? 'animate-spin' : ''}" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-xs opacity-40">Local workspace</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Main content panel -->
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
|||
let tasks = $state<Task[]>([]);
|
||||
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<SyncResult | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
let missingWorkspace = $state<string | null>(null);
|
||||
let lastSyncTime = 0;
|
||||
let _syncInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let _syncDebounce: ReturnType<typeof setTimeout> | 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<AppConfig>("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<SyncResult>("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<AppConfig>("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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue