refactor: fetch release information the same way for manual and automatic checks

This commit is contained in:
zhom
2025-06-17 06:17:57 +04:00
parent 251016609f
commit 2d92cbb0e5
7 changed files with 109 additions and 232 deletions
+18 -3
View File
@@ -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();
+2 -3
View File
@@ -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
+52 -50
View File
@@ -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<Mutex<Option<tauri::AppHandle>>>,
update_interval: Interval,
app_handle: Option<tauri::AppHandle>,
}
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<PathBuf, Box<dyn std::error::Error>> {
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<dyn std::error::Error + Send + Sync>> {
@@ -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<dyn std::error::Error + Send + Sync>> {
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)
}
+4
View File
@@ -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 {
-127
View File
@@ -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 (
<Card>
<CardHeader>
<CardTitle className="flex gap-2 items-center">
<LuRefreshCw className="w-5 h-5" />
Background Version Updates
</CardTitle>
<CardDescription>
Browser versions are automatically checked every 3 hours in the
background. New versions are cached and ready when you need them.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Current Status */}
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<div className="flex gap-2 items-center text-sm font-medium">
<LuClock className="w-4 h-4" />
Last Update
</div>
<div className="text-sm text-muted-foreground">
{formatLastUpdateTime(lastUpdateTime)}
</div>
</div>
<div className="space-y-2">
<div className="flex gap-2 items-center text-sm font-medium">
<LuCheckCheck className="w-4 h-4" />
Next Update
</div>
<div className="text-sm text-muted-foreground">
{timeUntilNextUpdate <= 0
? "Now"
: `In ${formatTimeUntilUpdate(timeUntilNextUpdate)}`}
</div>
</div>
</div>
{/* Progress indicator */}
{isUpdating && updateProgress && (
<Alert>
<LuRefreshCw className="w-4 h-4 animate-spin" />
<AlertTitle>Updating Browser Versions</AlertTitle>
<AlertDescription>
{updateProgress.current_browser ? (
<>
Looking for updates for {updateProgress.current_browser} (
{updateProgress.completed_browsers}/
{updateProgress.total_browsers})
</>
) : (
"Starting version update..."
)}
</AlertDescription>
</Alert>
)}
{/* Manual update button */}
<div className="flex justify-between items-center pt-2 border-t">
<div className="space-y-1">
<div className="text-sm font-medium">Manual Update</div>
<div className="text-xs text-muted-foreground">
Check for new browser versions now
</div>
</div>
<LoadingButton
isLoading={isUpdating}
onClick={() => {
void triggerManualUpdate();
}}
variant="outline"
size="sm"
disabled={isUpdating}
>
<LuRefreshCw className="mr-2 w-4 h-4" />
{isUpdating ? "Updating..." : "Check Now"}
</LoadingButton>
</div>
{/* Information about background updates */}
<Alert>
<LuCircleAlert className="w-4 h-4" />
<AlertTitle>How it works</AlertTitle>
<AlertDescription className="text-xs">
Version information is checked automatically every 3 hours
<br /> New versions are added to the cache without removing old
ones
<br /> When creating profiles or changing versions, you&apos;ll see
how many new versions were found
<br /> This keeps the app responsive while ensuring you have the
latest information
</AlertDescription>
</Alert>
</CardContent>
</Card>
);
}
+10 -27
View File
@@ -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<string>
>(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<Set<string>>(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),
};
}
+23 -22
View File
@@ -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(