diff --git a/src/components/custom-toast.tsx b/src/components/custom-toast.tsx index 9d451e2..99bbdaa 100644 --- a/src/components/custom-toast.tsx +++ b/src/components/custom-toast.tsx @@ -10,6 +10,7 @@ * - Progress bars for downloads/updates * - Success/error states * - Customizable icons and content + * - Auto-update notifications * * Usage Examples: * @@ -23,6 +24,11 @@ * }); * ``` * + * Auto-update toast: + * ``` + * showAutoUpdateToast("Firefox", "125.0.1"); + * ``` + * * Download progress toast: * ``` * showToast({ @@ -47,6 +53,7 @@ import { LuCheckCheck, LuDownload, LuRefreshCw, + LuRocket, LuTriangleAlert, } from "react-icons/lu"; @@ -139,6 +146,10 @@ function getToastIcon(type: ToastProps["type"], stage?: string) { return ( ); + case "loading": + return ( +
+ ); default: return (
@@ -151,11 +162,33 @@ export function UnifiedToast(props: ToastProps) { const stage = "stage" in props ? props.stage : undefined; const progress = "progress" in props ? props.progress : undefined; + // Check if this is an auto-update toast + const isAutoUpdate = title.includes("update started"); + return ( -
-
{getToastIcon(type, stage)}
+
+
+ {isAutoUpdate ? ( + + ) : ( + getToastIcon(type, stage) + )} +
-

+

{title}

@@ -225,7 +258,13 @@ export function UnifiedToast(props: ToastProps) { {/* Description */} {description && ( -

+

{description}

)} diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 5542ced..609d3a1 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -1,6 +1,5 @@ "use client"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -275,70 +274,6 @@ export function ProfilesDataTable({ return browserA.localeCompare(browserB); }, }, - { - accessorKey: "version", - header: "Version", - }, - { - id: "status", - header: ({ column }) => { - const isSorted = column.getIsSorted(); - return ( - - ); - }, - cell: ({ row }) => { - const profile = row.original; - const isRunning = isClient && runningProfiles.has(profile.name); - return ( -
- - {isClient ? (isRunning ? "Running" : "Stopped") : "Loading..."} - - {isClient && isRunning && profile.process_id && ( - - PID: {profile.process_id} - - )} -
- ); - }, - enableSorting: true, - sortingFn: (rowA, rowB) => { - // If not on client, sort by name only to ensure consistency - if (!isClient) { - return rowA.original.name.localeCompare(rowB.original.name); - } - - const isRunningA = runningProfiles.has(rowA.original.name); - const isRunningB = runningProfiles.has(rowB.original.name); - - // Running profiles come first, then stopped ones - // Secondary sort by profile name - if (isRunningA === isRunningB) { - return rowA.original.name.localeCompare(rowB.original.name); - } - return isRunningA ? -1 : 1; - }, - }, { id: "proxy", header: "Proxy", diff --git a/src/hooks/use-browser-download.ts b/src/hooks/use-browser-download.ts index f0f70f8..c6986d9 100644 --- a/src/hooks/use-browser-download.ts +++ b/src/hooks/use-browser-download.ts @@ -3,9 +3,7 @@ import { dismissToast, showDownloadToast, showErrorToast, - showFetchingToast, showSuccessToast, - showUnifiedVersionUpdateToast, } from "@/lib/toast-utils"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; @@ -45,15 +43,6 @@ interface BrowserVersionsResult { total_versions_count: number; } -interface VersionUpdateProgress { - current_browser: string; - total_browsers: number; - completed_browsers: number; - new_versions_found: number; - browser_new_versions: number; - status: string; -} - export function useBrowserDownload() { const [availableVersions, setAvailableVersions] = useState( [], @@ -62,7 +51,6 @@ export function useBrowserDownload() { const [isDownloading, setIsDownloading] = useState(false); const [downloadProgress, setDownloadProgress] = useState(null); - const [isUpdatingVersions, setIsUpdatingVersions] = useState(false); // Listen for download progress events useEffect(() => { @@ -128,70 +116,6 @@ export function useBrowserDownload() { }; }, []); - // Listen for version update progress events - useEffect(() => { - const unlisten = listen( - "version-update-progress", - (event) => { - const progress = event.payload; - - if (progress.status === "updating") { - setIsUpdatingVersions(true); - - // Show unified progress toast - const currentBrowserName = progress.current_browser - ? getBrowserDisplayName(progress.current_browser) - : undefined; - - showUnifiedVersionUpdateToast("Checking for browser updates...", { - description: currentBrowserName - ? `Fetching ${currentBrowserName} release information...` - : "Initializing version check...", - progress: { - current: progress.completed_browsers, - total: progress.total_browsers, - found: progress.new_versions_found, - current_browser: currentBrowserName, - }, - }); - } else if (progress.status === "completed") { - setIsUpdatingVersions(false); - dismissToast("unified-version-update"); - - if (progress.new_versions_found > 0) { - showSuccessToast( - `Found ${progress.new_versions_found} new browser versions!`, - { - duration: 4000, - description: - "Version information has been updated in the background", - }, - ); - } else { - showSuccessToast("No new browser versions found", { - duration: 3000, - description: "All browser versions are up to date", - }); - } - } else if (progress.status === "error") { - setIsUpdatingVersions(false); - dismissToast("unified-version-update"); - - showErrorToast("Failed to check for new versions", { - duration: 4000, - description: "Check your internet connection and try again", - }); - } - }, - ); - - return () => { - void unlisten.then((fn) => { - fn(); - }); - }; - }, []); - const formatTime = (seconds: number): string => { if (seconds < 60) { return `${Math.round(seconds)}s`; @@ -217,10 +141,8 @@ export function useBrowserDownload() { const loadVersions = useCallback(async (browserStr: string) => { const browserName = getBrowserDisplayName(browserStr); - // Show fetching toast - const toastId = showFetchingToast(browserName, { - id: `fetch-${browserStr}`, - }); + // Use a simple loading state instead of toast for version fetching + console.log(`Fetching ${browserName} versions...`); try { const versionInfos = await invoke( @@ -239,11 +161,9 @@ export function useBrowserDownload() { ); setAvailableVersions(githubReleases); - dismissToast(toastId); return githubReleases; } catch (error) { console.error("Failed to load versions:", error); - dismissToast(toastId); showErrorToast(`Failed to fetch ${browserName} versions`, { description: error instanceof Error ? error.message : "Unknown error occurred", @@ -377,7 +297,6 @@ export function useBrowserDownload() { downloadedVersions, isDownloading, downloadProgress, - isUpdatingVersions, loadVersions, loadVersionsWithNewCount, loadDownloadedVersions, diff --git a/src/hooks/use-update-notifications.tsx b/src/hooks/use-update-notifications.tsx index 35f7deb..76b3233 100644 --- a/src/hooks/use-update-notifications.tsx +++ b/src/hooks/use-update-notifications.tsx @@ -1,9 +1,7 @@ -import { UpdateNotificationComponent } from "@/components/update-notification"; import { getBrowserDisplayName } from "@/lib/browser-utils"; -import { showToast } from "@/lib/toast-utils"; +import { showAutoUpdateToast, showToast } from "@/lib/toast-utils"; import { invoke } from "@tauri-apps/api/core"; -import React, { useCallback, useEffect, useState } from "react"; -import { toast } from "sonner"; +import { useCallback, useEffect, useRef, useState } from "react"; interface UpdateNotification { id: string; @@ -23,73 +21,82 @@ export function useUpdateNotifications( const [updatingBrowsers, setUpdatingBrowsers] = useState>( new Set(), ); - const [dismissedNotifications, setDismissedNotifications] = useState< + const [processedNotifications, setProcessedNotifications] = useState< Set >(new Set()); + // Add refs to track ongoing operations to prevent duplicates + const isCheckingForUpdates = useRef(false); + const activeDownloads = useRef>(new Set()); // Track "browser-version" keys + const checkForUpdates = useCallback(async () => { + // Prevent multiple simultaneous calls + if (isCheckingForUpdates.current) { + console.log("Already checking for updates, skipping duplicate call"); + return; + } + + isCheckingForUpdates.current = true; + try { const updates = await invoke( "check_for_browser_updates", ); - // Filter out dismissed notifications unless they're for a newer version - const filteredUpdates = updates.filter((notification) => { - // Check if this exact notification was dismissed - if (dismissedNotifications.has(notification.id)) { - return false; - } - - // Check if we dismissed an older version for this browser - const dismissedForBrowser = Array.from(dismissedNotifications).find( - (dismissedId) => { - const parts = dismissedId.split("_"); - if (parts.length >= 2) { - const browser = parts[0]; - return browser === notification.browser; - } - return false; - }, - ); - - if (dismissedForBrowser) { - // Extract the dismissed version to compare - const dismissedParts = dismissedForBrowser.split("_to_"); - if (dismissedParts.length === 2) { - const dismissedToVersion = dismissedParts[1]; - // Only show if this is a newer version than what was dismissed - return notification.new_version !== dismissedToVersion; - } - } - - return true; + // Filter out already processed notifications + const newUpdates = updates.filter((notification) => { + return !processedNotifications.has(notification.id); }); - setNotifications(filteredUpdates); + setNotifications(newUpdates); - // Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately - // to avoid circular dependencies + // Automatically start downloads for new update notifications + for (const notification of newUpdates) { + if (!processedNotifications.has(notification.id)) { + setProcessedNotifications((prev) => + new Set(prev).add(notification.id), + ); + // Start automatic update without user interaction + void handleAutoUpdate( + notification.browser, + notification.new_version, + notification.id, + ); + } + } } catch (error) { console.error("Failed to check for updates:", error); + } finally { + isCheckingForUpdates.current = false; } - }, [dismissedNotifications]); + }, [processedNotifications]); + + const handleAutoUpdate = useCallback( + async (browser: string, newVersion: string, notificationId: string) => { + const downloadKey = `${browser}-${newVersion}`; + + // Check if this download is already in progress + if (activeDownloads.current.has(downloadKey)) { + console.log( + `Download already in progress for ${downloadKey}, skipping duplicate`, + ); + return; + } + + // Mark download as active + activeDownloads.current.add(downloadKey); - const handleUpdate = useCallback( - async (browser: string, newVersion: string) => { try { setUpdatingBrowsers((prev) => new Set(prev).add(browser)); const browserDisplayName = getBrowserDisplayName(browser); - // Dismiss all notifications for this browser first - const browserNotifications = notifications.filter( - (n) => n.browser === browser, - ); - for (const notification of browserNotifications) { - toast.dismiss(notification.id); - await invoke("dismiss_update_notification", { - notificationId: notification.id, - }); - } + // Dismiss the notification in the backend + await invoke("dismiss_update_notification", { + notificationId, + }); + + // Show auto-update started toast + showAutoUpdateToast(browserDisplayName, newVersion); try { // Check if browser already exists before downloading @@ -134,17 +141,19 @@ export function useUpdateNotifications( : `${updatedProfiles.length} profiles have been updated`; showToast({ + id: `auto-update-success-${browser}-${newVersion}`, type: "success", title: `${browserDisplayName} update completed`, - description: `${profileText} to version ${newVersion}. Running profiles were not updated and can be updated manually.`, + description: `${profileText} to version ${newVersion}. To update running profiles, restart them.`, duration: 5000, }); } else { showToast({ + id: `auto-update-success-${browser}-${newVersion}`, type: "success", title: `${browserDisplayName} update ready`, description: - "All affected profiles are currently running. Stop them and manually update their versions to use the new version.", + "All affected profiles are currently running. To update them, restart them.", duration: 5000, }); } @@ -167,6 +176,7 @@ export function useUpdateNotifications( } showToast({ + id: `auto-update-error-${browser}-${newVersion}`, type: "error", title: `Failed to download ${browserDisplayName} ${newVersion}`, description: String(downloadError), @@ -175,18 +185,21 @@ export function useUpdateNotifications( throw downloadError; } - // Refresh notifications to clear any remaining ones - await checkForUpdates(); + // Don't call checkForUpdates() again here as it can cause recursion and duplicates + // The periodic checks will handle finding any remaining updates } catch (error) { - console.error("Failed to start update:", error); + console.error("Failed to start auto-update:", error); const browserDisplayName = getBrowserDisplayName(browser); showToast({ + id: `auto-update-error-${browser}-${newVersion}`, type: "error", title: `Failed to update ${browserDisplayName}`, description: String(error), duration: 6000, }); } finally { + // Remove from active downloads and updating browsers + activeDownloads.current.delete(downloadKey); setUpdatingBrowsers((prev) => { const next = new Set(prev); next.delete(browser); @@ -194,52 +207,18 @@ export function useUpdateNotifications( }); } }, - [notifications, checkForUpdates, onProfilesUpdated], + [onProfilesUpdated], ); - const handleDismiss = useCallback( - async (notificationId: string) => { - try { - toast.dismiss(notificationId); - await invoke("dismiss_update_notification", { notificationId }); - - // Track this notification as dismissed to prevent showing it again - setDismissedNotifications((prev) => new Set(prev).add(notificationId)); - - await checkForUpdates(); - } catch (error) { - console.error("Failed to dismiss notification:", error); - } - }, - [checkForUpdates], - ); - - // Separate effect to show toasts when notifications change + // Clean up notifications when they're no longer needed useEffect(() => { - for (const notification of notifications) { - const isUpdating = updatingBrowsers.has(notification.browser); - - toast.custom( - () => ( - - ), - { - id: notification.id, - duration: Number.POSITIVE_INFINITY, // Persistent until user action - position: "top-right", - style: { - zIndex: 99999, // Ensure notifications appear above dialogs - pointerEvents: "auto", // Ensure notifications remain interactive - }, - }, - ); - } - }, [notifications, updatingBrowsers, handleUpdate, handleDismiss]); + // Remove notifications that have been processed + setNotifications((prev) => + prev.filter( + (notification) => !processedNotifications.has(notification.id), + ), + ); + }, [processedNotifications]); return { notifications, diff --git a/src/hooks/use-version-updater.ts b/src/hooks/use-version-updater.ts index 6c50810..fd2f81e 100644 --- a/src/hooks/use-version-updater.ts +++ b/src/hooks/use-version-updater.ts @@ -68,14 +68,11 @@ export function useVersionUpdater() { dismissToast("unified-version-update"); if (progress.new_versions_found > 0) { - toast.success( - `Found ${progress.new_versions_found} new browser versions!`, - { - duration: 4000, - description: - "Version information has been updated in the background", - }, - ); + toast.success("Browser versions updated successfully", { + duration: 4000, + description: + "Version information has been updated in the background", + }); } else { toast.success("No new browser versions found", { duration: 3000, diff --git a/src/lib/toast-utils.ts b/src/lib/toast-utils.ts index 23a058d..778a93e 100644 --- a/src/lib/toast-utils.ts +++ b/src/lib/toast-utils.ts @@ -294,6 +294,26 @@ export function showTwilightUpdateToast( }); } +export function showAutoUpdateToast( + browserName: string, + version: string, + options?: { + id?: string; + description?: string; + duration?: number; + }, +) { + return showToast({ + type: "loading", + title: `${browserName} update started`, + description: + options?.description ?? + `Automatically downloading ${browserName} ${version}. Progress will be shown in download notifications.`, + id: options?.id ?? `auto-update-${browserName.toLowerCase()}-${version}`, + duration: options?.duration ?? 4000, + }); +} + // Generic helper for dismissing toasts export function dismissToast(id: string) { sonnerToast.dismiss(id);