mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-25 21:36:23 +02:00
refactor: simplify auto-update login
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user