feat: show real-time download progress bar for app updates

Leverage the Tauri updater plugin's DownloadEvent callback to display
a progress bar with downloaded/total bytes during app updates, replacing
the previous spinner-only feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-12 22:01:46 +08:00
parent a763adaf36
commit 1554425b03
12 changed files with 90 additions and 4 deletions

View File

@@ -39,8 +39,15 @@ import {
normalizeAppUpdateError,
relaunchApp,
} from "@/lib/updater"
import type { DownloadEvent } from "@/lib/updater"
import { APP_LOCALES } from "@/lib/i18n"
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
const PROXY_EXAMPLE = "http://127.0.0.1:7890"
const APP_LANGUAGE_VALUES = APP_LOCALES
@@ -71,6 +78,11 @@ export function SystemNetworkSettings() {
const [installingUpdate, setInstallingUpdate] = useState(false)
const [updateError, setUpdateError] = useState<string | null>(null)
const [lastCheckedAt, setLastCheckedAt] = useState<Date | null>(null)
const [downloadProgress, setDownloadProgress] = useState<{
downloaded: number
total: number | null
phase: "downloading" | "installing"
} | null>(null)
const [appLanguage, setAppLanguage] = useState<LanguageSelectValue>(
languageSettings.mode === "system" ? "system" : languageSettings.language
@@ -273,9 +285,37 @@ export function SystemNetworkSettings() {
setInstallingUpdate(true)
setUpdateError(null)
setDownloadProgress(null)
let downloaded = 0
try {
await installAppUpdate(availableUpdate)
await installAppUpdate(availableUpdate, (event: DownloadEvent) => {
switch (event.event) {
case "Started":
setDownloadProgress({
downloaded: 0,
total: event.data.contentLength ?? null,
phase: "downloading",
})
break
case "Progress":
downloaded += event.data.chunkLength
setDownloadProgress((prev) => ({
downloaded,
total: prev?.total ?? null,
phase: "downloading",
}))
break
case "Finished":
setDownloadProgress((prev) => ({
downloaded: prev?.downloaded ?? downloaded,
total: prev?.total ?? null,
phase: "installing",
}))
break
}
})
toast.success(t("installSuccess"))
await relaunchApp()
} catch (err) {
@@ -285,6 +325,7 @@ export function SystemNetworkSettings() {
console.error("[Settings] install app update failed:", err)
} finally {
setInstallingUpdate(false)
setDownloadProgress(null)
}
}, [availableUpdate, formatUpdateError, t])
@@ -389,10 +430,39 @@ export function SystemNetworkSettings() {
</p>
)}
{updateStatusMessage && (
{updateStatusMessage && !downloadProgress && (
<p className="text-muted-foreground">{updateStatusMessage}</p>
)}
{downloadProgress && (
<div className="space-y-1.5">
<div className="flex items-center justify-between text-muted-foreground">
<span>
{downloadProgress.phase === "downloading"
? t("downloading")
: t("updating")}
</span>
<span>
{formatBytes(downloadProgress.downloaded)}
{downloadProgress.total
? ` / ${formatBytes(downloadProgress.total)}`
: ""}
</span>
</div>
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
<div
className="h-full rounded-full bg-primary transition-all duration-300"
style={{
width:
downloadProgress.total && downloadProgress.total > 0
? `${Math.min(100, (downloadProgress.downloaded / downloadProgress.total) * 100)}%`
: "30%",
}}
/>
</div>
</div>
)}
{availableUpdate && (
<div className="space-y-2 pt-2 border-t border-border/70">
<div className="flex items-center justify-between gap-3">

View File

@@ -172,6 +172,7 @@
"checking": "جارٍ التحقق...",
"checkUpdate": "التحقق من التحديثات",
"updating": "جارٍ التثبيت...",
"downloading": "جارٍ التنزيل...",
"upgradeTo": "الترقية إلى v{version}",
"foundUpdate": "تم العثور على إصدار جديد v{version}",
"alreadyLatest": "أنت على أحدث إصدار",

View File

@@ -172,6 +172,7 @@
"checking": "Wird geprüft...",
"checkUpdate": "Nach Updates suchen",
"updating": "Wird installiert...",
"downloading": "Herunterladen...",
"upgradeTo": "Auf v{version} aktualisieren",
"foundUpdate": "Neue Version v{version} gefunden",
"alreadyLatest": "Du verwendest bereits die neueste Version",

View File

@@ -172,6 +172,7 @@
"checking": "Checking...",
"checkUpdate": "Check for updates",
"updating": "Installing...",
"downloading": "Downloading...",
"upgradeTo": "Upgrade to v{version}",
"foundUpdate": "New version v{version} found",
"alreadyLatest": "You're on the latest version",

View File

@@ -172,6 +172,7 @@
"checking": "Comprobando...",
"checkUpdate": "Buscar actualizaciones",
"updating": "Instalando...",
"downloading": "Descargando...",
"upgradeTo": "Actualizar a v{version}",
"foundUpdate": "Nueva versión v{version} encontrada",
"alreadyLatest": "Ya tienes la versión más reciente",

View File

@@ -172,6 +172,7 @@
"checking": "Vérification...",
"checkUpdate": "Rechercher les mises à jour",
"updating": "Installation...",
"downloading": "Téléchargement...",
"upgradeTo": "Mettre à jour vers v{version}",
"foundUpdate": "Nouvelle version v{version} trouvée",
"alreadyLatest": "Vous utilisez déjà la dernière version",

View File

@@ -172,6 +172,7 @@
"checking": "確認中...",
"checkUpdate": "更新を確認",
"updating": "インストール中...",
"downloading": "ダウンロード中...",
"upgradeTo": "v{version} にアップグレード",
"foundUpdate": "新しいバージョン v{version} が見つかりました",
"alreadyLatest": "すでに最新バージョンです",

View File

@@ -172,6 +172,7 @@
"checking": "확인 중...",
"checkUpdate": "업데이트 확인",
"updating": "설치 중...",
"downloading": "다운로드 중...",
"upgradeTo": "v{version}로 업그레이드",
"foundUpdate": "새 버전 v{version}을 찾았습니다",
"alreadyLatest": "이미 최신 버전입니다",

View File

@@ -172,6 +172,7 @@
"checking": "Verificando...",
"checkUpdate": "Verificar atualizações",
"updating": "Instalando...",
"downloading": "Baixando...",
"upgradeTo": "Atualizar para v{version}",
"foundUpdate": "Nova versão v{version} encontrada",
"alreadyLatest": "Você já está na versão mais recente",

View File

@@ -172,6 +172,7 @@
"checking": "检查中...",
"checkUpdate": "检查更新",
"updating": "升级中...",
"downloading": "下载中...",
"upgradeTo": "升级到 v{version}",
"foundUpdate": "发现新版本 v{version}",
"alreadyLatest": "当前已经是最新版本",

View File

@@ -172,6 +172,7 @@
"checking": "檢查中...",
"checkUpdate": "檢查更新",
"updating": "升級中...",
"downloading": "下載中...",
"upgradeTo": "升級到 v{version}",
"foundUpdate": "發現新版本 v{version}",
"alreadyLatest": "目前已是最新版本",

View File

@@ -4,6 +4,11 @@ import { getTransport, isDesktop } from "./transport"
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Update = any
export type DownloadEvent =
| { event: "Started"; data: { contentLength?: number } }
| { event: "Progress"; data: { chunkLength: number } }
| { event: "Finished" }
export interface AppUpdateCheckResult {
currentVersion: string
update: Update | null
@@ -46,9 +51,10 @@ export async function checkAppUpdate(): Promise<AppUpdateCheckResult> {
}
export async function installAppUpdate(
update: NonNullable<Update>
update: NonNullable<Update>,
onEvent?: (progress: DownloadEvent) => void
): Promise<void> {
await update.downloadAndInstall()
await update.downloadAndInstall(onEvent)
}
export async function relaunchApp(): Promise<void> {