diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8ee9d0f..5cda46a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -276,10 +276,25 @@ pub fn run() { let app_handle = app.handle().clone(); tauri::async_runtime::spawn(async move { let version_updater = get_version_updater(); - let mut updater_guard = version_updater.lock().await; - updater_guard.set_app_handle(app_handle.clone()).await; - updater_guard.start_background_updates().await; + // Set the app handle + { + let mut updater_guard = version_updater.lock().await; + updater_guard.set_app_handle(app_handle); + } + + // Run startup check without holding the lock + { + let updater_guard = version_updater.lock().await; + if let Err(e) = updater_guard.start_background_updates().await { + eprintln!("Failed to start background updates: {e}"); + } + } + }); + + // Start the background update task separately + tauri::async_runtime::spawn(async move { + version_updater::VersionUpdater::run_background_task().await; }); let app_handle_update = app.handle().clone(); diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 7464891..647fe08 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -4,6 +4,7 @@ use std::fs::{self, create_dir_all}; use std::path::PathBuf; use crate::api_client::ApiClient; +use crate::version_updater; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TableSortingSettings { @@ -210,9 +211,7 @@ pub async fn clear_all_version_cache_and_refetch( .clear_all_cache() .map_err(|e| format!("Failed to clear version cache: {e}"))?; - // Use the version updater to trigger a proper update with progress events - use crate::version_updater::get_version_updater; - let updater = get_version_updater(); + let updater = version_updater::get_version_updater(); let updater_guard = updater.lock().await; updater_guard diff --git a/src-tauri/src/version_updater.rs b/src-tauri/src/version_updater.rs index e998734..8402c27 100644 --- a/src-tauri/src/version_updater.rs +++ b/src-tauri/src/version_updater.rs @@ -1,4 +1,3 @@ -use crate::browser_version_service::BrowserVersionService; use directories::BaseDirs; use serde::{Deserialize, Serialize}; use std::fs; @@ -8,7 +7,9 @@ use std::sync::OnceLock; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tauri::Emitter; use tokio::sync::Mutex; -use tokio::time::{interval, Interval}; +use tokio::time::interval; + +use crate::browser_version_service::BrowserVersionService; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct VersionUpdateProgress { @@ -46,22 +47,21 @@ impl Default for BackgroundUpdateState { pub struct VersionUpdater { version_service: BrowserVersionService, - app_handle: Arc>>, - update_interval: Interval, + app_handle: Option, } impl VersionUpdater { pub fn new() -> Self { - let mut update_interval = interval(Duration::from_secs(5 * 60)); // Check every 5 minutes - update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - Self { version_service: BrowserVersionService::new(), - app_handle: Arc::new(Mutex::new(None)), - update_interval, + app_handle: None, } } + pub fn set_app_handle(&mut self, app_handle: tauri::AppHandle) { + self.app_handle = Some(app_handle); + } + fn get_cache_dir() -> Result> { let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?; let app_name = if cfg!(debug_assertions) { @@ -143,11 +143,6 @@ impl VersionUpdater { should_update } - pub async fn set_app_handle(&self, app_handle: tauri::AppHandle) { - let mut handle = self.app_handle.lock().await; - *handle = Some(app_handle); - } - pub async fn check_and_run_startup_update( &self, ) -> Result<(), Box> { @@ -157,15 +152,10 @@ impl VersionUpdater { return Ok(()); } - let app_handle = { - let handle_guard = self.app_handle.lock().await; - handle_guard.clone() - }; - - if let Some(handle) = app_handle { + if let Some(ref app_handle) = self.app_handle { println!("Running startup version update..."); - match self.update_all_browser_versions(&handle).await { + match self.update_all_browser_versions(app_handle).await { Ok(_) => { // Update the persistent state after successful update let state = BackgroundUpdateState { @@ -191,7 +181,9 @@ impl VersionUpdater { Ok(()) } - pub async fn start_background_updates(&mut self) { + pub async fn start_background_updates( + &self, + ) -> Result<(), Box> { println!( "Starting background version update service (checking every 5 minutes for 3-hour intervals)" ); @@ -201,41 +193,54 @@ impl VersionUpdater { eprintln!("Startup version update failed: {e}"); } + Ok(()) + } + + pub async fn run_background_task() { + let mut update_interval = interval(Duration::from_secs(5 * 60)); // Check every 5 minutes + update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + loop { - self.update_interval.tick().await; + update_interval.tick().await; // Check if we should run an update based on persistent state if !Self::should_run_background_update() { continue; } - // Check if we have an app handle - let app_handle = { - let handle_guard = self.app_handle.lock().await; - handle_guard.clone() - }; + println!("Starting background version update..."); - if let Some(handle) = app_handle { - println!("Starting background version update..."); + // Get the updater instance for this update cycle + let updater = get_version_updater(); + let result = { + let updater_guard = updater.lock().await; + if let Some(ref app_handle) = updater_guard.app_handle { + updater_guard.update_all_browser_versions(app_handle).await + } else { + Err("App handle not available for background update".into()) + } + }; // Release the lock here - match self.update_all_browser_versions(&handle).await { - Ok(_) => { - // Update the persistent state after successful update - let state = BackgroundUpdateState { - last_update_time: Self::get_current_timestamp(), - update_interval_hours: 3, - }; + match result { + Ok(_) => { + // Update the persistent state after successful update + let state = BackgroundUpdateState { + last_update_time: Self::get_current_timestamp(), + update_interval_hours: 3, + }; - if let Err(e) = Self::save_background_update_state(&state) { - eprintln!("Failed to save background update state: {e}"); - } else { - println!("Background version update completed successfully"); - } + if let Err(e) = Self::save_background_update_state(&state) { + eprintln!("Failed to save background update state: {e}"); + } else { + println!("Background version update completed successfully"); } - Err(e) => { - eprintln!("Background version update failed: {e}"); + } + Err(e) => { + eprintln!("Background version update failed: {e}"); - // Emit error event + // Try to emit error event if we have an app handle + let updater_guard = updater.lock().await; + if let Some(ref app_handle) = updater_guard.app_handle { let progress = VersionUpdateProgress { current_browser: "".to_string(), total_browsers: 0, @@ -244,11 +249,9 @@ impl VersionUpdater { browser_new_versions: 0, status: "error".to_string(), }; - let _ = handle.emit("version-update-progress", &progress); + let _ = app_handle.emit("version-update-progress", &progress); } } - } else { - println!("App handle not available, skipping background update"); } } } @@ -330,7 +333,6 @@ impl VersionUpdater { println!("Emitted progress event for browser: {browser}"); } - // Check if individual browser cache is expired before updating if !self.version_service.should_update_cache(browser) { println!("Skipping {browser} - cache is still fresh"); @@ -395,7 +397,7 @@ impl VersionUpdater { println!("Emitted completion progress event"); } - println!("Background version update completed. Found {total_new_versions} new versions total"); + println!("Version update completed. Found {total_new_versions} new versions total"); Ok(results) } diff --git a/src/app/page.tsx b/src/app/page.tsx index 322cc60..c6e2c21 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,6 +25,7 @@ import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications" import { usePermissions } from "@/hooks/use-permissions"; import type { PermissionType } from "@/hooks/use-permissions"; import { useUpdateNotifications } from "@/hooks/use-update-notifications"; +import { useVersionUpdater } from "@/hooks/use-version-updater"; import { showErrorToast } from "@/lib/toast-utils"; import type { BrowserProfile, ProxySettings } from "@/types"; import { invoke } from "@tauri-apps/api/core"; @@ -85,6 +86,9 @@ export default function Home() { const updateNotifications = useUpdateNotifications(loadProfiles); const { checkForUpdates, isUpdating } = updateNotifications; + // Version updater for handling version fetching progress events + useVersionUpdater(); + // Profiles loader with update check (for initial load and manual refresh) const loadProfilesWithUpdateCheck = useCallback(async () => { try { diff --git a/src/components/version-update-settings.tsx b/src/components/version-update-settings.tsx deleted file mode 100644 index 0bbf250..0000000 --- a/src/components/version-update-settings.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { LoadingButton } from "@/components/loading-button"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { useVersionUpdater } from "@/hooks/use-version-updater"; -import { - LuCheckCheck, - LuCircleAlert, - LuClock, - LuRefreshCw, -} from "react-icons/lu"; - -export function VersionUpdateSettings() { - const { - isUpdating, - lastUpdateTime, - timeUntilNextUpdate, - updateProgress, - triggerManualUpdate, - formatTimeUntilUpdate, - formatLastUpdateTime, - } = useVersionUpdater(); - - return ( - - - - - Background Version Updates - - - Browser versions are automatically checked every 3 hours in the - background. New versions are cached and ready when you need them. - - - - {/* Current Status */} -
-
-
- - Last Update -
-
- {formatLastUpdateTime(lastUpdateTime)} -
-
- -
-
- - Next Update -
-
- {timeUntilNextUpdate <= 0 - ? "Now" - : `In ${formatTimeUntilUpdate(timeUntilNextUpdate)}`} -
-
-
- - {/* Progress indicator */} - {isUpdating && updateProgress && ( - - - Updating Browser Versions - - {updateProgress.current_browser ? ( - <> - Looking for updates for {updateProgress.current_browser} ( - {updateProgress.completed_browsers}/ - {updateProgress.total_browsers}) - - ) : ( - "Starting version update..." - )} - - - )} - - {/* Manual update button */} -
-
-
Manual Update
-
- Check for new browser versions now -
-
- { - void triggerManualUpdate(); - }} - variant="outline" - size="sm" - disabled={isUpdating} - > - - {isUpdating ? "Updating..." : "Check Now"} - -
- - {/* Information about background updates */} - - - How it works - - • Version information is checked automatically every 3 hours -
• New versions are added to the cache without removing old - ones -
• When creating profiles or changing versions, you'll see - how many new versions were found -
• This keeps the app responsive while ensuring you have the - latest information -
-
-
-
- ); -} diff --git a/src/hooks/use-update-notifications.tsx b/src/hooks/use-update-notifications.tsx index 4a5ce6c..f51b157 100644 --- a/src/hooks/use-update-notifications.tsx +++ b/src/hooks/use-update-notifications.tsx @@ -1,7 +1,7 @@ import { getBrowserDisplayName } from "@/lib/browser-utils"; import { dismissToast, showToast } from "@/lib/toast-utils"; import { invoke } from "@tauri-apps/api/core"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useRef, useState } from "react"; interface UpdateNotification { id: string; @@ -25,6 +25,11 @@ export function useUpdateNotifications( Set >(new Set()); + const isUpdating = useCallback( + (browser: string) => updatingBrowsers.has(browser), + [updatingBrowsers], + ); + // Add refs to track ongoing operations to prevent duplicates const isCheckingForUpdates = useRef(false); const activeDownloads = useRef>(new Set()); // Track "browser-version" keys @@ -85,10 +90,9 @@ export function useUpdateNotifications( // Mark download as active and disable browser activeDownloads.current.add(downloadKey); + setUpdatingBrowsers((prev) => new Set(prev).add(browser)); try { - // Set browser as updating FIRST before any async operations - setUpdatingBrowsers((prev) => new Set(prev).add(browser)); const browserDisplayName = getBrowserDisplayName(browser); // Dismiss the notification in the backend @@ -136,7 +140,6 @@ export function useUpdateNotifications( }); // Download the browser - this will trigger download progress events automatically - // The use-browser-download hook will handle showing the download progress toasts await invoke("download_browser", { browserStr: browser, version: newVersion, @@ -193,21 +196,11 @@ export function useUpdateNotifications( }); throw downloadError; } - - // 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 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: 8000, - }); + throw error; } finally { - // Remove from active downloads and updating browsers + // Clean up activeDownloads.current.delete(downloadKey); setUpdatingBrowsers((prev) => { const next = new Set(prev); @@ -219,19 +212,9 @@ export function useUpdateNotifications( [onProfilesUpdated], ); - // Clean up notifications when they're no longer needed - useEffect(() => { - // Remove notifications that have been processed - setNotifications((prev) => - prev.filter( - (notification) => !processedNotifications.has(notification.id), - ), - ); - }, [processedNotifications]); - return { notifications, + isUpdating, checkForUpdates, - isUpdating: (browser: string) => updatingBrowsers.has(browser), }; } diff --git a/src/hooks/use-version-updater.ts b/src/hooks/use-version-updater.ts index 896f038..2b0b5aa 100644 --- a/src/hooks/use-version-updater.ts +++ b/src/hooks/use-version-updater.ts @@ -1,9 +1,13 @@ import { getBrowserDisplayName } from "@/lib/browser-utils"; -import { dismissToast, showUnifiedVersionUpdateToast } from "@/lib/toast-utils"; +import { + dismissToast, + showErrorToast, + showSuccessToast, + showUnifiedVersionUpdateToast, +} from "@/lib/toast-utils"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { useCallback, useEffect, useState } from "react"; -import { toast } from "sonner"; interface VersionUpdateProgress { current_browser: string; @@ -80,12 +84,12 @@ export function useVersionUpdater() { dismissToast("unified-version-update"); if (progress.new_versions_found > 0) { - toast.success("Browser versions updated successfully", { + showSuccessToast("Browser versions updated successfully", { duration: 5000, - description: `Found ${progress.new_versions_found} new browser versions. Update notifications will appear shortly.`, + description: "Updates will start automatically.", }); } else { - toast.success("No new browser versions found", { + showSuccessToast("No new browser versions found", { duration: 3000, description: "All browser versions are up to date", }); @@ -98,7 +102,7 @@ export function useVersionUpdater() { setUpdateProgress(null); dismissToast("unified-version-update"); - toast.error("Failed to update browser versions", { + showErrorToast("Failed to update browser versions", { duration: 6000, description: "Check your internet connection and try again", }); @@ -146,17 +150,17 @@ export function useVersionUpdater() { ).length; if (failedUpdates > 0) { - toast.warning("Update completed with some errors", { + showErrorToast("Update completed with some errors", { description: `${totalNewVersions} new versions found, ${failedUpdates} browsers failed to update`, duration: 5000, }); } else if (totalNewVersions > 0) { - toast.success("Browser versions updated successfully", { - description: `Updated ${successfulUpdates} browsers successfully`, + showSuccessToast("Browser versions updated successfully", { + description: `Found ${totalNewVersions} new versions across ${successfulUpdates} browsers. Updates will start automatically.`, duration: 4000, }); } else { - toast.success("No new browser versions found", { + showSuccessToast("No new browser versions found", { description: "All browser versions are up to date", duration: 3000, }); @@ -166,7 +170,7 @@ export function useVersionUpdater() { return results; } catch (error) { console.error("Failed to trigger manual update:", error); - toast.error("Failed to update browser versions", { + showErrorToast("Failed to update browser versions", { description: error instanceof Error ? error.message : "Unknown error occurred", duration: 4000, @@ -188,7 +192,7 @@ export function useVersionUpdater() { // Show notification about new versions if any were found if (result.new_versions_count && result.new_versions_count > 0) { const browserName = getBrowserDisplayName(browserStr); - toast.success( + showSuccessToast( `Found ${result.new_versions_count} new ${browserName} versions!`, { duration: 3000, @@ -207,18 +211,15 @@ export function useVersionUpdater() { ); const formatTimeUntilUpdate = useCallback((seconds: number): string => { - if (seconds <= 0) return "Update overdue"; - - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - - if (hours > 0) { - return `${hours}h ${minutes}m`; + if (seconds < 60) { + return `${seconds} seconds`; } - if (minutes > 0) { - return `${minutes}m`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) { + return `${minutes} minute${minutes === 1 ? "" : "s"}`; } - return "< 1m"; + const hours = Math.floor(minutes / 60); + return `${hours} hour${hours === 1 ? "" : "s"}`; }, []); const formatLastUpdateTime = useCallback(