commit
a508879fab
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
||||
|
|
@ -556,6 +556,19 @@ fn set_sync_interval(
|
|||
s.save_config()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_sync_interval_unfocused(
|
||||
workspace_id: String,
|
||||
interval_secs: Option<u64>,
|
||||
state: State<'_, Mutex<AppState>>,
|
||||
) -> 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,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@
|
|||
{:else if app.screen === "setup"}
|
||||
<SetupScreen cancellable={app.hasWorkspace} />
|
||||
{:else}
|
||||
{#key app.config?.current_workspace}
|
||||
<TasksScreen />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="mb-1 block text-xs font-medium opacity-60">Sync interval</label>
|
||||
<label class="mb-1 block text-xs font-medium opacity-60">Sync interval (focused)</label>
|
||||
<select
|
||||
value={String(app.syncIntervalSecs)}
|
||||
onchange={(e) => {
|
||||
|
|
@ -221,6 +221,24 @@
|
|||
<option value="600">10 minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="mb-1 block text-xs font-medium opacity-60">Sync interval (background)</label>
|
||||
<select
|
||||
value={String(app.syncIntervalUnfocusedSecs)}
|
||||
onchange={(e) => {
|
||||
const val = parseInt((e.target as HTMLSelectElement).value);
|
||||
app.setSyncIntervalUnfocused(val === 600 ? null : val);
|
||||
}}
|
||||
class="w-full 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="60">1 minute</option>
|
||||
<option value="120">2 minutes</option>
|
||||
<option value="300">5 minutes</option>
|
||||
<option value="600">10 minutes</option>
|
||||
<option value="1800">30 minutes</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
function openTask(task: Task) {
|
||||
taskStack = [task.id];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,8 +35,9 @@ let _syncInterval: ReturnType<typeof setInterval> | null = null;
|
|||
let _syncDebounce: ReturnType<typeof setTimeout> | 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<AppConfig>("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<AppConfig>("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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -33,11 +33,13 @@ pub struct WorkspaceConfig {
|
|||
pub theme: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub sync_interval_secs: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub sync_interval_unfocused_secs: Option<u64>,
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue