Add download progress bar
This commit is contained in:
+7
-10
@@ -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]
|
#[tauri::command]
|
||||||
fn move_entry(
|
fn move_entry(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -308,7 +307,6 @@ fn duplicate_entry(
|
|||||||
Ok(candidate)
|
Ok(candidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Absolute path on disk, used to reveal an entry in the system file manager.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn absolute_path(
|
fn absolute_path(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -761,8 +759,6 @@ fn cloud_list_files(
|
|||||||
sync::list_account_files(&server_url, &token, folder_id.as_deref())
|
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]
|
#[tauri::command]
|
||||||
fn cloud_download_file(
|
fn cloud_download_file(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -770,7 +766,10 @@ fn cloud_download_file(
|
|||||||
file_id: String,
|
file_id: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let (server_url, token) = cloud_credentials(&app, &store)?;
|
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)?;
|
let file = sync::pull_account_file(&server_url, &token, &file_id)?;
|
||||||
|
sync::report_progress(&app, &file.name, 1, 1, true);
|
||||||
|
|
||||||
let bytes = BASE64
|
let bytes = BASE64
|
||||||
.decode(file.content.as_bytes())
|
.decode(file.content.as_bytes())
|
||||||
@@ -791,8 +790,6 @@ fn cloud_list_shared(
|
|||||||
sync::list_shared(&server_url, &token)
|
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]
|
#[tauri::command]
|
||||||
fn cloud_download_document(
|
fn cloud_download_document(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -801,7 +798,10 @@ fn cloud_download_document(
|
|||||||
parent: String,
|
parent: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let (server_url, token) = cloud_credentials(&app, &store)?;
|
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)?;
|
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();
|
let mut name = document.title.replace('/', "-").trim().to_string();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
@@ -872,8 +872,6 @@ pub struct LinkedDocument {
|
|||||||
pub sync_state: Option<String>,
|
pub sync_state: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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]
|
#[tauri::command]
|
||||||
fn cloud_linked_documents(
|
fn cloud_linked_documents(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -908,7 +906,6 @@ pub struct LinkedSpace {
|
|||||||
pub sync_state: Option<String>,
|
pub sync_state: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cloud spaces that have been downloaded, wherever they sit in the workspace.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn cloud_linked_spaces(
|
fn cloud_linked_spaces(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
@@ -968,7 +965,7 @@ fn cloud_clone_space(
|
|||||||
if dir.exists() {
|
if dir.exists() {
|
||||||
return Err(format!("A project named '{}' already exists", project_name));
|
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]
|
#[tauri::command]
|
||||||
|
|||||||
+36
-4
@@ -493,9 +493,39 @@ pub fn push_project(
|
|||||||
Ok(report)
|
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(
|
pub fn clone_space(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
|
app: &tauri::AppHandle,
|
||||||
store: &Store,
|
store: &Store,
|
||||||
project: &str,
|
project: &str,
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
@@ -509,14 +539,19 @@ pub fn clone_space(
|
|||||||
meta.entrypoint = manifest.entrypoint.clone();
|
meta.entrypoint = manifest.entrypoint.clone();
|
||||||
|
|
||||||
let mut report = SyncReport::default();
|
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)?;
|
let remote = pull_file(server_url, token, space_id, &entry.path)?;
|
||||||
write_local(project_dir, &entry.path, &remote.bytes()?)?;
|
write_local(project_dir, &entry.path, &remote.bytes()?)?;
|
||||||
meta.base_hashes.insert(entry.path.clone(), remote.hash);
|
meta.base_hashes.insert(entry.path.clone(), remote.hash);
|
||||||
report.pulled.push(entry.path.clone());
|
report.pulled.push(entry.path.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
report_progress(app, project, total, total, true);
|
||||||
|
|
||||||
meta.last_synced_at = Some(chrono::Utc::now().to_rfc3339());
|
meta.last_synced_at = Some(chrono::Utc::now().to_rfc3339());
|
||||||
store.save_meta(project, &meta)?;
|
store.save_meta(project, &meta)?;
|
||||||
save_base_snapshots(store, project, project_dir, &meta)?;
|
save_base_snapshots(store, project, project_dir, &meta)?;
|
||||||
@@ -652,8 +687,6 @@ pub fn pull_document(
|
|||||||
.map_err(|e| e.to_string())
|
.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(
|
pub fn sync_document(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
@@ -788,7 +821,6 @@ pub fn sync_document(
|
|||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies a resolved cloud document and uploads it.
|
|
||||||
pub fn resolve_document_conflict(
|
pub fn resolve_document_conflict(
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
token: &str,
|
token: &str,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ interface AppState {
|
|||||||
compiling: boolean;
|
compiling: boolean;
|
||||||
lspStatus: LspStatus;
|
lspStatus: LspStatus;
|
||||||
|
|
||||||
|
download: DownloadProgress | null;
|
||||||
syncing: boolean;
|
syncing: boolean;
|
||||||
conflicts: Conflict[];
|
conflicts: Conflict[];
|
||||||
status: string;
|
status: string;
|
||||||
@@ -83,6 +84,7 @@ export const app = $state<AppState>({
|
|||||||
compiling: false,
|
compiling: false,
|
||||||
lspStatus: "off",
|
lspStatus: "off",
|
||||||
|
|
||||||
|
download: null,
|
||||||
syncing: false,
|
syncing: false,
|
||||||
conflicts: [],
|
conflicts: [],
|
||||||
status: "",
|
status: "",
|
||||||
@@ -90,6 +92,31 @@ export const app = $state<AppState>({
|
|||||||
theme: "light",
|
theme: "light",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface DownloadProgress {
|
||||||
|
label: string;
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
done: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let downloadClearTimer: ReturnType<typeof setTimeout> | 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) {
|
export function setError(error: unknown) {
|
||||||
app.error = api.errorMessage(error);
|
app.error = api.errorMessage(error);
|
||||||
app.status = "";
|
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[] {
|
export function cloudBreadcrumbs(): CloudFolder[] {
|
||||||
if (app.cloudFolder === "shared" || app.cloudFolder === null) return [];
|
if (app.cloudFolder === "shared" || app.cloudFolder === null) return [];
|
||||||
|
|
||||||
|
|||||||
+49
-2
@@ -4,6 +4,7 @@
|
|||||||
import { save } from "@tauri-apps/plugin-dialog";
|
import { save } from "@tauri-apps/plugin-dialog";
|
||||||
import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
||||||
import { getCurrentWebview } from "@tauri-apps/api/webview";
|
import { getCurrentWebview } from "@tauri-apps/api/webview";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
import FileViewer from "$lib/components/FileViewer.svelte";
|
import FileViewer from "$lib/components/FileViewer.svelte";
|
||||||
import FileTree from "$lib/components/FileTree.svelte";
|
import FileTree from "$lib/components/FileTree.svelte";
|
||||||
@@ -47,7 +48,9 @@
|
|||||||
scheduleCompile,
|
scheduleCompile,
|
||||||
setError,
|
setError,
|
||||||
setStatus,
|
setStatus,
|
||||||
|
trackDownload,
|
||||||
} from "$lib/ts/state.svelte";
|
} from "$lib/ts/state.svelte";
|
||||||
|
import type { DownloadProgress } from "$lib/ts/state.svelte";
|
||||||
|
|
||||||
type Dialog =
|
type Dialog =
|
||||||
| { kind: "none" }
|
| { kind: "none" }
|
||||||
@@ -77,7 +80,6 @@
|
|||||||
let selectedIsDir = $state(false);
|
let selectedIsDir = $state(false);
|
||||||
let treeDropTarget = $state<string | null>(null);
|
let treeDropTarget = $state<string | null>(null);
|
||||||
|
|
||||||
/** Folder that new files, folders, and imports go into. */
|
|
||||||
const selectedFolder = $derived(
|
const selectedFolder = $derived(
|
||||||
!selectedEntry
|
!selectedEntry
|
||||||
? ""
|
? ""
|
||||||
@@ -337,7 +339,6 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Folder row under the pointer, so an OS drop lands where it is aimed. */
|
|
||||||
function folderUnderPointer(x: number, y: number): string | null {
|
function folderUnderPointer(x: number, y: number): string | null {
|
||||||
const element = document
|
const element = document
|
||||||
.elementFromPoint(x, y)
|
.elementFromPoint(x, y)
|
||||||
@@ -373,6 +374,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
const downloads = listen<DownloadProgress>("download://progress", (event) =>
|
||||||
|
trackDownload(event.payload),
|
||||||
|
);
|
||||||
|
|
||||||
const pending = getCurrentWebview().onDragDropEvent((event) => {
|
const pending = getCurrentWebview().onDragDropEvent((event) => {
|
||||||
if (event.payload.type === "over") {
|
if (event.payload.type === "over") {
|
||||||
dropActive = dropDestination() !== null;
|
dropActive = dropDestination() !== null;
|
||||||
@@ -396,6 +401,7 @@
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
pending.then((unlisten) => unlisten());
|
pending.then((unlisten) => unlisten());
|
||||||
|
downloads.then((unlisten) => unlisten());
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -517,6 +523,47 @@
|
|||||||
<WindowControls />
|
<WindowControls />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{#if app.download}
|
||||||
|
{@const progress = app.download}
|
||||||
|
<div
|
||||||
|
class="flex shrink-0 items-center gap-3 border-b border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={progress.done ? "ph:check-circle" : "ph:cloud-arrow-down"}
|
||||||
|
class="shrink-0 text-base {progress.done
|
||||||
|
? 'text-[var(--color-success)]'
|
||||||
|
: 'text-[var(--color-accent)]'}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="shrink-0 text-xs">
|
||||||
|
{progress.done ? "Downloaded" : "Downloading"}
|
||||||
|
<span class="font-medium">{progress.label}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-[var(--color-surface-sunken)]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full transition-all duration-200
|
||||||
|
{progress.done
|
||||||
|
? 'bg-[var(--color-success)]'
|
||||||
|
: 'bg-[var(--color-accent)]'}"
|
||||||
|
style="width: {progress.total > 0
|
||||||
|
? Math.round((progress.current / progress.total) * 100)
|
||||||
|
: 0}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if progress.total > 1}
|
||||||
|
<span
|
||||||
|
class="shrink-0 tabular-nums text-[10px] text-[var(--color-ink-muted)]"
|
||||||
|
>
|
||||||
|
{progress.current} of {progress.total} files
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if app.status || app.error}
|
{#if app.status || app.error}
|
||||||
<div
|
<div
|
||||||
class="flex shrink-0 items-center gap-2 border-b px-3 py-1.5 text-xs
|
class="flex shrink-0 items-center gap-2 border-b px-3 py-1.5 text-xs
|
||||||
|
|||||||
Reference in New Issue
Block a user