diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs index aff0d2e..c80ee3c 100644 --- a/src-tauri/src/api_client.rs +++ b/src-tauri/src/api_client.rs @@ -12,6 +12,7 @@ pub struct VersionComponent { pub major: u32, pub minor: u32, pub patch: u32, + pub build: u32, pub pre_release: Option, } @@ -47,6 +48,7 @@ impl VersionComponent { major: 999, // High major version to indicate it's a rolling release minor: 0, patch: 0, + build: 0, pre_release: Some(PreRelease { kind: PreReleaseKind::Alpha, number: Some(999), // High number to indicate it's a rolling release @@ -66,6 +68,7 @@ impl VersionComponent { let major = parts.first().copied().unwrap_or(0); let minor = parts.get(1).copied().unwrap_or(0); let patch = parts.get(2).copied().unwrap_or(0); + let build = parts.get(3).copied().unwrap_or(0); // Parse pre-release part let pre_release = pre_release_part @@ -76,6 +79,7 @@ impl VersionComponent { major, minor, patch, + build, pre_release, } } @@ -173,7 +177,12 @@ impl Ord for VersionComponent { match (self_is_twilight, other_is_twilight) { (true, true) => { // Both are twilight, compare by base version - return (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)); + return (self.major, self.minor, self.patch, self.build).cmp(&( + other.major, + other.minor, + other.patch, + other.build, + )); } (false, false) => { // Neither is twilight, continue with normal comparison @@ -181,8 +190,13 @@ impl Ord for VersionComponent { _ => unreachable!(), // Already handled above } - // Compare major.minor.patch first - match (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) { + // Compare major.minor.patch.build first + match (self.major, self.minor, self.patch, self.build).cmp(&( + other.major, + other.minor, + other.patch, + other.build, + )) { Ordering::Equal => { // If numeric parts are equal, compare pre-release match (&self.pre_release, &other.pre_release) { diff --git a/src-tauri/src/app_auto_updater.rs b/src-tauri/src/app_auto_updater.rs index d4c54f0..656ffa9 100644 --- a/src-tauri/src/app_auto_updater.rs +++ b/src-tauri/src/app_auto_updater.rs @@ -1602,6 +1602,16 @@ rm "{}" #[tauri::command] pub async fn check_for_app_updates() -> Result, String> { + // The disable_auto_updates setting controls app self-updates only + let disabled = crate::settings_manager::SettingsManager::instance() + .load_settings() + .map(|s| s.disable_auto_updates) + .unwrap_or(false); + if disabled { + log::info!("App auto-updates disabled by user setting"); + return Ok(None); + } + let updater = AppAutoUpdater::instance(); updater .check_for_updates() diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index 6c8bb64..ff5c6b4 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -1,5 +1,4 @@ use crate::browser_version_manager::{BrowserVersionInfo, BrowserVersionManager}; -use crate::events; use crate::profile::{BrowserProfile, ProfileManager}; use crate::settings_manager::SettingsManager; use serde::{Deserialize, Serialize}; @@ -146,115 +145,72 @@ impl AutoUpdater { pub async fn check_for_updates_with_progress(&self, app_handle: &tauri::AppHandle) { log::info!("Starting auto-update check with progress..."); - // Check if auto-updates are disabled in settings - let auto_download = { - let disable = self - .settings_manager - .load_settings() - .map(|s| s.disable_auto_updates) - .unwrap_or(false); - !disable && !cfg!(target_os = "linux") - }; + // Browser auto-updates are always enabled — the disable_auto_updates setting + // only controls app self-updates, not browser version updates. // Check for browser updates and trigger auto-downloads match self.check_for_updates().await { Ok(update_notifications) => { - if !update_notifications.is_empty() { - log::info!( - "Found {} browser updates (auto_download={})", - update_notifications.len(), - auto_download - ); + // Group by browser+version to avoid duplicate downloads + let grouped = self.group_update_notifications(update_notifications); + if !grouped.is_empty() { + log::info!("Found {} browser updates", grouped.len()); - if !auto_download { - // Emit notification events instead of downloading - for notification in update_notifications { - let update_event = serde_json::json!({ - "browser": notification.browser, - "new_version": notification.new_version, - "current_version": notification.current_version, - "affected_profiles": notification.affected_profiles - }); - - if let Err(e) = events::emit("browser-update-available", &update_event) { - log::error!( - "Failed to emit update-available event for {}: {e}", - notification.browser - ); - } else { - log::info!( - "Emitted update-available event for {} {}", - notification.browser, - notification.new_version - ); - } - } - return; - } - - // Trigger automatic downloads for each update - for notification in update_notifications { + for notification in grouped { log::info!( - "Auto-downloading {} version {}", + "Auto-updating {} to version {} ({} profiles)", notification.browser, - notification.new_version + notification.new_version, + notification.affected_profiles.len() ); - // Clone app_handle for the async task let browser = notification.browser.clone(); let new_version = notification.new_version.clone(); - let notification_id = notification.id.clone(); - let affected_profiles = notification.affected_profiles.clone(); let app_handle_clone = app_handle.clone(); // Spawn async task to handle the download and auto-update tokio::spawn(async move { - // TODO: update the logic to use the downloaded browsers registry instance instead of the static method - // First, check if browser already exists - match crate::downloaded_browsers_registry::is_browser_downloaded( - browser.clone(), - new_version.clone(), - ) { - true => { - log::info!("Browser {browser} {new_version} already downloaded, proceeding to auto-update profiles"); + let registry = + crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(); - // Browser already exists, go straight to profile update - match AutoUpdater::instance() - .complete_browser_update_with_auto_update( - &app_handle_clone, - &browser.clone(), - &new_version.clone(), - ) - .await - { - Ok(updated_profiles) => { + if registry.is_browser_downloaded(&browser, &new_version) { + log::info!("Browser {browser} {new_version} already downloaded, proceeding to auto-update profiles"); + + // Browser already exists, go straight to profile update + match AutoUpdater::instance() + .auto_update_profile_versions(&app_handle_clone, &browser, &new_version) + .await + { + Ok(updated_profiles) => { + if !updated_profiles.is_empty() { log::info!( - "Auto-update completed for {} profiles: {:?}", + "Auto-updated {} profiles to {browser} {new_version}: {:?}", updated_profiles.len(), updated_profiles ); } - Err(e) => { - log::error!("Failed to complete auto-update for {browser}: {e}"); - } + } + Err(e) => { + log::error!("Failed to auto-update profiles for {browser}: {e}"); } } - false => { - log::info!("Downloading browser {browser} version {new_version}..."); + } else { + log::info!("Downloading browser {browser} version {new_version}..."); - // Emit the auto-update event to trigger frontend handling - let auto_update_event = serde_json::json!({ - "browser": browser, - "new_version": new_version, - "notification_id": notification_id, - "affected_profiles": affected_profiles - }); - - if let Err(e) = events::emit("browser-auto-update-available", &auto_update_event) - { - log::error!("Failed to emit auto-update event for {browser}: {e}"); - } else { - log::info!("Emitted auto-update event for {browser}"); + // Download directly from Rust — download_browser_full already + // auto-updates non-running profiles after successful download. + match crate::downloader::download_browser( + app_handle_clone, + browser.clone(), + new_version.clone(), + ) + .await + { + Ok(actual_version) => { + log::info!("Auto-download completed for {browser} {actual_version}"); + } + Err(e) => { + log::error!("Failed to auto-download {browser} {new_version}: {e}"); } } } @@ -268,6 +224,24 @@ impl AutoUpdater { log::error!("Failed to check for browser updates: {e}"); } } + + // Also update any profiles that can be bumped to an already-installed newer version. + // This handles cases where a version was downloaded but profiles weren't updated + // (e.g., they were running at the time, or the update was missed). + match self.update_profiles_to_latest_installed(app_handle) { + Ok(updated) => { + if !updated.is_empty() { + log::info!( + "Updated {} profiles to latest installed versions: {:?}", + updated.len(), + updated + ); + } + } + Err(e) => { + log::error!("Failed to update profiles to latest installed versions: {e}"); + } + } } /// Check if a specific profile has an available update @@ -532,6 +506,148 @@ impl AutoUpdater { Ok(None) } + + /// Get the latest installed version for a browser from the downloaded browsers registry + pub fn get_latest_installed_version(&self, browser: &str) -> Option { + let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(); + let versions = registry.get_downloaded_versions(browser); + versions + .into_iter() + .filter(|v| registry.is_browser_downloaded(browser, v)) + .max_by(|a, b| self.compare_versions(a, b)) + } + + /// Update a single profile to the latest installed version for its browser. + /// Used when a browser closes to ensure it's on the latest version. + pub fn update_profile_to_latest_installed( + &self, + app_handle: &tauri::AppHandle, + profile: &crate::profile::BrowserProfile, + ) -> Option { + let latest = self.get_latest_installed_version(&profile.browser)?; + + if !self.is_version_newer(&latest, &profile.version) { + return None; + } + + // Only update stable->stable and nightly->nightly + let is_profile_nightly = + crate::api_client::is_browser_version_nightly(&profile.browser, &profile.version, None); + let is_latest_nightly = + crate::api_client::is_browser_version_nightly(&profile.browser, &latest, None); + if is_profile_nightly != is_latest_nightly { + return None; + } + + match self + .profile_manager + .update_profile_version(app_handle, &profile.id.to_string(), &latest) + { + Ok(updated) => { + log::info!( + "Updated profile {} from {} {} to latest installed version {}", + profile.name, + profile.browser, + profile.version, + latest + ); + Some(updated) + } + Err(e) => { + log::error!( + "Failed to update profile {} to latest installed version: {e}", + profile.name + ); + None + } + } + } + + /// Update all non-running profiles to the latest installed version for each browser. + /// Handles the case where a newer version was downloaded but profiles weren't updated. + pub fn update_profiles_to_latest_installed( + &self, + app_handle: &tauri::AppHandle, + ) -> Result, Box> { + let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(); + let profiles = self + .profile_manager + .list_profiles() + .map_err(|e| format!("Failed to list profiles: {e}"))?; + + let mut all_updated = Vec::new(); + + // Group profiles by browser + let mut browser_profiles: HashMap> = HashMap::new(); + for profile in profiles { + if profile.is_cross_os() { + continue; + } + browser_profiles + .entry(profile.browser.clone()) + .or_default() + .push(profile); + } + + for (browser, profiles) in browser_profiles { + let installed_versions = registry.get_downloaded_versions(&browser); + if installed_versions.is_empty() { + continue; + } + + // Find the latest installed version that actually exists on disk + let latest_installed = installed_versions + .iter() + .filter(|v| registry.is_browser_downloaded(&browser, v)) + .max_by(|a, b| self.compare_versions(a, b)); + + let latest_version = match latest_installed { + Some(v) => v.clone(), + None => continue, + }; + + for profile in profiles { + if profile.process_id.is_some() { + continue; + } + + if !self.is_version_newer(&latest_version, &profile.version) { + continue; + } + + // Only update stable->stable and nightly->nightly + let is_profile_nightly = + crate::api_client::is_browser_version_nightly(&browser, &profile.version, None); + let is_latest_nightly = + crate::api_client::is_browser_version_nightly(&browser, &latest_version, None); + if is_profile_nightly != is_latest_nightly { + continue; + } + + match self.profile_manager.update_profile_version( + app_handle, + &profile.id.to_string(), + &latest_version, + ) { + Ok(_) => { + log::info!( + "Updated profile {} from {} {} to latest installed version {}", + profile.name, + browser, + profile.version, + latest_version + ); + all_updated.push(profile.name); + } + Err(e) => { + log::error!("Failed to update profile {}: {e}", profile.name); + } + } + } + } + + Ok(all_updated) + } } // Tauri commands diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 17baef9..bd842af 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1712,6 +1712,16 @@ impl BrowserRunner { } } + // If no pending update was applied, check if a newer installed version exists + if updated_profile.version == profile.version { + if let Some(p) = self + .auto_updater + .update_profile_to_latest_installed(&app_handle, &updated_profile) + { + updated_profile = p; + } + } + self .save_process_info(&updated_profile) .map_err(|e| format!("Failed to update profile: {e}"))?; @@ -2041,6 +2051,16 @@ impl BrowserRunner { } } + // If no pending update was applied, check if a newer installed version exists + if updated_profile.version == profile.version { + if let Some(p) = self + .auto_updater + .update_profile_to_latest_installed(&app_handle, &updated_profile) + { + updated_profile = p; + } + } + self .save_process_info(&updated_profile) .map_err(|e| format!("Failed to update profile: {e}"))?; @@ -2319,6 +2339,16 @@ impl BrowserRunner { } } + // If no pending update was applied, check if a newer installed version exists + if updated_profile.version == profile.version { + if let Some(p) = self + .auto_updater + .update_profile_to_latest_installed(&app_handle, &updated_profile) + { + updated_profile = p; + } + } + self .save_process_info(&updated_profile) .map_err(|e| format!("Failed to update profile: {e}"))?; diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index ea9393d..b8658db 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -1033,28 +1033,17 @@ impl Downloader { tokens.remove(&download_key); } - // Auto-update non-running profiles to the new version and cleanup unused binaries + // Auto-update non-running profiles to the latest installed version and cleanup unused binaries { - let browser_for_update = browser_str.clone(); - let version_for_update = version.clone(); let app_handle_for_update = app_handle.clone(); tauri::async_runtime::spawn(async move { let auto_updater = crate::auto_updater::AutoUpdater::instance(); - match auto_updater - .auto_update_profile_versions( - &app_handle_for_update, - &browser_for_update, - &version_for_update, - ) - .await - { + match auto_updater.update_profiles_to_latest_installed(&app_handle_for_update) { Ok(updated) => { if !updated.is_empty() { log::info!( - "Auto-updated {} profiles to {} {}: {:?}", + "Auto-updated {} profiles to latest installed versions: {:?}", updated.len(), - browser_for_update, - version_for_update, updated ); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 436d574..754281f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1133,6 +1133,28 @@ pub fn run() { } } + // Immediately bump non-running profiles to the latest installed browser version. + // This runs synchronously before any network calls so profiles are updated on launch. + { + let app_handle_bump = app.handle().clone(); + match auto_updater::AutoUpdater::instance() + .update_profiles_to_latest_installed(&app_handle_bump) + { + Ok(updated) => { + if !updated.is_empty() { + log::info!( + "Startup: bumped {} profiles to latest installed versions: {:?}", + updated.len(), + updated + ); + } + } + Err(e) => { + log::error!("Startup: failed to bump profiles to latest installed versions: {e}"); + } + } + } + let app_handle_auto_updater = app.handle().clone(); // Start the auto-update check task separately diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 1da5972..a86990f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -135,8 +135,8 @@ "clearCache": "Clear All Version Cache", "clearCacheDescription": "Clear all cached browser version data and refresh all browser versions from their sources. This will force a fresh download of version information for all browsers." }, - "disableAutoUpdates": "Disable Auto Updates", - "disableAutoUpdatesDescription": "Only notify when browser updates are available, without downloading automatically." + "disableAutoUpdates": "Disable App Auto Updates", + "disableAutoUpdatesDescription": "Prevent the app from automatically checking and installing Donut Browser updates. Browser updates are not affected." }, "header": { "searchPlaceholder": "Search profiles...", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 9a3d1c1..7c6c276 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -135,8 +135,8 @@ "clearCache": "Limpiar Toda la Caché de Versiones", "clearCacheDescription": "Limpia todos los datos de versiones de navegadores en caché y actualiza todas las versiones desde sus fuentes. Esto forzará una descarga nueva de información de versiones para todos los navegadores." }, - "disableAutoUpdates": "Desactivar Actualizaciones Automáticas", - "disableAutoUpdatesDescription": "Evita que la aplicación busque e instale actualizaciones automáticamente. Deberá actualizar la aplicación manualmente." + "disableAutoUpdates": "Desactivar Actualizaciones Automáticas de la App", + "disableAutoUpdatesDescription": "Evita que la aplicación busque e instale actualizaciones de Donut Browser automáticamente. Las actualizaciones de navegadores no se ven afectadas." }, "header": { "searchPlaceholder": "Buscar perfiles...", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 5067c53..e273f5c 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -135,8 +135,8 @@ "clearCache": "Effacer tout le cache des versions", "clearCacheDescription": "Efface toutes les données de versions de navigateurs en cache et actualise toutes les versions depuis leurs sources. Cela forcera un nouveau téléchargement des informations de version pour tous les navigateurs." }, - "disableAutoUpdates": "Désactiver les mises à jour automatiques", - "disableAutoUpdatesDescription": "Empêche l'application de vérifier et d'installer automatiquement les mises à jour. Vous devrez mettre à jour l'application manuellement." + "disableAutoUpdates": "Désactiver les mises à jour automatiques de l'app", + "disableAutoUpdatesDescription": "Empêche l'application de vérifier et d'installer automatiquement les mises à jour de Donut Browser. Les mises à jour des navigateurs ne sont pas affectées." }, "header": { "searchPlaceholder": "Rechercher des profils...", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 94f270b..1debdd1 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -135,8 +135,8 @@ "clearCache": "すべてのバージョンキャッシュをクリア", "clearCacheDescription": "キャッシュされたすべてのブラウザバージョンデータをクリアし、すべてのブラウザバージョンをソースから更新します。これにより、すべてのブラウザのバージョン情報が強制的に再ダウンロードされます。" }, - "disableAutoUpdates": "自動更新を無効にする", - "disableAutoUpdatesDescription": "アプリケーションが自動的に更新を確認・インストールすることを防ぎます。手動でアプリケーションを更新する必要があります。" + "disableAutoUpdates": "アプリの自動更新を無効にする", + "disableAutoUpdatesDescription": "Donut Browserの自動更新確認・インストールを無効にします。ブラウザの更新には影響しません。" }, "header": { "searchPlaceholder": "プロファイルを検索...", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 2646221..2b6a506 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -135,8 +135,8 @@ "clearCache": "Limpar Todo o Cache de Versões", "clearCacheDescription": "Limpa todos os dados de versões de navegadores em cache e atualiza todas as versões de suas fontes. Isso forçará um novo download das informações de versão para todos os navegadores." }, - "disableAutoUpdates": "Desativar Atualizações Automáticas", - "disableAutoUpdatesDescription": "Impede que o aplicativo verifique e instale atualizações automaticamente. Você precisará atualizar o aplicativo manualmente." + "disableAutoUpdates": "Desativar Atualizações Automáticas do App", + "disableAutoUpdatesDescription": "Impede que o aplicativo verifique e instale atualizações do Donut Browser automaticamente. As atualizações de navegadores não são afetadas." }, "header": { "searchPlaceholder": "Pesquisar perfis...", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 7b7b78c..5b1e531 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -135,8 +135,8 @@ "clearCache": "Очистить весь кэш версий", "clearCacheDescription": "Очищает все кэшированные данные версий браузеров и обновляет все версии из источников. Это принудительно загрузит информацию о версиях для всех браузеров." }, - "disableAutoUpdates": "Отключить автоматические обновления", - "disableAutoUpdatesDescription": "Запретить приложению автоматически проверять и устанавливать обновления. Вам нужно будет обновлять приложение вручную." + "disableAutoUpdates": "Отключить автообновление приложения", + "disableAutoUpdatesDescription": "Запретить автоматическую проверку и установку обновлений Donut Browser. Обновления браузеров не затрагиваются." }, "header": { "searchPlaceholder": "Поиск профилей...", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 0e67627..d1c48f3 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -135,8 +135,8 @@ "clearCache": "清除所有版本缓存", "clearCacheDescription": "清除所有缓存的浏览器版本数据并从源刷新所有浏览器版本。这将强制重新下载所有浏览器的版本信息。" }, - "disableAutoUpdates": "禁用自动更新", - "disableAutoUpdatesDescription": "阻止应用程序自动检查和安装更新。您需要手动更新应用程序。" + "disableAutoUpdates": "禁用应用自动更新", + "disableAutoUpdatesDescription": "阻止应用程序自动检查和安装 Donut Browser 更新。浏览器更新不受影响。" }, "header": { "searchPlaceholder": "搜索配置文件...",