refactor: auto-update in background

This commit is contained in:
zhom
2026-01-18 01:34:07 +04:00
parent df460f9ab7
commit e9f4edd120
13 changed files with 635 additions and 271 deletions
+10 -2
View File
@@ -15,6 +15,7 @@ import { GroupManagementDialog } from "@/components/group-management-dialog";
import HomeHeader from "@/components/home-header";
import { ImportProfileDialog } from "@/components/import-profile-dialog";
import { IntegrationsDialog } from "@/components/integrations-dialog";
import { LaunchOnLoginDialog } from "@/components/launch-on-login-dialog";
import { PermissionDialog } from "@/components/permission-dialog";
import { ProfilesDataTable } from "@/components/profile-data-table";
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
@@ -118,6 +119,7 @@ export default function Home() {
const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] =
useState<BrowserProfile | null>(null);
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
const [launchOnLoginDialogOpen, setLaunchOnLoginDialogOpen] = useState(false);
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [currentPermissionType, setCurrentPermissionType] =
useState<PermissionType>("microphone");
@@ -268,10 +270,10 @@ export default function Home() {
try {
const shouldShow = await invoke<boolean>(
"should_show_settings_on_startup",
"should_show_launch_on_login_prompt",
);
if (shouldShow) {
setSettingsDialogOpen(true);
setLaunchOnLoginDialogOpen(true);
}
} catch (error) {
console.error("Failed to check startup prompt:", error);
@@ -1040,6 +1042,12 @@ export default function Home() {
}
onClose={checkTrialStatus}
/>
{/* Launch on Login Dialog - shown on every startup until enabled or declined */}
<LaunchOnLoginDialog
isOpen={launchOnLoginDialogOpen}
onClose={() => setLaunchOnLoginDialogOpen(false)}
/>
</div>
);
}
+84 -55
View File
@@ -10,9 +10,11 @@ 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) {
@@ -52,17 +54,22 @@ function getStageDisplayName(stage?: string) {
export function AppUpdateToast({
updateInfo,
onUpdate,
onRestart,
onDismiss,
isUpdating = false,
updateProgress,
updateReady = false,
}: AppUpdateToastProps) {
const handleUpdateClick = async () => {
await onUpdate(updateInfo);
};
const handleRestartClick = async () => {
await onRestart();
};
const handleViewRelease = () => {
if (updateInfo.release_page_url) {
// Trigger the same URL handling logic as if the URL came from the system
const event = new CustomEvent("url-open-request", {
detail: updateInfo.release_page_url,
});
@@ -85,7 +92,11 @@ export function AppUpdateToast({
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">
{getStageIcon(updateProgress?.stage, isUpdating)}
{updateReady ? (
<LuCheckCheck className="flex-shrink-0 w-5 h-5 text-green-500" />
) : (
getStageIcon(updateProgress?.stage, isUpdating)
)}
</div>
<div className="flex-1 min-w-0">
@@ -93,35 +104,43 @@ 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">
{isUpdating
? `${getStageDisplayName(updateProgress?.stage)} Donut Browser Update`
: "Donut Browser Update Available"}
{updateReady
? "The update is ready, restart app"
: isUpdating
? `${getStageDisplayName(updateProgress?.stage)} Donut Browser Update`
: "Donut Browser Update Available"}
</span>
<Badge
variant={updateInfo.is_nightly ? "secondary" : "default"}
className="text-xs"
>
{updateInfo.is_nightly ? "Nightly" : "Stable"}
</Badge>
</div>
<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>
)}
</>
{!updateReady && (
<Badge
variant={updateInfo.is_nightly ? "secondary" : "default"}
className="text-xs"
>
{updateInfo.is_nightly ? "Nightly" : "Stable"}
</Badge>
)}
</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 && (
{!isUpdating && !updateReady && (
<Button
variant="ghost"
size="sm"
@@ -133,8 +152,7 @@ export function AppUpdateToast({
)}
</div>
{/* Download progress */}
{showDownloadProgress && updateProgress && (
{!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">
@@ -152,10 +170,8 @@ export function AppUpdateToast({
</div>
)}
{/* Other stage progress (with visual indicators) */}
{showOtherStageProgress && (
{!updateReady && showOtherStageProgress && (
<div className="mt-2 space-y-1">
{/* Progress indicator for non-downloading stages */}
<div className="w-full bg-muted rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all duration-500 ${
@@ -168,36 +184,49 @@ export function AppUpdateToast({
</div>
)}
{!isUpdating && (
{updateReady ? (
<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" />
Update Now
</RippleButton>
)}
<RippleButton
variant="outline"
onClick={onDismiss}
onClick={() => void handleRestartClick()}
size="sm"
className="text-xs"
className="flex gap-2 items-center text-xs"
>
Later
<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>
)}
<RippleButton
variant="outline"
onClick={onDismiss}
size="sm"
className="text-xs"
>
Later
</RippleButton>
</div>
)
)}
</div>
</div>
+96
View File
@@ -0,0 +1,96 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useState } from "react";
import { LoadingButton } from "@/components/loading-button";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
interface LaunchOnLoginDialogProps {
isOpen: boolean;
onClose: () => void;
}
export function LaunchOnLoginDialog({
isOpen,
onClose,
}: LaunchOnLoginDialogProps) {
const [isEnabling, setIsEnabling] = useState(false);
const [isDeclining, setIsDeclining] = useState(false);
const handleEnable = useCallback(async () => {
setIsEnabling(true);
try {
await invoke("enable_launch_on_login");
showSuccessToast("Launch on login enabled");
onClose();
} catch (error) {
console.error("Failed to enable launch on login:", error);
showErrorToast("Failed to enable launch on login", {
description:
error instanceof Error ? error.message : "Please try again",
});
} finally {
setIsEnabling(false);
}
}, [onClose]);
const handleDecline = useCallback(async () => {
setIsDeclining(true);
try {
await invoke("decline_launch_on_login");
onClose();
} catch (error) {
console.error("Failed to decline launch on login:", error);
showErrorToast("Failed to save preference", {
description:
error instanceof Error ? error.message : "Please try again",
});
} finally {
setIsDeclining(false);
}
}, [onClose]);
return (
<Dialog open={isOpen}>
<DialogContent
className="sm:max-w-sm"
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>Enable Launch on Login?</DialogTitle>
</DialogHeader>
<p className="text-sm text-muted-foreground">
Running in the background helps keep your proxies and browsers alive.
</p>
<DialogFooter className="flex-row justify-between sm:justify-between">
<Button
variant="ghost"
onClick={handleDecline}
disabled={isEnabling || isDeclining}
>
{isDeclining ? "..." : "Don't Ask Again"}
</Button>
<LoadingButton
onClick={handleEnable}
isLoading={isEnabling}
disabled={isDeclining}
>
Enable
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+1 -1
View File
@@ -38,7 +38,7 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
) {
setDefaultTheme(themeValue);
} else if (themeValue === "custom") {
setDefaultTheme("light");
setDefaultTheme("dark");
if (
settings.custom_theme &&
Object.keys(settings.custom_theme).length > 0
+31 -1
View File
@@ -13,6 +13,7 @@ export function useAppUpdateNotifications() {
const [isUpdating, setIsUpdating] = useState(false);
const [updateProgress, setUpdateProgress] =
useState<AppUpdateProgress | null>(null);
const [updateReady, setUpdateReady] = useState(false);
const [isClient, setIsClient] = useState(false);
const [dismissedVersion, setDismissedVersion] = useState<string | null>(null);
@@ -68,7 +69,7 @@ export function useAppUpdateNotifications() {
message: "Starting update...",
});
await invoke("download_and_install_app_update", {
await invoke("download_and_prepare_app_update", {
updateInfo: appUpdateInfo,
});
} catch (error) {
@@ -84,6 +85,20 @@ export function useAppUpdateNotifications() {
}
}, []);
const handleRestart = useCallback(async () => {
try {
await invoke("restart_application");
} catch (error) {
console.error("Failed to restart app:", error);
showToast({
type: "error",
title: "Failed to restart",
description: String(error),
duration: 6000,
});
}
}, []);
const dismissAppUpdate = useCallback(() => {
if (!isClient) return;
@@ -125,6 +140,13 @@ export function useAppUpdateNotifications() {
},
);
const unlistenReady = listen<string>("app-update-ready", (event) => {
console.log("App update ready:", event.payload);
setUpdateReady(true);
setIsUpdating(false);
setUpdateProgress(null);
});
return () => {
void unlistenUpdate.then((unlisten) => {
unlisten();
@@ -132,6 +154,9 @@ export function useAppUpdateNotifications() {
void unlistenProgress.then((unlisten) => {
unlisten();
});
void unlistenReady.then((unlisten) => {
unlisten();
});
};
}, [isClient]);
@@ -144,9 +169,11 @@ export function useAppUpdateNotifications() {
<AppUpdateToast
updateInfo={updateInfo}
onUpdate={handleAppUpdate}
onRestart={handleRestart}
onDismiss={dismissAppUpdate}
isUpdating={isUpdating}
updateProgress={updateProgress}
updateReady={updateReady}
/>
),
{
@@ -163,9 +190,11 @@ export function useAppUpdateNotifications() {
}, [
updateInfo,
handleAppUpdate,
handleRestart,
dismissAppUpdate,
isUpdating,
updateProgress,
updateReady,
isClient,
]);
@@ -181,6 +210,7 @@ export function useAppUpdateNotifications() {
updateInfo,
isUpdating,
updateProgress,
updateReady,
checkForAppUpdates,
checkForAppUpdatesManual,
dismissAppUpdate,