From 91c595bb42bd10804ed76f5b13dfcf56c6095d88 Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Sat, 18 Jul 2026 20:08:02 -0400 Subject: [PATCH] Add download progress bar --- src-tauri/src/lib.rs | 17 ++++++------- src-tauri/src/sync.rs | 40 +++++++++++++++++++++++++++--- src/lib/ts/state.svelte.ts | 28 ++++++++++++++++++++- src/routes/+page.svelte | 51 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 119 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ece3b18..74b60dc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -228,7 +228,6 @@ fn delete_entry(app: AppHandle, store: State<'_, Store>, path: String) -> Result } } -/// Moves an entry into another folder, keeping its name. #[tauri::command] fn move_entry( app: AppHandle, @@ -308,7 +307,6 @@ fn duplicate_entry( Ok(candidate) } -/// Absolute path on disk, used to reveal an entry in the system file manager. #[tauri::command] fn absolute_path( app: AppHandle, @@ -761,8 +759,6 @@ fn cloud_list_files( sync::list_account_files(&server_url, &token, folder_id.as_deref()) } -/// Downloads an account file into the shared asset library, where every -/// project can reference it by name. #[tauri::command] fn cloud_download_file( app: AppHandle, @@ -770,7 +766,10 @@ fn cloud_download_file( file_id: String, ) -> Result { let (server_url, token) = cloud_credentials(&app, &store)?; + + sync::report_progress(&app, "file", 0, 1, false); let file = sync::pull_account_file(&server_url, &token, &file_id)?; + sync::report_progress(&app, &file.name, 1, 1, true); let bytes = BASE64 .decode(file.content.as_bytes()) @@ -791,8 +790,6 @@ fn cloud_list_shared( sync::list_shared(&server_url, &token) } -/// Downloads a cloud document into the workspace and remembers where it came -/// from so it can be synced back. #[tauri::command] fn cloud_download_document( app: AppHandle, @@ -801,7 +798,10 @@ fn cloud_download_document( parent: String, ) -> Result { let (server_url, token) = cloud_credentials(&app, &store)?; + + sync::report_progress(&app, "document", 0, 1, false); let document = sync::pull_document(&server_url, &token, &document_id)?; + sync::report_progress(&app, &document.title, 1, 1, true); let mut name = document.title.replace('/', "-").trim().to_string(); if name.is_empty() { @@ -872,8 +872,6 @@ pub struct LinkedDocument { pub sync_state: Option, } -/// Every cloud document that has been downloaded, so the cloud view can show -/// which ones live on this device and whether they are up to date. #[tauri::command] fn cloud_linked_documents( app: AppHandle, @@ -908,7 +906,6 @@ pub struct LinkedSpace { pub sync_state: Option, } -/// Cloud spaces that have been downloaded, wherever they sit in the workspace. #[tauri::command] fn cloud_linked_spaces( app: AppHandle, @@ -968,7 +965,7 @@ fn cloud_clone_space( if dir.exists() { return Err(format!("A project named '{}' already exists", project_name)); } - sync::clone_space(&server_url, &token, &store, &project, &dir, &space_id) + sync::clone_space(&server_url, &token, &app, &store, &project, &dir, &space_id) } #[tauri::command] diff --git a/src-tauri/src/sync.rs b/src-tauri/src/sync.rs index b7634e9..4d2ad78 100644 --- a/src-tauri/src/sync.rs +++ b/src-tauri/src/sync.rs @@ -493,9 +493,39 @@ pub fn push_project( Ok(report) } +#[derive(Serialize, Clone)] +pub struct DownloadProgress { + pub label: String, + pub current: usize, + pub total: usize, + pub done: bool, +} + +pub const PROGRESS_EVENT: &str = "download://progress"; + +pub fn report_progress( + app: &tauri::AppHandle, + label: &str, + current: usize, + total: usize, + done: bool, +) { + use tauri::Emitter; + let _ = app.emit( + PROGRESS_EVENT, + DownloadProgress { + label: label.to_string(), + current, + total, + done, + }, + ); +} + pub fn clone_space( server_url: &str, token: &str, + app: &tauri::AppHandle, store: &Store, project: &str, project_dir: &Path, @@ -509,14 +539,19 @@ pub fn clone_space( meta.entrypoint = manifest.entrypoint.clone(); let mut report = SyncReport::default(); + let total = manifest.files.len(); + + for (index, entry) in manifest.files.iter().enumerate() { + report_progress(app, project, index, total, false); - for entry in &manifest.files { let remote = pull_file(server_url, token, space_id, &entry.path)?; write_local(project_dir, &entry.path, &remote.bytes()?)?; meta.base_hashes.insert(entry.path.clone(), remote.hash); report.pulled.push(entry.path.clone()); } + report_progress(app, project, total, total, true); + meta.last_synced_at = Some(chrono::Utc::now().to_rfc3339()); store.save_meta(project, &meta)?; save_base_snapshots(store, project, project_dir, &meta)?; @@ -652,8 +687,6 @@ pub fn pull_document( .map_err(|e| e.to_string()) } -/// Syncs one downloaded cloud document. The merge base is the copy stored when -/// the document was last exchanged with the server. pub fn sync_document( server_url: &str, token: &str, @@ -788,7 +821,6 @@ pub fn sync_document( Ok(report) } -/// Applies a resolved cloud document and uploads it. pub fn resolve_document_conflict( server_url: &str, token: &str, diff --git a/src/lib/ts/state.svelte.ts b/src/lib/ts/state.svelte.ts index d691c61..9dbf082 100644 --- a/src/lib/ts/state.svelte.ts +++ b/src/lib/ts/state.svelte.ts @@ -48,6 +48,7 @@ interface AppState { compiling: boolean; lspStatus: LspStatus; + download: DownloadProgress | null; syncing: boolean; conflicts: Conflict[]; status: string; @@ -83,6 +84,7 @@ export const app = $state({ compiling: false, lspStatus: "off", + download: null, syncing: false, conflicts: [], status: "", @@ -90,6 +92,31 @@ export const app = $state({ theme: "light", }); +export interface DownloadProgress { + label: string; + current: number; + total: number; + done: boolean; +} + +let downloadClearTimer: ReturnType | null = null; + +export function trackDownload(progress: DownloadProgress) { + if (downloadClearTimer) { + clearTimeout(downloadClearTimer); + downloadClearTimer = null; + } + + app.download = progress; + + if (progress.done) { + downloadClearTimer = setTimeout(() => { + app.download = null; + downloadClearTimer = null; + }, 1200); + } +} + export function setError(error: unknown) { app.error = api.errorMessage(error); app.status = ""; @@ -205,7 +232,6 @@ export async function refreshCloud() { } } -/** Trail from the drive root down to the folder being viewed. */ export function cloudBreadcrumbs(): CloudFolder[] { if (app.cloudFolder === "shared" || app.cloudFolder === null) return []; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 56c5f43..8910c92 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -4,6 +4,7 @@ import { save } from "@tauri-apps/plugin-dialog"; import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { getCurrentWebview } from "@tauri-apps/api/webview"; + import { listen } from "@tauri-apps/api/event"; import FileViewer from "$lib/components/FileViewer.svelte"; import FileTree from "$lib/components/FileTree.svelte"; @@ -47,7 +48,9 @@ scheduleCompile, setError, setStatus, + trackDownload, } from "$lib/ts/state.svelte"; + import type { DownloadProgress } from "$lib/ts/state.svelte"; type Dialog = | { kind: "none" } @@ -77,7 +80,6 @@ let selectedIsDir = $state(false); let treeDropTarget = $state(null); - /** Folder that new files, folders, and imports go into. */ const selectedFolder = $derived( !selectedEntry ? "" @@ -337,7 +339,6 @@ return null; } - /** Folder row under the pointer, so an OS drop lands where it is aimed. */ function folderUnderPointer(x: number, y: number): string | null { const element = document .elementFromPoint(x, y) @@ -373,6 +374,10 @@ } onMount(() => { + const downloads = listen("download://progress", (event) => + trackDownload(event.payload), + ); + const pending = getCurrentWebview().onDragDropEvent((event) => { if (event.payload.type === "over") { dropActive = dropDestination() !== null; @@ -396,6 +401,7 @@ return () => { pending.then((unlisten) => unlisten()); + downloads.then((unlisten) => unlisten()); }; }); @@ -517,6 +523,47 @@ + {#if app.download} + {@const progress = app.download} +
+ + + + {progress.done ? "Downloaded" : "Downloading"} + {progress.label} + + +
+
+
+ + {#if progress.total > 1} + + {progress.current} of {progress.total} files + + {/if} +
+ {/if} + {#if app.status || app.error}