Merge pull request #36 from SteelDynamite/fix-bugs

fix-bugs
This commit is contained in:
SteelDynamite 2026-04-06 09:37:52 -07:00 committed by GitHub
commit a508879fab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 85 additions and 14 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
* text=auto eol=lf

View file

@ -556,6 +556,19 @@ fn set_sync_interval(
s.save_config() 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. /// A remote folder entry returned to the frontend.
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct RemoteFolderEntry { struct RemoteFolderEntry {
@ -792,6 +805,8 @@ async fn sync_workspace(
{ {
let mut s = lock_state(&state)?; 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) { if let Some(ws) = s.config.workspaces.get_mut(&workspace_id) {
ws.last_sync = Some(Utc::now()); ws.last_sync = Some(Utc::now());
} }
@ -914,6 +929,7 @@ pub fn run() {
set_webdav_config, set_webdav_config,
set_workspace_theme, set_workspace_theme,
set_sync_interval, set_sync_interval,
set_sync_interval_unfocused,
add_webdav_workspace, add_webdav_workspace,
list_remote_folder, list_remote_folder,
inspect_remote_workspace, inspect_remote_workspace,

View file

@ -60,7 +60,9 @@
{:else if app.screen === "setup"} {:else if app.screen === "setup"}
<SetupScreen cancellable={app.hasWorkspace} /> <SetupScreen cancellable={app.hasWorkspace} />
{:else} {:else}
{#key app.config?.current_workspace}
<TasksScreen /> <TasksScreen />
{/key}
{/if} {/if}
</div> </div>
</div> </div>

View file

@ -205,7 +205,7 @@
</div> </div>
<div class="mt-3"> <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 <select
value={String(app.syncIntervalSecs)} value={String(app.syncIntervalSecs)}
onchange={(e) => { onchange={(e) => {
@ -221,6 +221,24 @@
<option value="600">10 minutes</option> <option value="600">10 minutes</option>
</select> </select>
</div> </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> </section>
{/if} {/if}

View file

@ -25,6 +25,7 @@
} }
}); });
function openTask(task: Task) { function openTask(task: Task) {
taskStack = [task.id]; taskStack = [task.id];
} }

View file

@ -35,8 +35,9 @@ let _syncInterval: ReturnType<typeof setInterval> | null = null;
let _syncDebounce: ReturnType<typeof setTimeout> | null = null; let _syncDebounce: ReturnType<typeof setTimeout> | null = null;
let _focusUnlisten: (() => void) | null = null; let _focusUnlisten: (() => void) | null = null;
const DEFAULT_SYNC_INTERVAL_SECS = 60; const DEFAULT_SYNC_INTERVAL_SECS = 60;
const DEFAULT_SYNC_INTERVAL_UNFOCUSED_SECS = 600;
const SYNC_DEBOUNCE_MS = 5_000; const SYNC_DEBOUNCE_MS = 5_000;
const SYNC_FOCUS_THRESHOLD_MS = 30_000; let _appFocused = true;
// ── Derived ────────────────────────────────────────────────────────── // ── Derived ──────────────────────────────────────────────────────────
@ -85,6 +86,11 @@ let syncIntervalSecs = $derived(
? config.workspaces[config.current_workspace]?.sync_interval_secs ?? DEFAULT_SYNC_INTERVAL_SECS ? config.workspaces[config.current_workspace]?.sync_interval_secs ?? DEFAULT_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 ────────────────────────────────────────────────────────── // ── Actions ──────────────────────────────────────────────────────────
@ -133,6 +139,7 @@ async function switchWorkspace(id: string) {
await invoke("set_current_workspace", { id }); await invoke("set_current_workspace", { id });
config = await invoke<AppConfig>("get_config"); config = await invoke<AppConfig>("get_config");
activeListId = null; activeListId = null;
tasks = [];
await loadLists(); await loadLists();
const ws = config?.workspaces[id]; const ws = config?.workspaces[id];
if (ws) invoke("watch_workspace", { path: ws.path }).catch((e) => console.warn("File watcher failed:", e)); 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); _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() { function startAutoSync() {
stopAutoSync(); stopAutoSync();
_appFocused = true;
triggerSync(); triggerSync();
_syncInterval = setInterval(triggerSync, syncIntervalSecs * 1000); restartSyncInterval();
// Store the promise-returned unlisten function, ensuring we clean up any
// previous listener before assigning a new one.
getCurrentWindow().onFocusChanged(({ payload: focused }) => { 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) => { }).then((unlisten) => {
// If stopAutoSync was called while the promise was pending, immediately clean up if (!_syncInterval) unlisten();
if (!_syncInterval) { else _focusUnlisten = unlisten;
unlisten();
} else {
_focusUnlisten = unlisten;
}
}).catch((e) => { }).catch((e) => {
console.warn("Failed to set up focus listener:", 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) { async function setTheme(theme: string | null) {
if (!config?.current_workspace) return; if (!config?.current_workspace) return;
try { try {
@ -517,6 +543,9 @@ export const app = {
get syncIntervalSecs() { get syncIntervalSecs() {
return syncIntervalSecs; return syncIntervalSecs;
}, },
get syncIntervalUnfocusedSecs() {
return syncIntervalUnfocusedSecs;
},
get lastSyncResult() { get lastSyncResult() {
return lastSyncResult; return lastSyncResult;
}, },
@ -552,6 +581,7 @@ export const app = {
startAutoSync, startAutoSync,
stopAutoSync, stopAutoSync,
setSyncInterval, setSyncInterval,
setSyncIntervalUnfocused,
setTheme, setTheme,
addWebdavWorkspace, addWebdavWorkspace,
forgetMissingWorkspace, forgetMissingWorkspace,

View file

@ -29,6 +29,7 @@ export interface WorkspaceConfig {
last_sync: string | null; last_sync: string | null;
theme: string | null; theme: string | null;
sync_interval_secs: number | null; sync_interval_secs: number | null;
sync_interval_unfocused_secs: number | null;
} }
export interface AppConfig { export interface AppConfig {

View file

@ -33,11 +33,13 @@ pub struct WorkspaceConfig {
pub theme: Option<String>, pub theme: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub sync_interval_secs: Option<u64>, pub sync_interval_secs: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub sync_interval_unfocused_secs: Option<u64>,
} }
impl WorkspaceConfig { impl WorkspaceConfig {
pub fn new(name: String, path: PathBuf) -> Self { 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 }
} }
} }