mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-30 07:37:51 +02:00
feat: show donwload bar for app self-update
This commit is contained in:
Vendored
+8
@@ -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",
|
||||
|
||||
@@ -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<f64>,
|
||||
pub speed: Option<String>, // MB/s
|
||||
pub eta: Option<String>, // 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<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void>;
|
||||
onDismiss: () => void;
|
||||
isUpdating?: boolean;
|
||||
updateProgress?: string;
|
||||
updateProgress?: AppUpdateProgress | null;
|
||||
}
|
||||
|
||||
function getStageIcon(stage?: string, isUpdating?: boolean) {
|
||||
if (!isUpdating) {
|
||||
return <FaDownload className="flex-shrink-0 w-5 h-5 text-blue-500" />;
|
||||
}
|
||||
|
||||
switch (stage) {
|
||||
case "downloading":
|
||||
return <FaDownload className="flex-shrink-0 w-5 h-5 text-blue-500" />;
|
||||
case "extracting":
|
||||
return (
|
||||
<LuRefreshCw className="flex-shrink-0 w-5 h-5 text-blue-500 animate-spin" />
|
||||
);
|
||||
case "installing":
|
||||
return (
|
||||
<LuCog className="flex-shrink-0 w-5 h-5 text-blue-500 animate-spin" />
|
||||
);
|
||||
case "completed":
|
||||
return <LuCheckCheck className="flex-shrink-0 w-5 h-5 text-green-500" />;
|
||||
default:
|
||||
return (
|
||||
<LuRefreshCw className="flex-shrink-0 w-5 h-5 text-blue-500 animate-spin" />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex items-start p-4 w-full max-w-md bg-white rounded-lg border border-gray-200 shadow-lg dark:bg-gray-800 dark:border-gray-700">
|
||||
<div className="mr-3 mt-0.5">
|
||||
{isUpdating ? (
|
||||
<LuRefreshCw className="flex-shrink-0 w-5 h-5 text-blue-500 animate-spin" />
|
||||
) : (
|
||||
<FaDownload className="flex-shrink-0 w-5 h-5 text-blue-500" />
|
||||
)}
|
||||
{getStageIcon(updateProgress?.stage, isUpdating)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -48,7 +81,9 @@ export function AppUpdateToast({
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="text-sm font-semibold text-foreground">
|
||||
Donut Browser Update Available
|
||||
{isUpdating
|
||||
? `${getStageDisplayName(updateProgress?.stage)} Donut Browser Update`
|
||||
: "Donut Browser Update Available"}
|
||||
</span>
|
||||
<Badge
|
||||
variant={updateInfo.is_nightly ? "secondary" : "default"}
|
||||
@@ -58,8 +93,14 @@ export function AppUpdateToast({
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Update from {updateInfo.current_version} to{" "}
|
||||
<span className="font-medium">{updateInfo.new_version}</span>
|
||||
{isUpdating ? (
|
||||
updateProgress?.message || "Updating..."
|
||||
) : (
|
||||
<>
|
||||
Update from {updateInfo.current_version} to{" "}
|
||||
<span className="font-medium">{updateInfo.new_version}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,12 +116,51 @@ export function AppUpdateToast({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isUpdating && updateProgress && (
|
||||
<div className="mt-2">
|
||||
<p className="text-xs text-muted-foreground">{updateProgress}</p>
|
||||
{/* Download progress */}
|
||||
{showProgress && updateProgress && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="flex-1 min-w-0 text-xs text-muted-foreground">
|
||||
{updateProgress.percentage?.toFixed(1)}%
|
||||
{updateProgress.speed && ` • ${updateProgress.speed} MB/s`}
|
||||
{updateProgress.eta && ` • ${updateProgress.eta} remaining`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
|
||||
style={{ width: `${updateProgress.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other stage progress (without percentage) */}
|
||||
{isUpdating &&
|
||||
updateProgress &&
|
||||
updateProgress.stage !== "downloading" && (
|
||||
<div className="mt-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{updateProgress.message}
|
||||
</p>
|
||||
{updateProgress.stage === "extracting" && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Preparing update files...
|
||||
</p>
|
||||
)}
|
||||
{updateProgress.stage === "installing" && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Installing new version...
|
||||
</p>
|
||||
)}
|
||||
{updateProgress.stage === "completed" && (
|
||||
<p className="mt-1 text-xs text-green-600 dark:text-green-400">
|
||||
Update completed! Restarting application...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isUpdating && (
|
||||
<div className="flex gap-2 items-center mt-3">
|
||||
<Button
|
||||
|
||||
@@ -111,6 +111,16 @@ interface TwilightUpdateToastProps extends BaseToastProps {
|
||||
hasUpdate?: boolean;
|
||||
}
|
||||
|
||||
interface AppUpdateToastProps extends BaseToastProps {
|
||||
type: "app-update";
|
||||
stage?: "downloading" | "extracting" | "installing" | "completed";
|
||||
progress?: {
|
||||
percentage: number;
|
||||
speed?: string;
|
||||
eta?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type ToastProps =
|
||||
| LoadingToastProps
|
||||
| SuccessToastProps
|
||||
@@ -118,7 +128,8 @@ type ToastProps =
|
||||
| DownloadToastProps
|
||||
| VersionUpdateToastProps
|
||||
| FetchingToastProps
|
||||
| TwilightUpdateToastProps;
|
||||
| TwilightUpdateToastProps
|
||||
| AppUpdateToastProps;
|
||||
|
||||
function getToastIcon(type: ToastProps["type"], stage?: string) {
|
||||
switch (type) {
|
||||
@@ -133,6 +144,21 @@ function getToastIcon(type: ToastProps["type"], stage?: string) {
|
||||
);
|
||||
}
|
||||
return <LuDownload className="flex-shrink-0 w-4 h-4 text-blue-500" />;
|
||||
case "app-update":
|
||||
if (stage === "completed") {
|
||||
return (
|
||||
<LuCheckCheck className="flex-shrink-0 w-4 h-4 text-green-500" />
|
||||
);
|
||||
} else if (stage === "downloading") {
|
||||
return <LuDownload className="flex-shrink-0 w-4 h-4 text-blue-500" />;
|
||||
} else if (stage === "installing") {
|
||||
return (
|
||||
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-blue-500 animate-spin" />
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-blue-500 animate-spin" />
|
||||
);
|
||||
case "version-update":
|
||||
return (
|
||||
<LuRefreshCw className="flex-shrink-0 w-4 h-4 text-blue-500 animate-spin" />
|
||||
@@ -213,6 +239,28 @@ export function UnifiedToast(props: ToastProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* App update progress */}
|
||||
{type === "app-update" &&
|
||||
progress &&
|
||||
"percentage" in progress &&
|
||||
stage === "downloading" && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="flex-1 min-w-0 text-xs text-gray-600 dark:text-gray-300">
|
||||
{progress.percentage.toFixed(1)}%
|
||||
{progress.speed && ` • ${progress.speed} MB/s`}
|
||||
{progress.eta && ` • ${progress.eta} remaining`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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" && (
|
||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||
Preparing update files...
|
||||
</p>
|
||||
)}
|
||||
{stage === "installing" && (
|
||||
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||
Installing new version...
|
||||
</p>
|
||||
)}
|
||||
{stage === "completed" && (
|
||||
<p className="mt-1 text-xs text-green-600 dark:text-green-400">
|
||||
Update completed! Restarting application...
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<AppUpdateInfo | null>(null);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [updateProgress, setUpdateProgress] = useState<string>("");
|
||||
const [updateProgress, setUpdateProgress] =
|
||||
useState<AppUpdateProgress | null>(null);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [dismissedVersion, setDismissedVersion] = useState<string | null>(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<string>("app-update-progress", (event) => {
|
||||
console.log("App update progress:", event.payload);
|
||||
setUpdateProgress(event.payload);
|
||||
});
|
||||
const unlistenProgress = listen<AppUpdateProgress>(
|
||||
"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,
|
||||
|
||||
+36
-1
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user