diff --git a/src-tauri/src/app_auto_updater.rs b/src-tauri/src/app_auto_updater.rs index 465f01c..d4c54f0 100644 --- a/src-tauri/src/app_auto_updater.rs +++ b/src-tauri/src/app_auto_updater.rs @@ -744,13 +744,36 @@ impl AppAutoUpdater { log::info!("Extracting update..."); let extracted_app_path = self.extract_update(&download_path, &temp_dir).await?; - log::info!("Installing update (overwriting binary)..."); - self.install_update(&extracted_app_path).await?; + // On Windows, MSI/EXE installers close the running app, so running them now + // would kill the process before the "Update ready" toast can appear. Instead, + // defer execution to restart_application() when the user clicks "Restart Now". + #[cfg(target_os = "windows")] + { + let ext = extracted_app_path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + if ext == "msi" || ext == "exe" { + log::info!("Deferring Windows installer execution until user-initiated restart"); + *PENDING_INSTALLER_PATH.lock().unwrap() = Some(extracted_app_path); + } else { + log::info!("Installing update (overwriting binary)..."); + self.install_update(&extracted_app_path).await?; + log::info!("Cleaning up temporary files..."); + let _ = fs::remove_dir_all(&temp_dir); + } + } - log::info!("Cleaning up temporary files..."); - let _ = fs::remove_dir_all(&temp_dir); + #[cfg(not(target_os = "windows"))] + { + log::info!("Installing update (overwriting binary)..."); + self.install_update(&extracted_app_path).await?; + log::info!("Cleaning up temporary files..."); + let _ = fs::remove_dir_all(&temp_dir); + } - log::info!("Update installed successfully, emitting app-update-ready event"); + log::info!("Update ready, emitting app-update-ready event"); let _ = events::emit("app-update-ready", update_info.new_version.clone()); @@ -1421,14 +1444,63 @@ rm "{}" { let app_path = self.get_current_app_path()?; let current_pid = std::process::id(); + let pending = PENDING_INSTALLER_PATH.lock().unwrap().take(); - // Create a temporary restart batch script let temp_dir = std::env::temp_dir(); let script_path = temp_dir.join("donut_restart.bat"); + let update_temp_dir = temp_dir.join("donut_app_update"); - // Create the restart script content - let script_content = format!( - r#"@echo off + let script_content = if let Some(installer_path) = pending { + let ext = installer_path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + let install_cmd = match ext.as_str() { + "msi" => format!( + "msiexec /i \"{}\" /quiet /norestart REBOOT=ReallySuppress", + installer_path.to_str().unwrap() + ), + "exe" => format!("\"{}\" /S", installer_path.to_str().unwrap()), + _ => String::new(), + }; + + format!( + r#"@echo off +rem Wait for the current process to exit +:wait_loop +tasklist /fi "PID eq {pid}" >nul 2>&1 +if %errorlevel% equ 0 ( + timeout /t 1 /nobreak >nul + goto wait_loop +) + +rem Wait a bit more to ensure clean exit +timeout /t 2 /nobreak >nul + +rem Run the installer +{install_cmd} + +rem Wait for installation to complete +timeout /t 3 /nobreak >nul + +rem Start the new application +start "" "{app_path}" + +rem Clean up installer temp files +rmdir /s /q "{update_temp}" + +rem Clean up this script +del "%~f0" +"#, + pid = current_pid, + install_cmd = install_cmd, + app_path = app_path.to_str().unwrap(), + update_temp = update_temp_dir.to_str().unwrap(), + ) + } else { + format!( + r#"@echo off rem Wait for the current process to exit :wait_loop tasklist /fi "PID eq {}" >nul 2>&1 @@ -1446,24 +1518,20 @@ start "" "{}" rem Clean up this script del "%~f0" "#, - current_pid, - app_path.to_str().unwrap() - ); + current_pid, + app_path.to_str().unwrap() + ) + }; - // Write the script to file fs::write(&script_path, script_content)?; - // Execute the restart script in the background let mut cmd = Command::new("cmd"); cmd.args(["/C", script_path.to_str().unwrap()]); - // Start the process detached let _child = cmd.spawn()?; - // Give the script a moment to start tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - // Exit the current process std::process::exit(0); } @@ -1926,4 +1994,5 @@ mod tests { // Global singleton instance lazy_static::lazy_static! { static ref APP_AUTO_UPDATER: AppAutoUpdater = AppAutoUpdater::new(); + static ref PENDING_INSTALLER_PATH: std::sync::Mutex> = std::sync::Mutex::new(None); } diff --git a/src-tauri/src/bin/donut_daemon.rs b/src-tauri/src/bin/donut_daemon.rs index 254b0dd..738f35e 100644 --- a/src-tauri/src/bin/donut_daemon.rs +++ b/src-tauri/src/bin/donut_daemon.rs @@ -316,6 +316,20 @@ fn run_daemon() { } Event::Reopen { .. } => { tray::open_gui(); + + // Re-hide daemon from Dock. macOS activates the daemon (making it + // visible) when the user clicks the Dock icon, overriding the + // Accessory policy set at init. + #[cfg(target_os = "macos")] + { + use objc2::MainThreadMarker; + use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy}; + + if let Some(mtm) = MainThreadMarker::new() { + let app = NSApplication::sharedApplication(mtm); + app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); + } + } } _ => {} } diff --git a/src-tauri/src/camoufox_manager.rs b/src-tauri/src/camoufox_manager.rs index 38beb78..d48603a 100644 --- a/src-tauri/src/camoufox_manager.rs +++ b/src-tauri/src/camoufox_manager.rs @@ -628,6 +628,41 @@ impl CamoufoxManager { } } + // Write DuckDuckGo search engine prefs to user.js for all profiles + { + let user_js_path = profile_path.join("user.js"); + let ddg_prefs = concat!( + "user_pref(\"browser.search.defaultenginename\", \"DuckDuckGo\");\n", + "user_pref(\"browser.search.order.1\", \"DuckDuckGo\");\n", + "user_pref(\"browser.urlbar.placeholderName\", \"DuckDuckGo\");\n", + "user_pref(\"browser.urlbar.placeholderName.private\", \"DuckDuckGo\");\n", + ); + + let needs_write = if user_js_path.exists() { + std::fs::read_to_string(&user_js_path) + .map(|existing| !existing.contains("browser.search.defaultenginename")) + .unwrap_or(false) + } else { + true + }; + + if needs_write { + use std::fs::OpenOptions; + match OpenOptions::new() + .create(true) + .append(true) + .open(&user_js_path) + { + Ok(mut f) => { + if let Err(e) = std::io::Write::write_all(&mut f, ddg_prefs.as_bytes()) { + log::warn!("Failed to write DuckDuckGo prefs to user.js: {e}"); + } + } + Err(e) => log::warn!("Failed to open user.js for DuckDuckGo prefs: {e}"), + } + } + } + self .launch_camoufox( &app_handle, diff --git a/src-tauri/src/daemon/tray.rs b/src-tauri/src/daemon/tray.rs index d584811..652566c 100644 --- a/src-tauri/src/daemon/tray.rs +++ b/src-tauri/src/daemon/tray.rs @@ -74,18 +74,20 @@ fn get_app_bundle_path() -> Option { pub fn open_gui() { log::info!("Opening GUI..."); - // On macOS, use `open` WITHOUT `-n`. The daemon runs with Accessory - // activation policy so macOS won't confuse it with the GUI process. - // `open` will either activate the existing GUI or launch a new one. - // Using `-n` would bypass the single-instance plugin entirely. #[cfg(target_os = "macos")] { - // Use `open -n` to force launching a new process. Without `-n`, macOS - // re-activates the daemon (the existing process from the bundle) instead - // of launching the GUI binary. The single-instance Tauri plugin in the - // GUI handles deduplication if a GUI instance is already running. + // Launch the GUI binary directly. The daemon lives inside the same .app + // bundle, so `open` (even with `-n`) can re-activate the daemon instead + // of launching the GUI. Directly running the binary avoids macOS's app + // activation machinery. The single-instance Tauri plugin in the GUI + // handles deduplication if a GUI instance is already running. if let Some(app_bundle) = get_app_bundle_path() { - let _ = Command::new("open").args(["-n"]).arg(&app_bundle).spawn(); + let gui_binary = app_bundle.join("Contents").join("MacOS").join("Donut"); + if gui_binary.exists() { + let _ = Command::new(&gui_binary).spawn(); + } else { + let _ = Command::new("open").args(["-n"]).arg(&app_bundle).spawn(); + } } else { let _ = Command::new("open").args(["-n", "-a", "Donut"]).spawn(); } diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index 9e8f968..d3a5c03 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -158,7 +158,11 @@ impl Downloader { let release = releases .iter() .find(|r| r.tag_name == version) - .ok_or(format!("Camoufox version {version} not found"))?; + .or_else(|| { + log::info!("Camoufox: requested version {version} not found, using latest available"); + releases.first() + }) + .ok_or("No Camoufox releases found".to_string())?; // Get platform and architecture info let (os, arch) = Self::get_platform_info(); @@ -179,14 +183,10 @@ impl Downloader { .fetch_wayfern_version_with_caching(true) .await?; - // Verify requested version matches available version if version_info.version != version { - return Err( - format!( - "Wayfern version {version} not found. Available version: {}", - version_info.version - ) - .into(), + log::info!( + "Wayfern: requested version {version}, using available version {}", + version_info.version ); } @@ -659,6 +659,41 @@ impl Downloader { return Err("Please accept Wayfern Terms and Conditions before downloading browsers".into()); } + // For Wayfern/Camoufox, resolve the actual available version from the API + let version = if browser_str == "wayfern" { + match self + .api_client + .fetch_wayfern_version_with_caching(true) + .await + { + Ok(info) if info.version != version => { + log::info!( + "Wayfern: requested {version}, using available {}", + info.version + ); + info.version + } + _ => version, + } + } else if browser_str == "camoufox" { + match self + .api_client + .fetch_camoufox_releases_with_caching(true) + .await + { + Ok(releases) if !releases.is_empty() && releases[0].tag_name != version => { + log::info!( + "Camoufox: requested {version}, using available {}", + releases[0].tag_name + ); + releases[0].tag_name.clone() + } + _ => version, + } + } else { + version + }; + // Check if this browser-version pair is already being downloaded let download_key = format!("{browser_str}-{version}"); let cancel_token = { @@ -1033,57 +1068,164 @@ pub async fn cancel_download(browser_str: String, version: String) -> Result<(), } } -/// Set DuckDuckGo as the default search engine in Camoufox policies.json. -/// Removes the fake "None" search engine and explicitly sets DuckDuckGo as default. +/// Find all candidate `distribution/` directories inside the Camoufox browser dir. +/// On macOS: `/.app/Contents/Resources/distribution/` +/// On Linux: `/camoufox/distribution/` +/// On Windows: `/distribution/` +/// Also includes `/distribution/` as a fallback for all platforms. +fn find_camoufox_distribution_dirs(browser_dir: &Path) -> Vec { + let mut dirs = Vec::new(); + + #[cfg(target_os = "macos")] + { + if let Ok(entries) = std::fs::read_dir(browser_dir) { + for entry in entries.flatten() { + if entry.path().extension().is_some_and(|ext| ext == "app") { + dirs.push( + entry + .path() + .join("Contents") + .join("Resources") + .join("distribution"), + ); + } + } + } + } + + #[cfg(target_os = "linux")] + { + let camoufox_subdir = browser_dir.join("camoufox").join("distribution"); + dirs.push(camoufox_subdir); + } + + // Fallback for all platforms + dirs.push(browser_dir.join("distribution")); + + dirs +} + +/// Set DuckDuckGo as the default search engine in Camoufox. +/// Creates or updates distribution/policies.json with a proper DuckDuckGo engine definition. /// Called both at download time and at launch time to cover existing installations. pub fn configure_camoufox_search_engine( browser_dir: &Path, ) -> Result<(), Box> { - let policies_path = browser_dir.join("distribution").join("policies.json"); + let distribution_dirs = find_camoufox_distribution_dirs(browser_dir); - if !policies_path.exists() { - return Ok(()); - } + // Find an existing policies.json, or pick the first candidate dir to create one + let (policies_path, mut policies) = { + let mut found = None; + for dir in &distribution_dirs { + let path = dir.join("policies.json"); + if path.exists() { + if let Ok(content) = std::fs::read_to_string(&path) { + if let Ok(val) = serde_json::from_str::(&content) { + found = Some((path, val)); + break; + } + } + } + } + match found { + Some(f) => f, + None => { + // Pick the first candidate directory that exists (or can be created) + let target_dir = distribution_dirs + .iter() + .find(|d| d.parent().is_some_and(|p| p.exists())) + .or(distribution_dirs.first()) + .ok_or("No suitable distribution directory found")?; + std::fs::create_dir_all(target_dir)?; + ( + target_dir.join("policies.json"), + serde_json::json!({"policies": {}}), + ) + } + } + }; - let content = std::fs::read_to_string(&policies_path)?; - let mut policies: serde_json::Value = serde_json::from_str(&content)?; - - let current_default = policies + // Check if already configured + let has_ddg_default = policies .get("policies") .and_then(|p| p.get("SearchEngines")) .and_then(|se| se.get("Default")) .and_then(|d| d.as_str()) - .unwrap_or(""); + == Some("DuckDuckGo"); - if current_default == "DuckDuckGo" { + let has_ddg_engine = policies + .get("policies") + .and_then(|p| p.get("SearchEngines")) + .and_then(|se| se.get("Add")) + .and_then(|a| a.as_array()) + .is_some_and(|arr| { + arr + .iter() + .any(|e| e.get("Name").and_then(|n| n.as_str()) == Some("DuckDuckGo")) + }); + + if has_ddg_default && has_ddg_engine { return Ok(()); } - if let Some(policies_obj) = policies.get_mut("policies") { - if let Some(se) = policies_obj.get_mut("SearchEngines") { - // Set DuckDuckGo as the explicit default - if let Some(obj) = se.as_object_mut() { - obj.insert( - "Default".to_string(), - serde_json::Value::String("DuckDuckGo".to_string()), - ); - } + let ddg_engine = serde_json::json!({ + "Name": "DuckDuckGo", + "URLTemplate": "https://duckduckgo.com/?q={searchTerms}", + "SuggestURLTemplate": "https://duckduckgo.com/ac/?q={searchTerms}&type=list", + "Method": "GET", + "IconURL": "https://duckduckgo.com/favicon.ico", + "Alias": "ddg" + }); - // Remove the fake "None" search engine entry from Add - if let Some(add_arr) = se.get_mut("Add").and_then(|a| a.as_array_mut()) { - add_arr.retain(|entry| entry.get("Name").and_then(|n| n.as_str()) != Some("None")); - } + // Ensure policies.SearchEngines exists + let policies_obj = policies + .as_object_mut() + .ok_or("Invalid policies.json")? + .entry("policies") + .or_insert(serde_json::json!({})); + let se = policies_obj + .as_object_mut() + .ok_or("Invalid policies object")? + .entry("SearchEngines") + .or_insert(serde_json::json!({})); - // Ensure DuckDuckGo is not in the Remove list - if let Some(remove_arr) = se.get_mut("Remove").and_then(|r| r.as_array_mut()) { - remove_arr.retain(|v| v.as_str() != Some("DuckDuckGo")); - } + if let Some(se_obj) = se.as_object_mut() { + // Set DuckDuckGo as default + se_obj.insert( + "Default".to_string(), + serde_json::Value::String("DuckDuckGo".to_string()), + ); + + // Add DuckDuckGo engine definition if not present + let add_arr = se_obj + .entry("Add") + .or_insert(serde_json::json!([])) + .as_array_mut() + .ok_or("SearchEngines.Add is not an array")?; + + // Remove fake "None" engine + add_arr.retain(|entry| entry.get("Name").and_then(|n| n.as_str()) != Some("None")); + + // Add DuckDuckGo if not already present + if !add_arr + .iter() + .any(|e| e.get("Name").and_then(|n| n.as_str()) == Some("DuckDuckGo")) + { + add_arr.push(ddg_engine); + } + + // Ensure DuckDuckGo is not in the Remove list + if let Some(remove_arr) = se_obj.get_mut("Remove").and_then(|r| r.as_array_mut()) { + remove_arr.retain(|v| v.as_str() != Some("DuckDuckGo")); } } let updated = serde_json::to_string_pretty(&policies)?; std::fs::write(&policies_path, updated)?; - log::info!("Set DuckDuckGo as default search engine in Camoufox policies.json"); + log::info!( + "Configured DuckDuckGo search engine in {}", + policies_path.display() + ); Ok(()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 30a3c17..f78f63f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1006,29 +1006,31 @@ pub fn run() { } }); - let _app_handle_update = app.handle().clone(); tauri::async_runtime::spawn(async move { - log::info!("Starting app update check at startup..."); let updater = app_auto_updater::AppAutoUpdater::instance(); - match updater.check_for_updates().await { - Ok(Some(update_info)) => { - log::info!( - "App update available: {} -> {}", - update_info.current_version, - update_info.new_version - ); - // Emit update available event to the frontend - if let Err(e) = events::emit("app-update-available", &update_info) { - log::error!("Failed to emit app update event: {e}"); - } else { - log::debug!("App update event emitted successfully"); + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(3 * 60 * 60)); + + loop { + interval.tick().await; + + log::info!("Checking for app updates..."); + match updater.check_for_updates().await { + Ok(Some(update_info)) => { + log::info!( + "App update available: {} -> {}", + update_info.current_version, + update_info.new_version + ); + if let Err(e) = events::emit("app-update-available", &update_info) { + log::error!("Failed to emit app update event: {e}"); + } + } + Ok(None) => { + log::debug!("No app updates available"); + } + Err(e) => { + log::error!("Failed to check for app updates: {e}"); } - } - Ok(None) => { - log::debug!("No app updates available"); - } - Err(e) => { - log::error!("Failed to check for app updates: {e}"); } } }); diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 7e82605..9eef72f 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -128,23 +128,10 @@ impl SettingsManager { // Parse the settings file - serde will use default values for missing fields match serde_json::from_str::(&content) { - Ok(settings) => { - // Save the settings back to ensure any missing fields are written with defaults - if let Err(e) = self.save_settings(&settings) { - log::warn!("Warning: Failed to update settings file with defaults: {e}"); - } - Ok(settings) - } + Ok(settings) => Ok(settings), Err(e) => { log::warn!("Warning: Failed to parse settings file, using defaults: {e}"); - let default_settings = AppSettings::default(); - - // Try to save default settings to fix the corrupted file - if let Err(save_error) = self.save_settings(&default_settings) { - log::warn!("Warning: Failed to save default settings: {save_error}"); - } - - Ok(default_settings) + Ok(AppSettings::default()) } } } diff --git a/src/app/page.tsx b/src/app/page.tsx index 53f12b3..6d36039 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -168,6 +168,8 @@ export default function Home() { const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false); const [launchOnLoginDialogOpen, setLaunchOnLoginDialogOpen] = useState(false); const [windowResizeWarningOpen, setWindowResizeWarningOpen] = useState(false); + const [windowResizeWarningBrowserType, setWindowResizeWarningBrowserType] = + useState(undefined); const windowResizeWarningResolver = useRef< ((proceed: boolean) => void) | null >(null); @@ -523,7 +525,6 @@ export default function Home() { error instanceof Error ? error.message : String(error) }`, ); - throw error; } }, [selectedGroupId], @@ -541,6 +542,7 @@ export default function Home() { if (!dismissed) { const proceed = await new Promise((resolve) => { windowResizeWarningResolver.current = resolve; + setWindowResizeWarningBrowserType(profile.browser); setWindowResizeWarningOpen(true); }); if (!proceed) { @@ -1248,6 +1250,7 @@ export default function Home() { { setWindowResizeWarningOpen(false); windowResizeWarningResolver.current?.(proceed); diff --git a/src/components/profile-sync-dialog.tsx b/src/components/profile-sync-dialog.tsx index 190f4ec..b19fc56 100644 --- a/src/components/profile-sync-dialog.tsx +++ b/src/components/profile-sync-dialog.tsx @@ -16,6 +16,7 @@ import { } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { useCloudAuth } from "@/hooks/use-cloud-auth"; import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; import type { BrowserProfile, SyncMode, SyncSettings } from "@/types"; import { isSyncEnabled } from "@/types"; @@ -34,24 +35,34 @@ export function ProfileSyncDialog({ onSyncConfigOpen, }: ProfileSyncDialogProps) { const { t } = useTranslation(); + const { user: cloudUser } = useCloudAuth(); + const isCloudSyncEligible = + cloudUser != null && + cloudUser.plan !== "free" && + (cloudUser.subscriptionStatus === "active" || + cloudUser.planPeriod === "lifetime"); const [isSaving, setIsSaving] = useState(false); const [isSyncing, setIsSyncing] = useState(false); const [syncMode, setSyncMode] = useState( profile?.sync_mode ?? "Disabled", ); - const [hasConfig, setHasConfig] = useState(false); + const [hasSelfHostedConfig, setHasSelfHostedConfig] = useState(false); const [hasE2ePassword, setHasE2ePassword] = useState(false); const [isCheckingConfig, setIsCheckingConfig] = useState(false); + const hasConfig = isCloudSyncEligible || hasSelfHostedConfig; + const checkSyncConfig = useCallback(async () => { setIsCheckingConfig(true); try { const settings = await invoke("get_sync_settings"); - setHasConfig(Boolean(settings.sync_server_url && settings.sync_token)); + setHasSelfHostedConfig( + Boolean(settings.sync_server_url && settings.sync_token), + ); const hasPassword = await invoke("check_has_e2e_password"); setHasE2ePassword(hasPassword); } catch { - setHasConfig(false); + setHasSelfHostedConfig(false); } finally { setIsCheckingConfig(false); } diff --git a/src/components/shared-camoufox-config-form.tsx b/src/components/shared-camoufox-config-form.tsx index e86222d..a140e2b 100644 --- a/src/components/shared-camoufox-config-form.tsx +++ b/src/components/shared-camoufox-config-form.tsx @@ -1046,18 +1046,19 @@ export function SharedCamoufoxConfigForm({ {limitedMode && ( <> -
-
-
+
+
+
+
+
+
+
{t("fingerprint.proFeature")}
-
-
-
)}
@@ -1253,18 +1254,19 @@ export function SharedCamoufoxConfigForm({ {limitedMode && ( <> -
-
-
+
+
+
+
+
+
+
{t("fingerprint.proFeature")}
-
-
-
)}
diff --git a/src/components/wayfern-config-form.tsx b/src/components/wayfern-config-form.tsx index 68ed0bd..8db75dd 100644 --- a/src/components/wayfern-config-form.tsx +++ b/src/components/wayfern-config-form.tsx @@ -998,18 +998,19 @@ export function WayfernConfigForm({ {limitedMode && ( <> -
-
-
+
+
+
+
+
+
+
{t("fingerprint.proFeature")}
-
-
-
)}
@@ -1212,18 +1213,19 @@ export function WayfernConfigForm({ {limitedMode && ( <> -
-
-
+
+
+
+
+
+
+
{t("fingerprint.proFeature")}
-
-
-
)}
diff --git a/src/components/window-resize-warning-dialog.tsx b/src/components/window-resize-warning-dialog.tsx index 5479d45..b7d8507 100644 --- a/src/components/window-resize-warning-dialog.tsx +++ b/src/components/window-resize-warning-dialog.tsx @@ -1,7 +1,7 @@ "use client"; import { invoke } from "@tauri-apps/api/core"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -17,15 +17,23 @@ import { Label } from "@/components/ui/label"; interface WindowResizeWarningDialogProps { isOpen: boolean; onResult: (proceed: boolean) => void; + browserType?: string; } export function WindowResizeWarningDialog({ isOpen, onResult, + browserType, }: WindowResizeWarningDialogProps) { const { t } = useTranslation(); const [dontShowAgain, setDontShowAgain] = useState(false); + useEffect(() => { + if (isOpen) { + setDontShowAgain(false); + } + }, [isOpen]); + const handleContinue = async () => { if (dontShowAgain) { try { @@ -41,6 +49,16 @@ export function WindowResizeWarningDialog({ onResult(false); }; + const isCamoufox = browserType === "camoufox"; + + const title = isCamoufox + ? t("warnings.windowResizeCamoufoxTitle") + : t("warnings.windowResizeTitle"); + + const description = isCamoufox + ? t("warnings.windowResizeCamoufoxDescription") + : t("warnings.windowResizeDescription"); + return ( e.preventDefault()} > - {t("warnings.windowResizeTitle")} + {title} -

- {t("warnings.windowResizeDescription")} -

+

{description}