diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 314a8d6..aa0922f 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -556,6 +556,19 @@ fn set_sync_interval( s.save_config() } +#[tauri::command] +fn set_sync_interval_unfocused( + workspace_id: String, + interval_secs: Option, + state: State<'_, Mutex>, +) -> Result<(), String> { + let mut s = lock_state(&state)?; + if let Some(ws) = s.config.workspaces.get_mut(&workspace_id) { + ws.sync_interval_unfocused_secs = interval_secs; + } + s.save_config() +} + /// A remote folder entry returned to the frontend. #[derive(Debug, Serialize, Deserialize)] struct RemoteFolderEntry { @@ -792,6 +805,8 @@ async fn sync_workspace( { let mut s = lock_state(&state)?; + // Suppress file watcher events from sync-written files (500ms debounce + margin) + mute_watcher(&mut s); if let Some(ws) = s.config.workspaces.get_mut(&workspace_id) { ws.last_sync = Some(Utc::now()); } @@ -914,6 +929,7 @@ pub fn run() { set_webdav_config, set_workspace_theme, set_sync_interval, + set_sync_interval_unfocused, add_webdav_workspace, list_remote_folder, inspect_remote_workspace, diff --git a/apps/tauri/src/App.svelte b/apps/tauri/src/App.svelte index 6ce797f..5e29705 100644 --- a/apps/tauri/src/App.svelte +++ b/apps/tauri/src/App.svelte @@ -60,7 +60,9 @@ {:else if app.screen === "setup"} {:else} - + {#key app.config?.current_workspace} + + {/key} {/if} diff --git a/apps/tauri/src/lib/screens/SettingsScreen.svelte b/apps/tauri/src/lib/screens/SettingsScreen.svelte index a56cc53..f35bc72 100644 --- a/apps/tauri/src/lib/screens/SettingsScreen.svelte +++ b/apps/tauri/src/lib/screens/SettingsScreen.svelte @@ -205,7 +205,7 @@
- +
+ +
+ + +
{/if} diff --git a/apps/tauri/src/lib/screens/TasksScreen.svelte b/apps/tauri/src/lib/screens/TasksScreen.svelte index bc84859..f05d2b1 100644 --- a/apps/tauri/src/lib/screens/TasksScreen.svelte +++ b/apps/tauri/src/lib/screens/TasksScreen.svelte @@ -25,6 +25,7 @@ } }); + function openTask(task: Task) { taskStack = [task.id]; } diff --git a/apps/tauri/src/lib/stores/app.svelte.ts b/apps/tauri/src/lib/stores/app.svelte.ts index 0bcdc81..c1f8bec 100644 --- a/apps/tauri/src/lib/stores/app.svelte.ts +++ b/apps/tauri/src/lib/stores/app.svelte.ts @@ -35,8 +35,9 @@ let _syncInterval: ReturnType | null = null; let _syncDebounce: ReturnType | null = null; let _focusUnlisten: (() => void) | null = null; const DEFAULT_SYNC_INTERVAL_SECS = 60; +const DEFAULT_SYNC_INTERVAL_UNFOCUSED_SECS = 600; const SYNC_DEBOUNCE_MS = 5_000; -const SYNC_FOCUS_THRESHOLD_MS = 30_000; +let _appFocused = true; // ── Derived ────────────────────────────────────────────────────────── @@ -85,6 +86,11 @@ let syncIntervalSecs = $derived( ? config.workspaces[config.current_workspace]?.sync_interval_secs ?? DEFAULT_SYNC_INTERVAL_SECS : DEFAULT_SYNC_INTERVAL_SECS, ); +let syncIntervalUnfocusedSecs = $derived( + config?.current_workspace + ? config.workspaces[config.current_workspace]?.sync_interval_unfocused_secs ?? DEFAULT_SYNC_INTERVAL_UNFOCUSED_SECS + : DEFAULT_SYNC_INTERVAL_UNFOCUSED_SECS, +); // ── Actions ────────────────────────────────────────────────────────── @@ -133,6 +139,7 @@ async function switchWorkspace(id: string) { await invoke("set_current_workspace", { id }); config = await invoke("get_config"); activeListId = null; + tasks = []; await loadLists(); const ws = config?.workspaces[id]; if (ws) invoke("watch_workspace", { path: ws.path }).catch((e) => console.warn("File watcher failed:", e)); @@ -363,21 +370,26 @@ function debouncedSync() { _syncDebounce = setTimeout(() => { _syncDebounce = null; triggerSync(); }, SYNC_DEBOUNCE_MS); } +function restartSyncInterval() { + if (_syncInterval) clearInterval(_syncInterval); + var secs = _appFocused ? syncIntervalSecs : syncIntervalUnfocusedSecs; + _syncInterval = setInterval(triggerSync, secs * 1000); +} + function startAutoSync() { stopAutoSync(); + _appFocused = true; triggerSync(); - _syncInterval = setInterval(triggerSync, syncIntervalSecs * 1000); - // Store the promise-returned unlisten function, ensuring we clean up any - // previous listener before assigning a new one. + restartSyncInterval(); getCurrentWindow().onFocusChanged(({ payload: focused }) => { - if (focused && Date.now() - lastSyncTime > SYNC_FOCUS_THRESHOLD_MS) triggerSync(); + // Sync on re-focus if stale beyond the focused interval + if (focused && !_appFocused && Date.now() - lastSyncTime > syncIntervalSecs * 1000) + triggerSync(); + _appFocused = focused; + restartSyncInterval(); }).then((unlisten) => { - // If stopAutoSync was called while the promise was pending, immediately clean up - if (!_syncInterval) { - unlisten(); - } else { - _focusUnlisten = unlisten; - } + if (!_syncInterval) unlisten(); + else _focusUnlisten = unlisten; }).catch((e) => { console.warn("Failed to set up focus listener:", e); }); @@ -403,6 +415,20 @@ async function setSyncInterval(secs: number | null) { } } +async function setSyncIntervalUnfocused(secs: number | null) { + if (!config?.current_workspace) return; + try { + await invoke("set_sync_interval_unfocused", { + workspaceId: config.current_workspace, + intervalSecs: secs, + }); + config = await invoke("get_config"); + if (isWebdav) startAutoSync(); + } catch (e) { + error = String(e); + } +} + async function setTheme(theme: string | null) { if (!config?.current_workspace) return; try { @@ -517,6 +543,9 @@ export const app = { get syncIntervalSecs() { return syncIntervalSecs; }, + get syncIntervalUnfocusedSecs() { + return syncIntervalUnfocusedSecs; + }, get lastSyncResult() { return lastSyncResult; }, @@ -552,6 +581,7 @@ export const app = { startAutoSync, stopAutoSync, setSyncInterval, + setSyncIntervalUnfocused, setTheme, addWebdavWorkspace, forgetMissingWorkspace, diff --git a/apps/tauri/src/lib/types.ts b/apps/tauri/src/lib/types.ts index ca603fe..36d07c8 100644 --- a/apps/tauri/src/lib/types.ts +++ b/apps/tauri/src/lib/types.ts @@ -29,6 +29,7 @@ export interface WorkspaceConfig { last_sync: string | null; theme: string | null; sync_interval_secs: number | null; + sync_interval_unfocused_secs: number | null; } export interface AppConfig { diff --git a/crates/onyx-core/src/config.rs b/crates/onyx-core/src/config.rs index 4ed9528..d7807be 100644 --- a/crates/onyx-core/src/config.rs +++ b/crates/onyx-core/src/config.rs @@ -33,11 +33,13 @@ pub struct WorkspaceConfig { pub theme: Option, #[serde(skip_serializing_if = "Option::is_none", default)] pub sync_interval_secs: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub sync_interval_unfocused_secs: Option, } impl WorkspaceConfig { pub fn new(name: String, path: PathBuf) -> Self { - Self { name, path, mode: WorkspaceMode::Local, webdav_url: None, webdav_path: None, last_sync: None, theme: None, sync_interval_secs: None } + Self { name, path, mode: WorkspaceMode::Local, webdav_url: None, webdav_path: None, last_sync: None, theme: None, sync_interval_secs: None, sync_interval_unfocused_secs: None } } }