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()
}
#[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,

View file

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

View file

@ -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}

View file

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

View file

@ -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,

View file

@ -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 {

View file

@ -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 }
}
}