feat: sync status indicators and push/pull mode selection
Add workspace_name and mode params to sync_workspace Tauri command. After each sync, persist last_sync to WorkspaceConfig and save config. Frontend gains lastSyncResult and syncMode state; triggerSync reloads config post-sync and stores the result. SettingsScreen shows a sync direction selector (Full/Push/Pull) and a last-sync status line. TasksScreen shows an upload/download count chip after sync completes.
This commit is contained in:
parent
72475a552a
commit
70d519de64
|
|
@ -2,6 +2,8 @@ use std::path::PathBuf;
|
|||
use std::sync::Mutex;
|
||||
use std::time::Instant;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{Emitter, Manager, State};
|
||||
|
|
@ -422,21 +424,40 @@ async fn test_webdav_connection(
|
|||
|
||||
#[tauri::command]
|
||||
async fn sync_workspace(
|
||||
workspace_name: String,
|
||||
workspace_path: String,
|
||||
webdav_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
mode: String,
|
||||
state: State<'_, Mutex<AppState>>,
|
||||
) -> Result<SyncResult, String> {
|
||||
let sync_mode = match mode.as_str() {
|
||||
"push" => SyncMode::Push,
|
||||
"pull" => SyncMode::Pull,
|
||||
_ => SyncMode::Full,
|
||||
};
|
||||
let result = sync::sync_workspace(
|
||||
&PathBuf::from(workspace_path),
|
||||
&PathBuf::from(&workspace_path),
|
||||
&webdav_url,
|
||||
&username,
|
||||
&password,
|
||||
SyncMode::Full,
|
||||
sync_mode,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Persist last_sync timestamp to config
|
||||
{
|
||||
let mut s = state.lock().unwrap();
|
||||
if let Some(ws) = s.config.workspaces.get_mut(&workspace_name) {
|
||||
ws.last_sync = Some(Utc::now());
|
||||
}
|
||||
let config_path = AppConfig::get_config_path();
|
||||
s.config.save_to_file(&config_path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -122,13 +122,35 @@
|
|||
</div>
|
||||
|
||||
{#if app.config?.current_workspace}
|
||||
<button
|
||||
onclick={() => app.triggerSync()}
|
||||
disabled={app.syncing}
|
||||
class="mt-3 w-full rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-hover disabled:opacity-40"
|
||||
>
|
||||
{app.syncing ? "Syncing…" : "Sync Now"}
|
||||
</button>
|
||||
<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="rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm outline-none focus:border-primary dark:border-border-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.config.workspaces[app.config.current_workspace]?.last_sync}
|
||||
{@const lastSync = new Date(app.config.workspaces[app.config.current_workspace].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}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -550,9 +550,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync spinner -->
|
||||
<!-- Sync status indicator -->
|
||||
{#if app.syncing}
|
||||
<div class="absolute bottom-4 right-4 z-20 h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
{:else if app.lastSyncResult}
|
||||
<div class="absolute bottom-4 right-4 z-20 flex items-center gap-1 rounded-full bg-black/10 px-2.5 py-1 text-xs opacity-60 dark:bg-white/10">
|
||||
<span>↑{app.lastSyncResult.uploaded}</span>
|
||||
<span>↓{app.lastSyncResult.downloaded}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ let darkMode = $state(
|
|||
globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false,
|
||||
);
|
||||
let syncing = $state(false);
|
||||
let syncMode = $state<"full" | "push" | "pull">("full");
|
||||
let lastSyncResult = $state<SyncResult | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// ── Derived ──────────────────────────────────────────────────────────
|
||||
|
|
@ -254,7 +256,8 @@ async function setGroupByDueDate(listId: string, enabled: boolean) {
|
|||
|
||||
async function triggerSync() {
|
||||
if (!config?.current_workspace) return;
|
||||
const ws = config.workspaces[config.current_workspace];
|
||||
const workspaceName = config.current_workspace;
|
||||
const ws = config.workspaces[workspaceName];
|
||||
if (!ws?.webdav_url) {
|
||||
error = "No WebDAV URL configured";
|
||||
return;
|
||||
|
|
@ -265,14 +268,19 @@ async function triggerSync() {
|
|||
const domain = new URL(ws.webdav_url).hostname;
|
||||
const [username, password] = await invoke<[string, string]>("load_credentials", { domain });
|
||||
const result = await invoke<SyncResult>("sync_workspace", {
|
||||
workspaceName,
|
||||
workspacePath: ws.path,
|
||||
webdavUrl: ws.webdav_url,
|
||||
username,
|
||||
password,
|
||||
mode: syncMode,
|
||||
});
|
||||
lastSyncResult = result;
|
||||
if (result.errors.length > 0) {
|
||||
error = result.errors.join("; ");
|
||||
}
|
||||
// Reload config to pick up updated last_sync timestamp
|
||||
config = await invoke<AppConfig>("get_config");
|
||||
await loadLists();
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
|
|
@ -281,6 +289,10 @@ async function triggerSync() {
|
|||
}
|
||||
}
|
||||
|
||||
function setSyncMode(mode: "full" | "push" | "pull") {
|
||||
syncMode = mode;
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
darkMode = !darkMode;
|
||||
}
|
||||
|
|
@ -326,6 +338,12 @@ export const app = {
|
|||
get syncing() {
|
||||
return syncing;
|
||||
},
|
||||
get syncMode() {
|
||||
return syncMode;
|
||||
},
|
||||
get lastSyncResult() {
|
||||
return lastSyncResult;
|
||||
},
|
||||
get error() {
|
||||
return error;
|
||||
},
|
||||
|
|
@ -350,6 +368,7 @@ export const app = {
|
|||
renameList,
|
||||
setGroupByDueDate,
|
||||
triggerSync,
|
||||
setSyncMode,
|
||||
toggleDarkMode,
|
||||
setScreen,
|
||||
clearError,
|
||||
|
|
|
|||
Loading…
Reference in a new issue