diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a3cdd7..dc66aff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,6 +22,7 @@ "dtolnay", "dyld", "elif", + "errorlevel", "esac", "esbuild", "frontmost", @@ -29,6 +30,7 @@ "gsettings", "icns", "idletime", + "Inno", "KHTML", "launchservices", "libatk", @@ -41,12 +43,15 @@ "libwebkit", "libxdo", "mountpoint", + "msiexec", "msvc", "msys", "Mullvad", "mullvadbrowser", "nodecar", "nodemon", + "norestart", + "NSIS", "ntlm", "objc", "orhun", @@ -67,10 +72,12 @@ "staticlib", "stefanzweifel", "subdirs", + "SUPPRESSMSGBOXES", "swatinem", "sysinfo", "systempreferences", "taskkill", + "tasklist", "tauri", "titlebar", "Torbrowser", @@ -80,6 +87,7 @@ "unrs", "urlencoding", "vercel", + "VERYSILENT", "winreg", "wiremock", "xattr", diff --git a/src-tauri/src/app_auto_updater.rs b/src-tauri/src/app_auto_updater.rs index a705150..386819e 100644 --- a/src-tauri/src/app_auto_updater.rs +++ b/src-tauri/src/app_auto_updater.rs @@ -35,6 +35,15 @@ pub struct AppUpdateInfo { pub published_at: String, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AppUpdateProgress { + pub stage: String, // "downloading", "extracting", "installing", "completed" + pub percentage: Option, + pub speed: Option, // MB/s + pub eta: Option, // estimated time remaining + pub message: String, +} + pub struct AppAutoUpdater { client: Client, } @@ -98,9 +107,7 @@ impl AppAutoUpdater { // For stable builds, look for stable releases (semver format) let stable_releases: Vec<&AppRelease> = releases .iter() - .filter(|release| { - release.tag_name.starts_with('v') && !release.tag_name.starts_with("nightly-") - }) + .filter(|release| release.tag_name.starts_with('v')) .collect(); println!("Found {} stable releases", stable_releases.len()); stable_releases @@ -311,21 +318,48 @@ impl AppAutoUpdater { .to_string(); // Emit download start event - let _ = app_handle.emit("app-update-progress", "Downloading update..."); + let _ = app_handle.emit( + "app-update-progress", + AppUpdateProgress { + stage: "downloading".to_string(), + percentage: Some(0.0), + speed: None, + eta: None, + message: "Starting download...".to_string(), + }, + ); - // Download the update + // Download the update with progress tracking let download_path = self - .download_update(&update_info.download_url, &temp_dir, &filename) + .download_update_with_progress(&update_info.download_url, &temp_dir, &filename, app_handle) .await?; // Emit extraction start event - let _ = app_handle.emit("app-update-progress", "Preparing update..."); + let _ = app_handle.emit( + "app-update-progress", + AppUpdateProgress { + stage: "extracting".to_string(), + percentage: None, + speed: None, + eta: None, + message: "Preparing update...".to_string(), + }, + ); // Extract the update let extracted_app_path = self.extract_update(&download_path, &temp_dir).await?; // Emit installation start event - let _ = app_handle.emit("app-update-progress", "Installing update..."); + let _ = app_handle.emit( + "app-update-progress", + AppUpdateProgress { + stage: "installing".to_string(), + percentage: None, + speed: None, + eta: None, + message: "Installing update...".to_string(), + }, + ); // Install the update (overwrite current app) self.install_update(&extracted_app_path).await?; @@ -334,7 +368,16 @@ impl AppAutoUpdater { let _ = fs::remove_dir_all(&temp_dir); // Emit completion event - let _ = app_handle.emit("app-update-progress", "Update completed. Restarting..."); + let _ = app_handle.emit( + "app-update-progress", + AppUpdateProgress { + stage: "completed".to_string(), + percentage: Some(100.0), + speed: None, + eta: None, + message: "Update completed. Restarting...".to_string(), + }, + ); // Restart the application self.restart_application().await?; @@ -342,12 +385,13 @@ impl AppAutoUpdater { Ok(()) } - /// Download the update file - async fn download_update( + /// Download the update file with progress tracking + async fn download_update_with_progress( &self, download_url: &str, dest_dir: &Path, filename: &str, + app_handle: &tauri::AppHandle, ) -> Result> { let file_path = dest_dir.join(filename); @@ -362,15 +406,75 @@ impl AppAutoUpdater { return Err(format!("Download failed with status: {}", response.status()).into()); } + let total_size = response.content_length().unwrap_or(0); let mut file = fs::File::create(&file_path)?; let mut stream = response.bytes_stream(); + let mut downloaded = 0u64; + let start_time = std::time::Instant::now(); + let mut last_update = std::time::Instant::now(); use futures_util::StreamExt; while let Some(chunk) = stream.next().await { let chunk = chunk?; file.write_all(&chunk)?; + downloaded += chunk.len() as u64; + + // Update progress every 100ms to avoid overwhelming the UI + if last_update.elapsed().as_millis() > 100 { + let elapsed = start_time.elapsed().as_secs_f64(); + let percentage = if total_size > 0 { + (downloaded as f64 / total_size as f64) * 100.0 + } else { + 0.0 + }; + + let speed = if elapsed > 0.0 { + downloaded as f64 / elapsed / 1024.0 / 1024.0 // MB/s + } else { + 0.0 + }; + + let eta = if total_size > 0 && speed > 0.0 { + let remaining_bytes = total_size - downloaded; + let remaining_seconds = (remaining_bytes as f64 / 1024.0 / 1024.0) / speed; + if remaining_seconds < 60.0 { + format!("{}s", remaining_seconds as u32) + } else { + let minutes = remaining_seconds as u32 / 60; + let seconds = remaining_seconds as u32 % 60; + format!("{minutes}m {seconds}s") + } + } else { + "Unknown".to_string() + }; + + let _ = app_handle.emit( + "app-update-progress", + AppUpdateProgress { + stage: "downloading".to_string(), + percentage: Some(percentage), + speed: Some(format!("{speed:.1}")), + eta: Some(eta), + message: format!("Downloading update... {percentage:.1}%"), + }, + ); + + last_update = std::time::Instant::now(); + } } + // Emit final download completion + let _ = app_handle.emit( + "app-update-progress", + AppUpdateProgress { + stage: "downloading".to_string(), + percentage: Some(100.0), + speed: None, + eta: None, + message: "Download completed".to_string(), + }, + ); + Ok(file_path) } diff --git a/src/components/app-update-toast.tsx b/src/components/app-update-toast.tsx index 958e31f..b7f015c 100644 --- a/src/components/app-update-toast.tsx +++ b/src/components/app-update-toast.tsx @@ -1,25 +1,57 @@ "use client"; import { FaDownload, FaTimes } from "react-icons/fa"; -import { LuRefreshCw } from "react-icons/lu"; +import { LuCheckCheck, LuCog, LuRefreshCw } from "react-icons/lu"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; - -interface AppUpdateInfo { - current_version: string; - new_version: string; - release_notes: string; - download_url: string; - is_nightly: boolean; - published_at: string; -} +import type { AppUpdateInfo, AppUpdateProgress } from "@/types"; interface AppUpdateToastProps { updateInfo: AppUpdateInfo; onUpdate: (updateInfo: AppUpdateInfo) => Promise; onDismiss: () => void; isUpdating?: boolean; - updateProgress?: string; + updateProgress?: AppUpdateProgress | null; +} + +function getStageIcon(stage?: string, isUpdating?: boolean) { + if (!isUpdating) { + return ; + } + + switch (stage) { + case "downloading": + return ; + case "extracting": + return ( + + ); + case "installing": + return ( + + ); + case "completed": + return ; + default: + return ( + + ); + } +} + +function getStageDisplayName(stage?: string) { + switch (stage) { + case "downloading": + return "Downloading"; + case "extracting": + return "Extracting"; + case "installing": + return "Installing"; + case "completed": + return "Completed"; + default: + return "Updating"; + } } export function AppUpdateToast({ @@ -33,14 +65,15 @@ export function AppUpdateToast({ await onUpdate(updateInfo); }; + const showProgress = + isUpdating && + updateProgress?.stage === "downloading" && + updateProgress.percentage !== undefined; + return (
- {isUpdating ? ( - - ) : ( - - )} + {getStageIcon(updateProgress?.stage, isUpdating)}
@@ -48,7 +81,9 @@ export function AppUpdateToast({
- Donut Browser Update Available + {isUpdating + ? `${getStageDisplayName(updateProgress?.stage)} Donut Browser Update` + : "Donut Browser Update Available"}
- Update from {updateInfo.current_version} to{" "} - {updateInfo.new_version} + {isUpdating ? ( + updateProgress?.message || "Updating..." + ) : ( + <> + Update from {updateInfo.current_version} to{" "} + {updateInfo.new_version} + + )}
@@ -75,12 +116,51 @@ export function AppUpdateToast({ )}
- {isUpdating && updateProgress && ( -
-

{updateProgress}

+ {/* Download progress */} + {showProgress && updateProgress && ( +
+
+

+ {updateProgress.percentage?.toFixed(1)}% + {updateProgress.speed && ` • ${updateProgress.speed} MB/s`} + {updateProgress.eta && ` • ${updateProgress.eta} remaining`} +

+
+
+
+
)} + {/* Other stage progress (without percentage) */} + {isUpdating && + updateProgress && + updateProgress.stage !== "downloading" && ( +
+

+ {updateProgress.message} +

+ {updateProgress.stage === "extracting" && ( +

+ Preparing update files... +

+ )} + {updateProgress.stage === "installing" && ( +

+ Installing new version... +

+ )} + {updateProgress.stage === "completed" && ( +

+ Update completed! Restarting application... +

+ )} +
+ )} + {!isUpdating && (
)} + {/* App update progress */} + {type === "app-update" && + progress && + "percentage" in progress && + stage === "downloading" && ( +
+
+

+ {progress.percentage.toFixed(1)}% + {progress.speed && ` • ${progress.speed} MB/s`} + {progress.eta && ` • ${progress.eta} remaining`} +

+
+
+
+
+
+ )} + {/* Version update progress */} {type === "version-update" && progress && @@ -288,6 +336,27 @@ export function UnifiedToast(props: ToastProps) { )} )} + + {/* Stage-specific descriptions for app updates */} + {type === "app-update" && !description && ( + <> + {stage === "extracting" && ( +

+ Preparing update files... +

+ )} + {stage === "installing" && ( +

+ Installing new version... +

+ )} + {stage === "completed" && ( +

+ Update completed! Restarting application... +

+ )} + + )}
); diff --git a/src/hooks/use-app-update-notifications.tsx b/src/hooks/use-app-update-notifications.tsx index 53a41cc..699ee34 100644 --- a/src/hooks/use-app-update-notifications.tsx +++ b/src/hooks/use-app-update-notifications.tsx @@ -6,12 +6,13 @@ import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { AppUpdateToast } from "@/components/app-update-toast"; import { showToast } from "@/lib/toast-utils"; -import type { AppUpdateInfo } from "@/types"; +import type { AppUpdateInfo, AppUpdateProgress } from "@/types"; export function useAppUpdateNotifications() { const [updateInfo, setUpdateInfo] = useState(null); const [isUpdating, setIsUpdating] = useState(false); - const [updateProgress, setUpdateProgress] = useState(""); + const [updateProgress, setUpdateProgress] = + useState(null); const [isClient, setIsClient] = useState(false); const [dismissedVersion, setDismissedVersion] = useState(null); @@ -59,7 +60,13 @@ export function useAppUpdateNotifications() { const handleAppUpdate = useCallback(async (appUpdateInfo: AppUpdateInfo) => { try { setIsUpdating(true); - setUpdateProgress("Starting update..."); + setUpdateProgress({ + stage: "downloading", + percentage: 0, + speed: undefined, + eta: undefined, + message: "Starting update...", + }); await invoke("download_and_install_app_update", { updateInfo: appUpdateInfo, @@ -73,7 +80,7 @@ export function useAppUpdateNotifications() { duration: 6000, }); setIsUpdating(false); - setUpdateProgress(""); + setUpdateProgress(null); } }, []); @@ -102,10 +109,21 @@ export function useAppUpdateNotifications() { }, ); - const unlistenProgress = listen("app-update-progress", (event) => { - console.log("App update progress:", event.payload); - setUpdateProgress(event.payload); - }); + const unlistenProgress = listen( + "app-update-progress", + (event) => { + console.log("App update progress:", event.payload); + setUpdateProgress(event.payload); + + // If update is completed, mark as no longer updating after a delay + if (event.payload.stage === "completed") { + setTimeout(() => { + setIsUpdating(false); + setUpdateProgress(null); + }, 2000); + } + }, + ); return () => { void unlistenUpdate.then((unlisten) => { @@ -161,6 +179,7 @@ export function useAppUpdateNotifications() { return { updateInfo, isUpdating, + updateProgress, checkForAppUpdates, checkForAppUpdatesManual, dismissAppUpdate, diff --git a/src/lib/toast-utils.ts b/src/lib/toast-utils.ts index 0d52582..ccb0d8b 100644 --- a/src/lib/toast-utils.ts +++ b/src/lib/toast-utils.ts @@ -46,12 +46,23 @@ interface VersionUpdateToastProps extends BaseToastProps { }; } +interface AppUpdateToastProps extends BaseToastProps { + type: "app-update"; + stage?: "downloading" | "extracting" | "installing" | "completed"; + progress?: { + percentage: number; + speed?: string; + eta?: string; + }; +} + type ToastProps = | SuccessToastProps | ErrorToastProps | DownloadToastProps | LoadingToastProps - | VersionUpdateToastProps; + | VersionUpdateToastProps + | AppUpdateToastProps; export function showToast(props: ToastProps & { id?: string }) { const toastId = props.id ?? `toast-${props.type}-${Date.now()}`; @@ -246,3 +257,27 @@ export function showUnifiedVersionUpdateToast( ...options, }); } + +export function showAppUpdateToast( + title: string, + stage: "downloading" | "extracting" | "installing" | "completed", + options?: { + id?: string; + description?: string; + progress?: { + percentage: number; + speed?: string; + eta?: string; + }; + duration?: number; + }, +) { + return showToast({ + type: "app-update", + title, + stage, + id: options?.id ?? "app-update-progress", + duration: stage === "downloading" ? Number.POSITIVE_INFINITY : 5000, + ...options, + }); +} diff --git a/src/types.ts b/src/types.ts index 2090176..c1b6ef0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,3 +43,11 @@ export interface AppUpdateInfo { is_nightly: boolean; published_at: string; } + +export interface AppUpdateProgress { + stage: string; // "downloading", "extracting", "installing", "completed" + percentage?: number; + speed?: string; // MB/s + eta?: string; // estimated time remaining + message: string; +}