mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-06 23:13:58 +02:00
refactor: auto-update in background
This commit is contained in:
+10
-2
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user