refactor: simplify auto-update login

This commit is contained in:
zhom
2026-02-17 22:54:45 +04:00
parent d490ad3612
commit 6654ab9fdc
2 changed files with 72 additions and 197 deletions
+38 -174
View File
@@ -1,69 +1,24 @@
"use client";
import { FaDownload, FaExternalLinkAlt, FaTimes } from "react-icons/fa";
import { LuCheckCheck, LuCog, LuRefreshCw } from "react-icons/lu";
import { Badge } from "@/components/ui/badge";
import { FaExternalLinkAlt, FaTimes } from "react-icons/fa";
import { LuCheckCheck } from "react-icons/lu";
import { Button } from "@/components/ui/button";
import type { AppUpdateInfo, AppUpdateProgress } from "@/types";
import type { AppUpdateInfo } from "@/types";
import { RippleButton } from "./ui/ripple";
interface AppUpdateToastProps {
updateInfo: AppUpdateInfo;
onUpdate: (updateInfo: AppUpdateInfo) => Promise<void>;
onRestart: () => Promise<void>;
onDismiss: () => void;
isUpdating?: boolean;
updateProgress?: AppUpdateProgress | null;
updateReady?: boolean;
}
function getStageIcon(stage?: string, isUpdating?: boolean) {
if (!isUpdating) {
return <FaDownload className="flex-shrink-0 w-5 h-5" />;
}
switch (stage) {
case "downloading":
return <FaDownload className="flex-shrink-0 w-5 h-5" />;
case "extracting":
return <LuRefreshCw className="flex-shrink-0 w-5 h-5 animate-spin" />;
case "installing":
return <LuCog className="flex-shrink-0 w-5 h-5 animate-spin" />;
case "completed":
return <LuCheckCheck className="flex-shrink-0 w-5 h-5" />;
default:
return <LuRefreshCw className="flex-shrink-0 w-5 h-5 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({
updateInfo,
onUpdate,
onRestart,
onDismiss,
isUpdating = false,
updateProgress,
updateReady = false,
}: AppUpdateToastProps) {
const handleUpdateClick = async () => {
await onUpdate(updateInfo);
};
const handleRestartClick = async () => {
await onRestart();
};
@@ -77,115 +32,37 @@ export function AppUpdateToast({
}
};
const showDownloadProgress =
isUpdating &&
updateProgress?.stage === "downloading" &&
updateProgress.percentage !== undefined;
const showOtherStageProgress =
isUpdating &&
updateProgress &&
(updateProgress.stage === "extracting" ||
updateProgress.stage === "installing" ||
updateProgress.stage === "completed");
return (
<div className="flex items-start p-4 w-full max-w-md rounded-lg border shadow-lg bg-card border-border text-card-foreground">
<div className="mr-3 mt-0.5">
{updateReady ? (
<LuCheckCheck className="flex-shrink-0 w-5 h-5 text-green-500" />
) : (
getStageIcon(updateProgress?.stage, isUpdating)
)}
<LuCheckCheck className="flex-shrink-0 w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex gap-2 justify-between items-start">
<div className="flex flex-col gap-1">
<div className="flex gap-2 items-center">
<span className="text-sm font-semibold text-foreground">
{updateReady
? "The update is ready, restart app"
: isUpdating
? `${getStageDisplayName(updateProgress?.stage)} Donut Browser Update`
: "Donut Browser Update Available"}
</span>
{!updateReady && (
<Badge
variant={updateInfo.is_nightly ? "secondary" : "default"}
className="text-xs"
>
{updateInfo.is_nightly ? "Nightly" : "Stable"}
</Badge>
)}
<span className="text-sm font-semibold text-foreground">
{updateReady
? "Update ready, restart to apply"
: "Manual download required"}
</span>
<div className="text-xs text-muted-foreground">
{updateInfo.current_version} {updateInfo.new_version}
</div>
{!updateReady && (
<div className="text-xs text-muted-foreground">
{isUpdating ? (
updateProgress?.message || "Updating..."
) : (
<>
Update from {updateInfo.current_version} to{" "}
<span className="font-medium">
{updateInfo.new_version}
</span>
{updateInfo.manual_update_required && (
<span className="block mt-1 text-muted-foreground/80">
Manual download required on Linux
</span>
)}
</>
)}
</div>
)}
</div>
{!isUpdating && !updateReady && (
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="p-0 w-6 h-6 shrink-0"
>
<FaTimes className="w-3 h-3" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="p-0 w-6 h-6 shrink-0"
>
<FaTimes className="w-3 h-3" />
</Button>
</div>
{!updateReady && showDownloadProgress && 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-muted rounded-full h-1.5">
<div
className="bg-primary h-1.5 rounded-full transition-all duration-300"
style={{ width: `${updateProgress.percentage}%` }}
/>
</div>
</div>
)}
{!updateReady && showOtherStageProgress && (
<div className="mt-2 space-y-1">
<div className="w-full bg-muted rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all duration-500 ${
updateProgress.stage === "completed"
? "bg-green-500 w-full"
: "bg-primary w-full animate-pulse"
}`}
/>
</div>
</div>
)}
{updateReady ? (
<div className="flex gap-2 items-center mt-3">
<div className="flex gap-2 items-center mt-3">
{updateReady ? (
<RippleButton
onClick={() => void handleRestartClick()}
size="sm"
@@ -194,40 +71,27 @@ export function AppUpdateToast({
<LuCheckCheck className="w-3 h-3" />
Restart Now
</RippleButton>
</div>
) : (
!isUpdating && (
<div className="flex gap-2 items-center mt-3">
{updateInfo.manual_update_required ? (
<RippleButton
onClick={handleViewRelease}
size="sm"
className="flex gap-2 items-center text-xs"
>
<FaExternalLinkAlt className="w-3 h-3" />
View Release
</RippleButton>
) : (
<RippleButton
onClick={() => void handleUpdateClick()}
size="sm"
className="flex gap-2 items-center text-xs"
>
<FaDownload className="w-3 h-3" />
Download Update
</RippleButton>
)}
) : (
updateInfo.manual_update_required && (
<RippleButton
variant="outline"
onClick={onDismiss}
onClick={handleViewRelease}
size="sm"
className="text-xs"
className="flex gap-2 items-center text-xs"
>
Later
<FaExternalLinkAlt className="w-3 h-3" />
View Release
</RippleButton>
</div>
)
)}
)
)}
<RippleButton
variant="outline"
onClick={onDismiss}
size="sm"
className="text-xs"
>
Later
</RippleButton>
</div>
</div>
</div>
);
+34 -23
View File
@@ -2,7 +2,7 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { AppUpdateToast } from "@/components/app-update-toast";
import { showToast } from "@/lib/toast-utils";
@@ -16,6 +16,7 @@ export function useAppUpdateNotifications() {
const [updateReady, setUpdateReady] = useState(false);
const [isClient, setIsClient] = useState(false);
const [dismissedVersion, setDismissedVersion] = useState<string | null>(null);
const autoDownloadedVersion = useRef<string | null>(null);
// Ensure we're on the client side to prevent hydration mismatches
useEffect(() => {
@@ -52,6 +53,7 @@ export function useAppUpdateNotifications() {
console.log("Manual check result:", update);
// Always show manual check results, even if previously dismissed
autoDownloadedVersion.current = null;
setUpdateInfo(update);
} catch (error) {
console.error("Failed to manually check for app updates:", error);
@@ -112,7 +114,7 @@ export function useAppUpdateNotifications() {
toast.dismiss("app-update");
}, [isClient, updateInfo]);
// Listen for app update availability
// Listen for app update events
useEffect(() => {
if (!isClient) return;
@@ -127,16 +129,7 @@ export function useAppUpdateNotifications() {
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);
}, 5000); // Show completion message for 5 seconds instead of 2
}
},
);
@@ -160,41 +153,59 @@ export function useAppUpdateNotifications() {
};
}, [isClient]);
// Show toast when update is available
// Auto-download update in background when found
useEffect(() => {
if (!isClient || !updateInfo) return;
if (
!isClient ||
!updateInfo ||
updateInfo.manual_update_required ||
isUpdating ||
updateReady ||
autoDownloadedVersion.current === updateInfo.new_version
)
return;
autoDownloadedVersion.current = updateInfo.new_version;
console.log("Auto-downloading app update:", updateInfo.new_version);
void handleAppUpdate(updateInfo);
}, [isClient, updateInfo, isUpdating, updateReady, handleAppUpdate]);
// Show toast only when update is ready to install or requires manual action
useEffect(() => {
if (!isClient) return;
const showManualToast = updateInfo?.manual_update_required && !isUpdating;
if (!updateReady && !showManualToast) {
return;
}
if (!updateInfo) return;
toast.custom(
() => (
<AppUpdateToast
updateInfo={updateInfo}
onUpdate={handleAppUpdate}
onRestart={handleRestart}
onDismiss={dismissAppUpdate}
isUpdating={isUpdating}
updateProgress={updateProgress}
updateReady={updateReady}
/>
),
{
id: "app-update",
duration: Number.POSITIVE_INFINITY, // Persistent until user action
duration: Number.POSITIVE_INFINITY,
position: "top-left",
style: {
zIndex: 99999, // Ensure app updates appear above dialogs
pointerEvents: "auto", // Ensure app updates remain interactive
marginTop: "16px", // slightly lower on macOS-like top controls
zIndex: 99999,
pointerEvents: "auto",
marginTop: "16px",
},
},
);
}, [
updateInfo,
handleAppUpdate,
handleRestart,
dismissAppUpdate,
isUpdating,
updateProgress,
updateReady,
isUpdating,
isClient,
]);