From e9b54423407249de57803b20c01bf7c325a2561c Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:19:34 +0400 Subject: [PATCH] refactor: cleanup --- .vscode/settings.json | 1 + CLAUDE.md | 17 +- src-tauri/src/auto_updater.rs | 30 +- src-tauri/src/browser.rs | 627 ++------------ src-tauri/src/browser_runner.rs | 401 +-------- src-tauri/src/camoufox_manager.rs | 65 +- src-tauri/src/downloader.rs | 382 ++------- src-tauri/src/extension_manager.rs | 8 +- src-tauri/src/extraction.rs | 91 +- src-tauri/src/lib.rs | 43 +- src-tauri/src/mcp_server.rs | 782 +++++++++++++++++- src-tauri/src/platform_browser.rs | 3 + src-tauri/src/profile/manager.rs | 31 +- src-tauri/src/profile_importer.rs | 386 +++++++-- src-tauri/src/settings_manager.rs | 32 +- src-tauri/src/version_updater.rs | 7 +- src-tauri/src/wayfern_manager.rs | 19 + src/app/page.tsx | 46 +- src/components/cookie-management-dialog.tsx | 4 +- src/components/create-group-dialog.tsx | 2 +- src/components/create-profile-dialog.tsx | 79 +- src/components/custom-toast.tsx | 2 +- src/components/data-table-action-bar.tsx | 4 +- src/components/delete-group-dialog.tsx | 4 +- src/components/edit-group-dialog.tsx | 2 +- .../extension-group-assignment-dialog.tsx | 2 +- .../extension-management-dialog.tsx | 14 +- src/components/group-assignment-dialog.tsx | 2 +- src/components/group-management-dialog.tsx | 16 +- src/components/import-profile-dialog.tsx | 703 +++++++++------- src/components/integrations-dialog.tsx | 2 +- src/components/permission-dialog.tsx | 10 +- src/components/profile-data-table.tsx | 10 +- src/components/proxy-assignment-dialog.tsx | 2 +- src/components/proxy-import-dialog.tsx | 8 +- src/components/proxy-management-dialog.tsx | 14 +- src/components/settings-dialog.tsx | 4 +- src/components/sync-config-dialog.tsx | 6 +- src/components/vpn-check-button.tsx | 2 +- src/components/vpn-import-dialog.tsx | 10 +- src/hooks/use-version-updater.ts | 42 - src/i18n/locales/en.json | 5 - src/i18n/locales/es.json | 5 - src/i18n/locales/fr.json | 5 - src/i18n/locales/ja.json | 5 - src/i18n/locales/pt.json | 5 - src/i18n/locales/ru.json | 5 - src/i18n/locales/zh.json | 5 - src/lib/browser-utils.ts | 5 - src/lib/themes.ts | 48 ++ src/lib/toast-utils.ts | 7 +- src/styles/globals.css | 15 + src/types.ts | 1 + 53 files changed, 1930 insertions(+), 2096 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b9ec5b8..831f0ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -131,6 +131,7 @@ "ntlm", "numpy", "objc", + "oneshot", "opencode", "orhun", "orjson", diff --git a/CLAUDE.md b/CLAUDE.md index 1f21fce..7aaa487 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,4 +17,19 @@ ## UI Theming -- When modifying the UI, don't add random colors that are not controlled by `src/lib/themes.ts` +- Never use hardcoded Tailwind color classes (e.g., `text-red-500`, `bg-green-600`, `border-yellow-400`). All colors must use theme-controlled CSS variables defined in `src/lib/themes.ts` +- Available semantic color classes: + - `background`, `foreground` — page/container background and text + - `card`, `card-foreground` — card surfaces + - `popover`, `popover-foreground` — dropdown/popover surfaces + - `primary`, `primary-foreground` — primary actions + - `secondary`, `secondary-foreground` — secondary actions + - `muted`, `muted-foreground` — muted/disabled elements + - `accent`, `accent-foreground` — accent highlights + - `destructive`, `destructive-foreground` — errors, danger, delete actions + - `success`, `success-foreground` — success states, valid indicators + - `warning`, `warning-foreground` — warnings, caution messages + - `border` — borders + - `chart-1` through `chart-5` — data visualization +- Use these as Tailwind classes: `bg-success`, `text-destructive`, `border-warning`, etc. +- For lighter variants use opacity: `bg-destructive/10`, `bg-success/10`, `border-warning/50` diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index ff5c6b4..04d0d43 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -106,35 +106,7 @@ impl AutoUpdater { // Check each profile for updates for profile in profiles { if let Some(update) = self.check_profile_update(&profile, &versions)? { - // Apply chromium threshold logic - if browser == "chromium" { - // For chromium, only show notifications if there's a significant version jump - // Compare the major version component (first number before the dot) - let current_major: u32 = profile - .version - .split('.') - .next() - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - let new_major: u32 = update - .new_version - .split('.') - .next() - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - - let result = new_major.saturating_sub(current_major); - log::info!( - "Current major version: {current_major}, New major version: {new_major}, Diff: {result}" - ); - if result > 0 { - notifications.push(update); - } else { - log::info!("Skipping chromium update notification: same major version"); - } - } else { - notifications.push(update); - } + notifications.push(update); } } } diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 194f1ba..ddaa333 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -13,11 +13,6 @@ pub struct ProxySettings { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum BrowserType { - Chromium, - Firefox, - FirefoxDeveloper, - Brave, - Zen, Camoufox, Wayfern, } @@ -25,11 +20,6 @@ pub enum BrowserType { impl BrowserType { pub fn as_str(&self) -> &'static str { match self { - BrowserType::Chromium => "chromium", - BrowserType::Firefox => "firefox", - BrowserType::FirefoxDeveloper => "firefox-developer", - BrowserType::Brave => "brave", - BrowserType::Zen => "zen", BrowserType::Camoufox => "camoufox", BrowserType::Wayfern => "wayfern", } @@ -37,11 +27,6 @@ impl BrowserType { pub fn from_str(s: &str) -> Result { match s { - "chromium" => Ok(BrowserType::Chromium), - "firefox" => Ok(BrowserType::Firefox), - "firefox-developer" => Ok(BrowserType::FirefoxDeveloper), - "brave" => Ok(BrowserType::Brave), - "zen" => Ok(BrowserType::Zen), "camoufox" => Ok(BrowserType::Camoufox), "wayfern" => Ok(BrowserType::Wayfern), _ => Err(format!("Unknown browser type: {s}")), @@ -49,6 +34,7 @@ impl BrowserType { } } +#[allow(dead_code)] pub trait Browser: Send + Sync { fn get_executable_path(&self, install_dir: &Path) -> Result>; fn create_launch_args( @@ -88,10 +74,7 @@ mod macos { .filter(|entry| { let binding = entry.file_name(); let name = binding.to_string_lossy(); - name.starts_with("firefox") - || name.starts_with("zen") - || name.starts_with("camoufox") - || name.contains("Browser") + name.starts_with("firefox") || name.starts_with("camoufox") || name.contains("Browser") }) .map(|entry| entry.path()) .collect(); @@ -200,34 +183,6 @@ mod macos { Ok(executable_path) } - pub fn get_chromium_executable_path( - install_dir: &Path, - ) -> Result> { - // Find the .app directory - let app_path = std::fs::read_dir(install_dir)? - .filter_map(Result::ok) - .find(|entry| entry.path().extension().is_some_and(|ext| ext == "app")) - .ok_or("Browser app not found")?; - - // Construct the browser executable path - let mut executable_dir = app_path.path(); - executable_dir.push("Contents"); - executable_dir.push("MacOS"); - - // Find the first executable in the MacOS directory - let executable_path = std::fs::read_dir(&executable_dir)? - .filter_map(Result::ok) - .find(|entry| { - let binding = entry.file_name(); - let name = binding.to_string_lossy(); - name.contains("Chromium") || name.contains("Brave") || name.contains("Google Chrome") - }) - .map(|entry| entry.path()) - .ok_or("No executable found in MacOS directory")?; - - Ok(executable_path) - } - pub fn get_wayfern_executable_path( install_dir: &Path, ) -> Result> { @@ -281,18 +236,7 @@ mod macos { false } - pub fn is_chromium_version_downloaded(install_dir: &Path) -> bool { - // On macOS, check for .app files - if let Ok(entries) = std::fs::read_dir(install_dir) { - for entry in entries.flatten() { - if entry.path().extension().is_some_and(|ext| ext == "app") { - return true; - } - } - } - false - } - + #[allow(dead_code)] pub fn prepare_executable(_executable_path: &Path) -> Result<(), Box> { // On macOS, no special preparation needed Ok(()) @@ -316,20 +260,6 @@ mod linux { // Try common firefox executable locations (nested and flat) let possible_executables = match browser_type { - BrowserType::Firefox | BrowserType::FirefoxDeveloper => vec![ - // Nested "firefox/firefox" or "firefox/firefox-bin" - install_dir.join("firefox").join("firefox"), - install_dir.join("firefox").join("firefox-bin"), - // Flat under version directory - install_dir.join("firefox"), - install_dir.join("firefox-bin"), - // Under a subdirectory matching the browser type - browser_subdir.join("firefox"), - browser_subdir.join("firefox-bin"), - ], - BrowserType::Zen => { - vec![browser_subdir.join("zen"), browser_subdir.join("zen-bin")] - } BrowserType::Camoufox => { vec![ install_dir.join("camoufox-bin"), @@ -360,36 +290,10 @@ mod linux { browser_type: &BrowserType, ) -> Result> { let possible_executables = match browser_type { - BrowserType::Chromium => vec![ - // Direct paths (for manual installations) - install_dir.join("chromium"), - install_dir.join("chrome"), - install_dir.join("chromium-browser"), - // Subdirectory paths (for downloaded archives) - install_dir.join("chrome-linux").join("chrome"), - install_dir.join("chrome-linux").join("chromium"), - install_dir.join("chromium").join("chromium"), - install_dir.join("chromium").join("chrome"), - // Binary subdirectory - install_dir.join("bin").join("chromium"), - install_dir.join("bin").join("chrome"), - ], - BrowserType::Brave => vec![ - install_dir.join("brave"), - install_dir.join("brave-browser"), - install_dir.join("brave-browser-nightly"), - install_dir.join("brave-browser-beta"), - // Subdirectory paths - install_dir.join("brave").join("brave"), - install_dir.join("brave-browser").join("brave"), - install_dir.join("bin").join("brave"), - ], BrowserType::Wayfern => vec![ - // Wayfern extracts to a directory with chromium executable install_dir.join("chromium"), install_dir.join("chrome"), install_dir.join("wayfern"), - // Subdirectory paths (tar.xz may extract to a subdirectory) install_dir.join("wayfern").join("chromium"), install_dir.join("wayfern").join("chrome"), install_dir.join("chrome-linux").join("chrome"), @@ -421,19 +325,6 @@ mod linux { let browser_subdir = install_dir.join(browser_type.as_str()); let possible_executables = match browser_type { - BrowserType::Firefox | BrowserType::FirefoxDeveloper => { - vec![ - // Preferred: executable inside a subdirectory named after the browser type - browser_subdir.join("firefox-bin"), - browser_subdir.join("firefox"), - // Fallback: executable inside a generic "firefox" subdirectory - install_dir.join("firefox").join("firefox-bin"), - install_dir.join("firefox").join("firefox"), - ] - } - BrowserType::Zen => { - vec![browser_subdir.join("zen"), browser_subdir.join("zen-bin")] - } BrowserType::Camoufox => { vec![ install_dir.join("camoufox-bin"), @@ -454,36 +345,10 @@ mod linux { pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool { let possible_executables = match browser_type { - BrowserType::Chromium => vec![ - // Direct paths (for manual installations) - install_dir.join("chromium"), - install_dir.join("chrome"), - install_dir.join("chromium-browser"), - // Subdirectory paths (for downloaded archives) - install_dir.join("chrome-linux").join("chrome"), - install_dir.join("chrome-linux").join("chromium"), - install_dir.join("chromium").join("chromium"), - install_dir.join("chromium").join("chrome"), - // Binary subdirectory - install_dir.join("bin").join("chromium"), - install_dir.join("bin").join("chrome"), - ], - BrowserType::Brave => vec![ - install_dir.join("brave"), - install_dir.join("brave-browser"), - install_dir.join("brave-browser-nightly"), - install_dir.join("brave-browser-beta"), - // Subdirectory paths - install_dir.join("brave").join("brave"), - install_dir.join("brave-browser").join("brave"), - install_dir.join("bin").join("brave"), - ], BrowserType::Wayfern => vec![ - // Wayfern extracts to a directory with chromium executable install_dir.join("chromium"), install_dir.join("chrome"), install_dir.join("wayfern"), - // Subdirectory paths install_dir.join("wayfern").join("chromium"), install_dir.join("wayfern").join("chrome"), install_dir.join("chrome-linux").join("chrome"), @@ -500,6 +365,7 @@ mod linux { false } + #[allow(dead_code)] pub fn prepare_executable(executable_path: &Path) -> Result<(), Box> { // On Linux, ensure the executable has proper permissions log::info!("Setting execute permissions for: {:?}", executable_path); @@ -551,10 +417,7 @@ mod windows { .unwrap_or_default() .to_string_lossy() .to_lowercase(); - if name.starts_with("firefox") - || name.starts_with("zen") - || name.starts_with("camoufox") - || name.contains("browser") + if name.starts_with("firefox") || name.starts_with("camoufox") || name.contains("browser") { return Ok(path); } @@ -571,30 +434,11 @@ mod windows { ) -> Result> { // On Windows, look for .exe files let possible_paths = match browser_type { - BrowserType::Chromium => vec![ - install_dir.join("chromium.exe"), - install_dir.join("chrome.exe"), - install_dir.join("chromium-browser.exe"), - install_dir.join("bin").join("chromium.exe"), - // Common archive extraction patterns - install_dir.join("chrome-win").join("chrome.exe"), - install_dir.join("chromium").join("chromium.exe"), - install_dir.join("chromium").join("chrome.exe"), - ], - BrowserType::Brave => vec![ - install_dir.join("brave.exe"), - install_dir.join("brave-browser.exe"), - install_dir.join("bin").join("brave.exe"), - // Subdirectory patterns - install_dir.join("brave").join("brave.exe"), - install_dir.join("brave-browser").join("brave.exe"), - ], BrowserType::Wayfern => vec![ install_dir.join("chromium.exe"), install_dir.join("chrome.exe"), install_dir.join("wayfern.exe"), install_dir.join("bin").join("chromium.exe"), - // Subdirectory patterns install_dir.join("wayfern").join("chromium.exe"), install_dir.join("wayfern").join("chrome.exe"), install_dir.join("chrome-win").join("chrome.exe"), @@ -618,18 +462,14 @@ mod windows { .unwrap_or_default() .to_string_lossy() .to_lowercase(); - if name.contains("chromium") - || name.contains("brave") - || name.contains("chrome") - || name.contains("wayfern") - { + if name.contains("chromium") || name.contains("chrome") || name.contains("wayfern") { return Ok(path); } } } } - Err("Chromium/Brave/Wayfern executable not found in Windows installation directory".into()) + Err("Chromium/Wayfern executable not found in Windows installation directory".into()) } pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool { @@ -657,10 +497,7 @@ mod windows { .unwrap_or_default() .to_string_lossy() .to_lowercase(); - if name.starts_with("firefox") - || name.starts_with("zen") - || name.starts_with("camoufox") - || name.contains("browser") + if name.starts_with("firefox") || name.starts_with("camoufox") || name.contains("browser") { return true; } @@ -674,30 +511,11 @@ mod windows { pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool { // On Windows, check for .exe files let possible_executables = match browser_type { - BrowserType::Chromium => vec![ - install_dir.join("chromium.exe"), - install_dir.join("chrome.exe"), - install_dir.join("chromium-browser.exe"), - install_dir.join("bin").join("chromium.exe"), - // Common archive extraction patterns - install_dir.join("chrome-win").join("chrome.exe"), - install_dir.join("chromium").join("chromium.exe"), - install_dir.join("chromium").join("chrome.exe"), - ], - BrowserType::Brave => vec![ - install_dir.join("brave.exe"), - install_dir.join("brave-browser.exe"), - install_dir.join("bin").join("brave.exe"), - // Subdirectory patterns - install_dir.join("brave").join("brave.exe"), - install_dir.join("brave-browser").join("brave.exe"), - ], BrowserType::Wayfern => vec![ install_dir.join("chromium.exe"), install_dir.join("chrome.exe"), install_dir.join("wayfern.exe"), install_dir.join("bin").join("chromium.exe"), - // Subdirectory patterns install_dir.join("wayfern").join("chromium.exe"), install_dir.join("wayfern").join("chrome.exe"), install_dir.join("chrome-win").join("chrome.exe"), @@ -722,11 +540,7 @@ mod windows { .unwrap_or_default() .to_string_lossy() .to_lowercase(); - if name.contains("chromium") - || name.contains("brave") - || name.contains("chrome") - || name.contains("wayfern") - { + if name.contains("chromium") || name.contains("chrome") || name.contains("wayfern") { return true; } } @@ -736,236 +550,13 @@ mod windows { false } + #[allow(dead_code)] pub fn prepare_executable(_executable_path: &Path) -> Result<(), Box> { // On Windows, no special preparation needed Ok(()) } } -pub struct FirefoxBrowser { - browser_type: BrowserType, -} - -impl FirefoxBrowser { - pub fn new(browser_type: BrowserType) -> Self { - Self { browser_type } - } -} - -impl Browser for FirefoxBrowser { - fn get_executable_path(&self, install_dir: &Path) -> Result> { - #[cfg(target_os = "macos")] - return macos::get_firefox_executable_path(install_dir); - - #[cfg(target_os = "linux")] - return linux::get_firefox_executable_path(install_dir, &self.browser_type); - - #[cfg(target_os = "windows")] - return windows::get_firefox_executable_path(install_dir); - - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - Err("Unsupported platform".into()) - } - - fn create_launch_args( - &self, - profile_path: &str, - _proxy_settings: Option<&ProxySettings>, - url: Option, - remote_debugging_port: Option, - headless: bool, - ) -> Result, Box> { - let mut args = vec!["-profile".to_string(), profile_path.to_string()]; - - // Add remote debugging if requested - if let Some(port) = remote_debugging_port { - args.push("--start-debugger-server".to_string()); - args.push(port.to_string()); - } - - // Add headless mode if requested - if headless { - args.push("--headless".to_string()); - } - - // Use -no-remote when remote debugging to avoid conflicts with existing instances - if remote_debugging_port.is_some() { - args.push("-no-remote".to_string()); - } - - // Firefox-based browsers use profile directory and user.js for proxy configuration - if let Some(url) = url { - args.push(url); - } - - Ok(args) - } - - fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool { - // Expected structure: binaries// - let browser_dir = binaries_dir.join(self.browser_type.as_str()).join(version); - - log::info!("Firefox browser checking version {version} in directory: {browser_dir:?}"); - - if !browser_dir.exists() { - log::info!("Directory does not exist: {browser_dir:?}"); - return false; - } - - log::info!("Directory exists, checking for browser files..."); - - #[cfg(target_os = "macos")] - return macos::is_firefox_version_downloaded(&browser_dir); - - #[cfg(target_os = "linux")] - return linux::is_firefox_version_downloaded(&browser_dir, &self.browser_type); - - #[cfg(target_os = "windows")] - return windows::is_firefox_version_downloaded(&browser_dir); - - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - { - log::info!("Unsupported platform for browser verification"); - false - } - } - - fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box> { - #[cfg(target_os = "macos")] - return macos::prepare_executable(executable_path); - - #[cfg(target_os = "linux")] - return linux::prepare_executable(executable_path); - - #[cfg(target_os = "windows")] - return windows::prepare_executable(executable_path); - - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - Err("Unsupported platform".into()) - } -} - -// Chromium-based browsers (Chromium, Brave) -pub struct ChromiumBrowser { - #[allow(dead_code)] - browser_type: BrowserType, -} - -impl ChromiumBrowser { - pub fn new(browser_type: BrowserType) -> Self { - Self { browser_type } - } -} - -impl Browser for ChromiumBrowser { - fn get_executable_path(&self, install_dir: &Path) -> Result> { - #[cfg(target_os = "macos")] - return macos::get_chromium_executable_path(install_dir); - - #[cfg(target_os = "linux")] - return linux::get_chromium_executable_path(install_dir, &self.browser_type); - - #[cfg(target_os = "windows")] - return windows::get_chromium_executable_path(install_dir, &self.browser_type); - - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - Err("Unsupported platform".into()) - } - - fn create_launch_args( - &self, - profile_path: &str, - proxy_settings: Option<&ProxySettings>, - url: Option, - remote_debugging_port: Option, - headless: bool, - ) -> Result, Box> { - let mut args = vec![ - format!("--user-data-dir={}", profile_path), - "--no-default-browser-check".to_string(), - "--disable-background-mode".to_string(), - "--disable-component-update".to_string(), - "--disable-background-timer-throttling".to_string(), - "--crash-server-url=".to_string(), - "--disable-updater".to_string(), - // Disable quit confirmation and session restore prompts - "--disable-session-crashed-bubble".to_string(), - "--hide-crash-restore-bubble".to_string(), - "--disable-infobars".to_string(), - // Disable QUIC/HTTP3 to ensure traffic goes through HTTP proxy - "--disable-quic".to_string(), - ]; - - // Add remote debugging if requested - if let Some(port) = remote_debugging_port { - args.push("--remote-debugging-address=0.0.0.0".to_string()); - args.push(format!("--remote-debugging-port={port}")); - } - - // Add headless mode if requested - if headless { - args.push("--headless".to_string()); - } - - // Add proxy configuration if provided - if let Some(proxy) = proxy_settings { - args.push(format!( - "--proxy-server=http://{}:{}", - proxy.host, proxy.port - )); - } - - if let Some(url) = url { - args.push(url); - } - - Ok(args) - } - - fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool { - // Expected structure: binaries// - let browser_dir = binaries_dir.join(self.browser_type.as_str()).join(version); - - log::info!("Chromium browser checking version {version} in directory: {browser_dir:?}"); - - if !browser_dir.exists() { - log::info!("Directory does not exist: {browser_dir:?}"); - return false; - } - - log::info!("Directory exists, checking for browser files..."); - - #[cfg(target_os = "macos")] - return macos::is_chromium_version_downloaded(&browser_dir); - - #[cfg(target_os = "linux")] - return linux::is_chromium_version_downloaded(&browser_dir, &self.browser_type); - - #[cfg(target_os = "windows")] - return windows::is_chromium_version_downloaded(&browser_dir, &self.browser_type); - - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - { - log::info!("Unsupported platform for browser verification"); - false - } - } - - fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box> { - #[cfg(target_os = "macos")] - return macos::prepare_executable(executable_path); - - #[cfg(target_os = "linux")] - return linux::prepare_executable(executable_path); - - #[cfg(target_os = "windows")] - return windows::prepare_executable(executable_path); - - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] - Err("Unsupported platform".into()) - } -} - pub struct CamoufoxBrowser; impl CamoufoxBrowser { @@ -1175,10 +766,6 @@ impl BrowserFactory { pub fn create_browser(&self, browser_type: BrowserType) -> Box { match browser_type { - BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => { - Box::new(FirefoxBrowser::new(browser_type)) - } - BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)), BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()), BrowserType::Wayfern => Box::new(WayfernBrowser::new()), } @@ -1272,35 +859,10 @@ mod tests { #[test] fn test_browser_type_conversions() { // Test as_str - assert_eq!(BrowserType::Firefox.as_str(), "firefox"); - assert_eq!(BrowserType::FirefoxDeveloper.as_str(), "firefox-developer"); - assert_eq!(BrowserType::Chromium.as_str(), "chromium"); - assert_eq!(BrowserType::Brave.as_str(), "brave"); - assert_eq!(BrowserType::Zen.as_str(), "zen"); assert_eq!(BrowserType::Camoufox.as_str(), "camoufox"); assert_eq!(BrowserType::Wayfern.as_str(), "wayfern"); - // Test from_str - use expect with descriptive messages instead of unwrap - assert_eq!( - BrowserType::from_str("firefox").expect("firefox should be valid"), - BrowserType::Firefox - ); - assert_eq!( - BrowserType::from_str("firefox-developer").expect("firefox-developer should be valid"), - BrowserType::FirefoxDeveloper - ); - assert_eq!( - BrowserType::from_str("chromium").expect("chromium should be valid"), - BrowserType::Chromium - ); - assert_eq!( - BrowserType::from_str("brave").expect("brave should be valid"), - BrowserType::Brave - ); - assert_eq!( - BrowserType::from_str("zen").expect("zen should be valid"), - BrowserType::Zen - ); + // Test from_str assert_eq!( BrowserType::from_str("camoufox").expect("camoufox should be valid"), BrowserType::Camoufox @@ -1320,25 +882,25 @@ mod tests { let empty_result = BrowserType::from_str(""); assert!(empty_result.is_err(), "Empty string should return error"); - let case_sensitive_result = BrowserType::from_str("Firefox"); assert!( - case_sensitive_result.is_err(), - "Case sensitive check should fail" + BrowserType::from_str("firefox").is_err(), + "Removed browser types should return error" + ); + assert!( + BrowserType::from_str("chromium").is_err(), + "Removed browser types should return error" ); } #[test] - fn test_firefox_launch_args() { - // Test regular Firefox (should not use -no-remote for normal launch) - let browser = FirefoxBrowser::new(BrowserType::Firefox); + fn test_camoufox_launch_args() { + let browser = CamoufoxBrowser::new(); let args = browser .create_launch_args("/path/to/profile", None, None, None, false) - .expect("Failed to create launch args for Firefox"); - assert_eq!(args, vec!["-profile", "/path/to/profile"]); - assert!( - !args.contains(&"-no-remote".to_string()), - "Firefox should not use -no-remote for normal launch" - ); + .expect("Failed to create launch args for Camoufox"); + assert!(args.contains(&"-profile".to_string())); + assert!(args.contains(&"/path/to/profile".to_string())); + assert!(args.contains(&"-no-remote".to_string())); let args = browser .create_launch_args( @@ -1348,40 +910,20 @@ mod tests { None, false, ) - .expect("Failed to create launch args for Firefox with URL"); - assert_eq!( - args, - vec!["-profile", "/path/to/profile", "https://example.com"] - ); + .expect("Failed to create launch args for Camoufox with URL"); + assert!(args.contains(&"https://example.com".to_string())); - // Test Firefox with remote debugging (should use -no-remote) + // Test with remote debugging let args = browser .create_launch_args("/path/to/profile", None, None, Some(9222), false) - .expect("Failed to create launch args for Firefox with remote debugging"); - assert!( - args.contains(&"-no-remote".to_string()), - "Firefox should use -no-remote for remote debugging" - ); - assert!( - args.contains(&"--start-debugger-server".to_string()), - "Firefox should include debugger server arg" - ); - assert!( - args.contains(&"9222".to_string()), - "Firefox should include debugging port" - ); - - // Test Zen Browser (no special flags without remote debugging) - let browser = FirefoxBrowser::new(BrowserType::Zen); - let args = browser - .create_launch_args("/path/to/profile", None, None, None, false) - .expect("Failed to create launch args for Zen Browser"); - assert_eq!(args, vec!["-profile", "/path/to/profile"]); + .expect("Failed to create launch args for Camoufox with remote debugging"); + assert!(args.contains(&"--start-debugger-server".to_string())); + assert!(args.contains(&"9222".to_string())); // Test headless mode let args = browser .create_launch_args("/path/to/profile", None, None, None, true) - .expect("Failed to create launch args for Zen Browser headless"); + .expect("Failed to create launch args for Camoufox headless"); assert!( args.contains(&"--headless".to_string()), "Browser should include headless flag when requested" @@ -1389,30 +931,27 @@ mod tests { } #[test] - fn test_chromium_launch_args() { - let browser = ChromiumBrowser::new(BrowserType::Chromium); + fn test_wayfern_launch_args() { + let browser = WayfernBrowser::new(); let args = browser .create_launch_args("/path/to/profile", None, None, None, false) - .expect("Failed to create launch args for Chromium"); + .expect("Failed to create launch args for Wayfern"); - // Test that basic required arguments are present assert!( args.contains(&"--user-data-dir=/path/to/profile".to_string()), - "Chromium args should contain user-data-dir" + "Wayfern args should contain user-data-dir" ); assert!( args.contains(&"--no-default-browser-check".to_string()), - "Chromium args should contain no-default-browser-check" + "Wayfern args should contain no-default-browser-check" ); - - // Test that automatic update disabling arguments are present assert!( args.contains(&"--disable-background-mode".to_string()), - "Chromium args should contain disable-background-mode" + "Wayfern args should contain disable-background-mode" ); assert!( args.contains(&"--disable-component-update".to_string()), - "Chromium args should contain disable-component-update" + "Wayfern args should contain disable-component-update" ); let args_with_url = browser @@ -1423,13 +962,11 @@ mod tests { None, false, ) - .expect("Failed to create launch args for Chromium with URL"); + .expect("Failed to create launch args for Wayfern with URL"); assert!( args_with_url.contains(&"https://example.com".to_string()), - "Chromium args should contain the URL" + "Wayfern args should contain the URL" ); - - // Verify URL is at the end assert_eq!( args_with_url.last().expect("Args should not be empty"), "https://example.com" @@ -1438,23 +975,19 @@ mod tests { // Test remote debugging let args_with_debug = browser .create_launch_args("/path/to/profile", None, None, Some(9222), false) - .expect("Failed to create launch args for Chromium with remote debugging"); + .expect("Failed to create launch args for Wayfern with remote debugging"); assert!( args_with_debug.contains(&"--remote-debugging-port=9222".to_string()), - "Chromium args should contain remote debugging port" - ); - assert!( - args_with_debug.contains(&"--remote-debugging-address=0.0.0.0".to_string()), - "Chromium args should contain remote debugging address" + "Wayfern args should contain remote debugging port" ); // Test headless mode let args_headless = browser .create_launch_args("/path/to/profile", None, None, None, true) - .expect("Failed to create launch args for Chromium headless"); + .expect("Failed to create launch args for Wayfern headless"); assert!( - args_headless.contains(&"--headless".to_string()), - "Chromium args should contain headless flag when requested" + args_headless.contains(&"--headless=new".to_string()), + "Wayfern args should contain headless flag when requested" ); } @@ -1491,26 +1024,21 @@ mod tests { let temp_dir = TempDir::new().expect("Failed to create temp directory"); let binaries_dir = temp_dir.path(); - // Create a mock Firefox browser installation with new path structure: binaries/// - let browser_dir = binaries_dir.join("firefox").join("139.0"); + // Create a mock Camoufox browser installation + let browser_dir = binaries_dir.join("camoufox").join("135.0.1"); fs::create_dir_all(&browser_dir).expect("Failed to create browser directory"); #[cfg(target_os = "macos")] { - // Create a mock .app directory for macOS - let app_dir = browser_dir.join("Firefox.app"); - fs::create_dir_all(&app_dir).expect("Failed to create Firefox.app directory"); + let app_dir = browser_dir.join("Camoufox.app"); + fs::create_dir_all(&app_dir).expect("Failed to create Camoufox.app directory"); } #[cfg(target_os = "linux")] { - // Create a mock firefox subdirectory and executable for Linux - let firefox_subdir = browser_dir.join("firefox"); - fs::create_dir_all(&firefox_subdir).expect("Failed to create firefox subdirectory"); - let executable_path = firefox_subdir.join("firefox"); + let executable_path = browser_dir.join("camoufox"); fs::write(&executable_path, "mock executable").expect("Failed to write mock executable"); - // Set executable permissions on Linux use std::os::unix::fs::PermissionsExt; let mut permissions = executable_path .metadata() @@ -1523,67 +1051,62 @@ mod tests { #[cfg(target_os = "windows")] { - // Create a mock firefox.exe for Windows let executable_path = browser_dir.join("firefox.exe"); fs::write(&executable_path, "mock executable").expect("Failed to write mock executable"); } - let browser = FirefoxBrowser::new(BrowserType::Firefox); - assert!(browser.is_version_downloaded("139.0", binaries_dir)); - assert!(!browser.is_version_downloaded("140.0", binaries_dir)); + let browser = CamoufoxBrowser::new(); + assert!(browser.is_version_downloaded("135.0.1", binaries_dir)); + assert!(!browser.is_version_downloaded("999.0", binaries_dir)); - // Test with Chromium browser with new path structure - let chromium_dir = binaries_dir.join("chromium").join("1465660"); - fs::create_dir_all(&chromium_dir).expect("Failed to create chromium directory"); + // Test with Wayfern browser + let wayfern_dir = binaries_dir.join("wayfern").join("1.0.0"); + fs::create_dir_all(&wayfern_dir).expect("Failed to create wayfern directory"); #[cfg(target_os = "macos")] { - let chromium_app_dir = chromium_dir.join("Chromium.app"); - fs::create_dir_all(chromium_app_dir.join("Contents").join("MacOS")) + let wayfern_app_dir = wayfern_dir.join("Chromium.app"); + fs::create_dir_all(wayfern_app_dir.join("Contents").join("MacOS")) .expect("Failed to create Chromium.app structure"); - // Create a mock executable - let executable_path = chromium_app_dir + let executable_path = wayfern_app_dir .join("Contents") .join("MacOS") .join("Chromium"); fs::write(&executable_path, "mock executable") - .expect("Failed to write mock Chromium executable"); + .expect("Failed to write mock Wayfern executable"); } #[cfg(target_os = "linux")] { - // Create a mock chromium executable for Linux - let executable_path = chromium_dir.join("chromium"); + let executable_path = wayfern_dir.join("chromium"); fs::write(&executable_path, "mock executable") - .expect("Failed to write mock chromium executable"); + .expect("Failed to write mock wayfern executable"); - // Set executable permissions on Linux use std::os::unix::fs::PermissionsExt; let mut permissions = executable_path .metadata() - .expect("Failed to get chromium metadata") + .expect("Failed to get wayfern metadata") .permissions(); permissions.set_mode(0o755); fs::set_permissions(&executable_path, permissions) - .expect("Failed to set chromium permissions"); + .expect("Failed to set wayfern permissions"); } #[cfg(target_os = "windows")] { - // Create a mock chromium.exe for Windows - let executable_path = chromium_dir.join("chromium.exe"); + let executable_path = wayfern_dir.join("chromium.exe"); fs::write(&executable_path, "mock executable").expect("Failed to write mock chromium.exe"); } - let chromium_browser = ChromiumBrowser::new(BrowserType::Chromium); + let wayfern_browser = WayfernBrowser::new(); assert!( - chromium_browser.is_version_downloaded("1465660", binaries_dir), - "Chromium version should be detected as downloaded" + wayfern_browser.is_version_downloaded("1.0.0", binaries_dir), + "Wayfern version should be detected as downloaded" ); assert!( - !chromium_browser.is_version_downloaded("1465661", binaries_dir), - "Non-existent Chromium version should not be detected as downloaded" + !wayfern_browser.is_version_downloaded("9.9.9", binaries_dir), + "Non-existent Wayfern version should not be detected as downloaded" ); } @@ -1593,28 +1116,28 @@ mod tests { let binaries_dir = temp_dir.path(); // Create browser directory but no proper executable structure - let browser_dir = binaries_dir.join("firefox").join("139.0"); + let browser_dir = binaries_dir.join("camoufox").join("135.0.1"); fs::create_dir_all(&browser_dir).expect("Failed to create browser directory"); // Create some other files but no proper executable structure fs::write(browser_dir.join("readme.txt"), "Some content").expect("Failed to write readme file"); - let browser = FirefoxBrowser::new(BrowserType::Firefox); + let browser = CamoufoxBrowser::new(); assert!( - !browser.is_version_downloaded("139.0", binaries_dir), - "Firefox version should not be detected without proper executable structure" + !browser.is_version_downloaded("135.0.1", binaries_dir), + "Camoufox version should not be detected without proper executable structure" ); } #[test] fn test_browser_type_clone_and_debug() { - let browser_type = BrowserType::Firefox; + let browser_type = BrowserType::Camoufox; let cloned = browser_type.clone(); assert_eq!(browser_type, cloned); // Test Debug trait let debug_str = format!("{browser_type:?}"); - assert!(debug_str.contains("Firefox")); + assert!(debug_str.contains("Camoufox")); } #[test] diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 0e5bb2e..1e8b5b8 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -1,4 +1,4 @@ -use crate::browser::{create_browser, BrowserType, ProxySettings}; +use crate::browser::ProxySettings; use crate::camoufox_manager::{CamoufoxConfig, CamoufoxManager}; use crate::cloud_auth::CLOUD_AUTH; use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; @@ -98,9 +98,9 @@ impl BrowserRunner { app_handle: tauri::AppHandle, profile: &BrowserProfile, url: Option, - local_proxy_settings: Option<&ProxySettings>, + _local_proxy_settings: Option<&ProxySettings>, remote_debugging_port: Option, - headless: bool, + _headless: bool, ) -> Result> { // Handle Camoufox profiles using CamoufoxManager if profile.browser == "camoufox" { @@ -613,248 +613,12 @@ impl BrowserRunner { return Ok(updated_profile); } - // Create browser instance - let browser_type = BrowserType::from_str(&profile.browser) - .map_err(|_| format!("Invalid browser type: {}", profile.browser))?; - let browser = create_browser(browser_type.clone()); - - // Get executable path using common helper - let executable_path = self - .get_browser_executable_path(profile) - .expect("Failed to get executable path"); - - log::info!("Executable path: {executable_path:?}"); - - // Prepare the executable (set permissions, etc.) - if let Err(e) = browser.prepare_executable(&executable_path) { - log::warn!("Warning: Failed to prepare executable: {e}"); - // Continue anyway, the error might not be critical - } - - // Refresh cloud proxy credentials if needed before resolving - let _stored_proxy_settings = self - .resolve_proxy_with_refresh(profile.proxy_id.as_ref(), Some(&profile.id.to_string())) - .await; - - // Use provided local proxy for Chromium-based browsers launch arguments - let proxy_for_launch_args: Option<&ProxySettings> = local_proxy_settings; - - // Get profile data path and launch arguments - let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let browser_args = browser - .create_launch_args( - &profile_data_path.to_string_lossy(), - proxy_for_launch_args, - url, - remote_debugging_port, - headless, - ) - .expect("Failed to create launch arguments"); - - // Launch browser using platform-specific method - let child = { - #[cfg(target_os = "macos")] - { - platform_browser::macos::launch_browser_process(&executable_path, &browser_args).await? - } - - #[cfg(target_os = "windows")] - { - platform_browser::windows::launch_browser_process(&executable_path, &browser_args).await? - } - - #[cfg(target_os = "linux")] - { - platform_browser::linux::launch_browser_process(&executable_path, &browser_args).await? - } - - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] - { - return Err("Unsupported platform for browser launching".into()); - } - }; - - let launcher_pid = child.id(); - - log::info!( - "Launched browser with launcher PID: {} for profile: {} (ID: {})", - launcher_pid, - profile.name, - profile.id - ); - - // On macOS, when launching via `open -a`, the child PID is the `open` helper. - // Resolve and store the actual browser PID for all browser types. - let actual_pid = { - #[cfg(target_os = "macos")] - { - // Give the browser a moment to start - tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await; - - let system = System::new_all(); - let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); - let profile_data_path_str = profile_data_path.to_string_lossy(); - - let mut resolved_pid = launcher_pid; - - for (pid, process) in system.processes() { - let cmd = process.cmd(); - if cmd.is_empty() { - continue; - } - - // Determine if this process matches the intended browser type - let exe_name_lower = process.name().to_string_lossy().to_lowercase(); - let is_correct_browser = match profile.browser.as_str() { - "firefox" => { - exe_name_lower.contains("firefox") - && !exe_name_lower.contains("developer") - && !exe_name_lower.contains("camoufox") - } - "firefox-developer" => { - // More flexible detection for Firefox Developer Edition - (exe_name_lower.contains("firefox") && exe_name_lower.contains("developer")) - || (exe_name_lower.contains("firefox") - && cmd.iter().any(|arg| { - let arg_str = arg.to_str().unwrap_or(""); - arg_str.contains("Developer") - || arg_str.contains("developer") - || arg_str.contains("FirefoxDeveloperEdition") - || arg_str.contains("firefox-developer") - })) - || exe_name_lower == "firefox" // Firefox Developer might just show as "firefox" - } - "zen" => exe_name_lower.contains("zen"), - "chromium" => exe_name_lower.contains("chromium") || exe_name_lower.contains("chrome"), - "brave" => exe_name_lower.contains("brave") || exe_name_lower.contains("Brave"), - _ => false, - }; - - if !is_correct_browser { - continue; - } - - // Check for profile path match - let profile_path_match = if matches!( - profile.browser.as_str(), - "firefox" | "firefox-developer" | "zen" - ) { - // Firefox-based browsers: look for -profile argument followed by path - let mut found_profile_arg = false; - for (i, arg) in cmd.iter().enumerate() { - if let Some(arg_str) = arg.to_str() { - if arg_str == "-profile" && i + 1 < cmd.len() { - if let Some(next_arg) = cmd.get(i + 1).and_then(|a| a.to_str()) { - if next_arg == profile_data_path_str { - found_profile_arg = true; - break; - } - } - } - // Also check for combined -profile=path format - if arg_str == format!("-profile={profile_data_path_str}") { - found_profile_arg = true; - break; - } - // Check if the argument is the profile path directly - if arg_str == profile_data_path_str { - found_profile_arg = true; - break; - } - } - } - found_profile_arg - } else { - // Chromium-based browsers: look for --user-data-dir argument - cmd.iter().any(|s| { - if let Some(arg) = s.to_str() { - arg == format!("--user-data-dir={profile_data_path_str}") - || arg == profile_data_path_str - } else { - false - } - }) - }; - - if profile_path_match { - let pid_u32 = pid.as_u32(); - if pid_u32 != launcher_pid { - resolved_pid = pid_u32; - break; - } - } - } - - resolved_pid - } - - #[cfg(not(target_os = "macos"))] - { - launcher_pid - } - }; - - // Update profile with process info and save - let mut updated_profile = profile.clone(); - updated_profile.process_id = Some(actual_pid); - updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); - - self.save_process_info(&updated_profile)?; - let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { - let _ = tm.rebuild_from_profiles(&self.profile_manager.list_profiles().unwrap_or_default()); - }); - - // Apply proxy settings if needed (for Firefox-based browsers) - if profile.proxy_id.is_some() - && matches!( - browser_type, - BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen - ) - { - // Proxy settings for Firefox-based browsers are applied via user.js file - // which is already handled in the profile creation process - } - - log::info!( - "Emitting profile events for successful launch: {} (ID: {})", - updated_profile.name, - updated_profile.id - ); - - // Emit profile update event to frontend - if let Err(e) = events::emit("profile-updated", &updated_profile) { - log::warn!("Warning: Failed to emit profile update event: {e}"); - } - - // Emit minimal running changed event to frontend with a small delay to ensure UI consistency - #[derive(Serialize)] - struct RunningChangedPayload { - id: String, - is_running: bool, - } - let payload = RunningChangedPayload { - id: updated_profile.id.to_string(), - is_running: updated_profile.process_id.is_some(), - }; - - if let Err(e) = events::emit("profile-running-changed", &payload) { - log::warn!("Warning: Failed to emit profile running changed event: {e}"); - } else { - log::info!( - "Successfully emitted profile-running-changed event for {}: running={}", - updated_profile.name, - payload.is_running - ); - } - - Ok(updated_profile) + Err(format!("Unsupported browser type: {}", profile.browser).into()) } pub async fn open_url_in_existing_browser( &self, - app_handle: tauri::AppHandle, + _app_handle: tauri::AppHandle, profile: &BrowserProfile, url: &str, _internal_proxy_settings: Option<&ProxySettings>, @@ -948,134 +712,7 @@ impl BrowserRunner { } } - // Use the comprehensive browser status check for non-camoufox/wayfern browsers - let is_running = self - .check_browser_status(app_handle.clone(), profile) - .await?; - - if !is_running { - return Err("Browser is not running".into()); - } - - // Get the updated profile with current PID - let profiles = self - .profile_manager - .list_profiles() - .expect("Failed to list profiles"); - let updated_profile = profiles - .into_iter() - .find(|p| p.id == profile.id) - .unwrap_or_else(|| profile.clone()); - - // Ensure we have a valid process ID - if updated_profile.process_id.is_none() { - return Err("No valid process ID found for the browser".into()); - } - - let browser_type = BrowserType::from_str(&updated_profile.browser) - .map_err(|_| format!("Invalid browser type: {}", updated_profile.browser))?; - - // Get browser directory for all platforms - path structure: binaries/// - let mut browser_dir = self.get_binaries_dir(); - browser_dir.push(&updated_profile.browser); - browser_dir.push(&updated_profile.version); - - match browser_type { - BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => { - #[cfg(target_os = "macos")] - { - let profiles_dir = self.profile_manager.get_profiles_dir(); - return platform_browser::macos::open_url_in_existing_browser_firefox_like( - &updated_profile, - url, - browser_type, - &browser_dir, - &profiles_dir, - ) - .await; - } - - #[cfg(target_os = "windows")] - { - let profiles_dir = self.profile_manager.get_profiles_dir(); - return platform_browser::windows::open_url_in_existing_browser_firefox_like( - &updated_profile, - url, - browser_type, - &browser_dir, - &profiles_dir, - ) - .await; - } - - #[cfg(target_os = "linux")] - { - let profiles_dir = self.profile_manager.get_profiles_dir(); - return platform_browser::linux::open_url_in_existing_browser_firefox_like( - &updated_profile, - url, - browser_type, - &browser_dir, - &profiles_dir, - ) - .await; - } - - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] - return Err("Unsupported platform".into()); - } - BrowserType::Camoufox => { - // Camoufox URL opening is handled differently - Err("URL opening in existing Camoufox instance is not supported".into()) - } - BrowserType::Wayfern => { - // Wayfern URL opening is handled differently - Err("URL opening in existing Wayfern instance is not supported".into()) - } - BrowserType::Chromium | BrowserType::Brave => { - #[cfg(target_os = "macos")] - { - let profiles_dir = self.profile_manager.get_profiles_dir(); - return platform_browser::macos::open_url_in_existing_browser_chromium( - &updated_profile, - url, - browser_type, - &browser_dir, - &profiles_dir, - ) - .await; - } - - #[cfg(target_os = "windows")] - { - let profiles_dir = self.profile_manager.get_profiles_dir(); - return platform_browser::windows::open_url_in_existing_browser_chromium( - &updated_profile, - url, - browser_type, - &browser_dir, - &profiles_dir, - ) - .await; - } - - #[cfg(target_os = "linux")] - { - let profiles_dir = self.profile_manager.get_profiles_dir(); - return platform_browser::linux::open_url_in_existing_browser_chromium( - &updated_profile, - url, - browser_type, - &browser_dir, - &profiles_dir, - ) - .await; - } - - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] - return Err("Unsupported platform".into()); - } - } + Err(format!("Unsupported browser type: {}", profile.browser).into()) } pub async fn launch_browser_with_debugging( @@ -1115,32 +752,6 @@ impl BrowserRunner { let internal_proxy_settings = Some(internal_proxy.clone()); - // Configure Firefox profiles to use local proxy - { - // For Firefox-based browsers, apply PAC/user.js to point to the local proxy - if matches!( - profile.browser.as_str(), - "firefox" | "firefox-developer" | "zen" - ) { - let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - - // Provide a dummy upstream (ignored when internal proxy is provided) - let dummy_upstream = ProxySettings { - proxy_type: "http".to_string(), - host: "127.0.0.1".to_string(), - port: internal_proxy.port, - username: None, - password: None, - }; - - self - .profile_manager - .apply_proxy_settings_to_profile(&profile_path, &dummy_upstream, Some(&internal_proxy)) - .map_err(|e| format!("Failed to update profile proxy: {e}"))?; - } - } - let result = self .launch_browser_internal( app_handle.clone(), diff --git a/src-tauri/src/camoufox_manager.rs b/src-tauri/src/camoufox_manager.rs index 0f1d190..4109710 100644 --- a/src-tauri/src/camoufox_manager.rs +++ b/src-tauri/src/camoufox_manager.rs @@ -56,6 +56,7 @@ pub struct CamoufoxLaunchResult { #[serde(alias = "profile_path")] pub profilePath: Option, pub url: Option, + pub cdp_port: Option, } #[derive(Debug)] @@ -65,6 +66,7 @@ struct CamoufoxInstance { process_id: Option, profile_path: Option, url: Option, + cdp_port: Option, } struct CamoufoxManagerInner { @@ -88,6 +90,33 @@ impl CamoufoxManager { &CAMOUFOX_LAUNCHER } + async fn find_free_port() -> Result> { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let port = listener.local_addr()?.port(); + drop(listener); + Ok(port) + } + + #[allow(dead_code)] + pub async fn get_cdp_port(&self, profile_path: &str) -> Option { + let inner = self.inner.lock().await; + let target_path = std::path::Path::new(profile_path) + .canonicalize() + .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()); + + for instance in inner.instances.values() { + if let Some(path) = &instance.profile_path { + let instance_path = std::path::Path::new(path) + .canonicalize() + .unwrap_or_else(|_| std::path::Path::new(path).to_path_buf()); + if instance_path == target_path { + return instance.cdp_port; + } + } + } + None + } + pub fn get_profiles_dir(&self) -> PathBuf { crate::app_dirs::profiles_dir() } @@ -239,6 +268,9 @@ impl CamoufoxManager { .to_string(), ]; + let cdp_port = Self::find_free_port().await?; + args.push(format!("--remote-debugging-port={cdp_port}")); + // Add URL if provided if let Some(url) = url { args.push("-new-tab".to_string()); @@ -294,6 +326,7 @@ impl CamoufoxManager { process_id, profile_path: Some(profile_path.to_string()), url: url.map(String::from), + cdp_port: Some(cdp_port), }; let launch_result = CamoufoxLaunchResult { @@ -301,6 +334,7 @@ impl CamoufoxManager { processId: process_id, profilePath: Some(profile_path.to_string()), url: url.map(String::from), + cdp_port: Some(cdp_port), }; { @@ -418,6 +452,7 @@ impl CamoufoxManager { processId: instance.process_id, profilePath: instance.profile_path.clone(), url: instance.url.clone(), + cdp_port: instance.cdp_port, })); } } @@ -428,7 +463,9 @@ impl CamoufoxManager { // If not found in in-memory instances, scan system processes // This handles the case where the app was restarted but Camoufox is still running - if let Some((pid, found_profile_path)) = self.find_camoufox_process_by_profile(&target_path) { + if let Some((pid, found_profile_path, cdp_port)) = + self.find_camoufox_process_by_profile(&target_path) + { log::info!( "Found running Camoufox process (PID: {}) for profile path via system scan", pid @@ -444,6 +481,7 @@ impl CamoufoxManager { process_id: Some(pid), profile_path: Some(found_profile_path.clone()), url: None, + cdp_port, }, ); @@ -452,6 +490,7 @@ impl CamoufoxManager { processId: Some(pid), profilePath: Some(found_profile_path), url: None, + cdp_port, })); } @@ -462,7 +501,7 @@ impl CamoufoxManager { fn find_camoufox_process_by_profile( &self, target_path: &std::path::Path, - ) -> Option<(u32, String)> { + ) -> Option<(u32, String, Option)> { use sysinfo::{ProcessRefreshKind, RefreshKind, System}; let system = System::new_with_specifics( @@ -487,6 +526,10 @@ impl CamoufoxManager { continue; } + let mut matched = false; + let mut found_profile_path = None; + let mut cdp_port: Option = None; + // Check if the command line contains our profile path for (i, arg) in cmd.iter().enumerate() { if let Some(arg_str) = arg.to_str() { @@ -498,15 +541,27 @@ impl CamoufoxManager { .unwrap_or_else(|_| std::path::Path::new(next_arg).to_path_buf()); if cmd_path == target_path { - return Some((pid.as_u32(), next_arg.to_string())); + matched = true; + found_profile_path = Some(next_arg.to_string()); } } } // Also check if the argument contains the profile path directly - if arg_str.contains(&*target_path_str) { - return Some((pid.as_u32(), target_path_str.to_string())); + if !matched && arg_str.contains(&*target_path_str) { + matched = true; + found_profile_path = Some(target_path_str.to_string()); } + + if let Some(port_val) = arg_str.strip_prefix("--remote-debugging-port=") { + cdp_port = port_val.parse().ok(); + } + } + } + + if matched { + if let Some(profile_path) = found_profile_path { + return Some((pid.as_u32(), profile_path, cdp_port)); } } } diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index b8658db..296d1fc 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -56,7 +56,7 @@ impl Downloader { } #[cfg(test)] - pub fn new_with_api_client(_api_client: ApiClient) -> Self { + pub fn new_for_test() -> Self { Self { client: Client::new(), api_client: ApiClient::instance(), @@ -67,87 +67,53 @@ impl Downloader { } } + #[cfg(test)] + pub async fn download_file( + &self, + download_url: &str, + dest_path: &Path, + filename: &str, + ) -> Result> { + let file_path = dest_path.join(filename); + + let response = self + .client + .get(download_url) + .header( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", + ) + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("Download failed with status: {}", response.status()).into()); + } + + let mut file = std::fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&file_path)?; + + let mut stream = response.bytes_stream(); + use futures_util::StreamExt; + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + io::copy(&mut chunk.as_ref(), &mut file)?; + } + + Ok(file_path) + } + /// Resolve the actual download URL for browsers that need dynamic asset resolution pub async fn resolve_download_url( &self, browser_type: BrowserType, version: &str, - download_info: &DownloadInfo, + _download_info: &DownloadInfo, ) -> Result> { match browser_type { - BrowserType::Brave => { - // For Brave, we need to find the actual platform-specific asset - let releases = self - .api_client - .fetch_brave_releases_with_caching(true) - .await?; - - // Find the release with the matching version - let release = releases - .iter() - .find(|r| { - r.tag_name == version || r.tag_name == format!("v{}", version.trim_start_matches('v')) - }) - .ok_or(format!("Brave version {version} not found"))?; - - // Get platform and architecture info - let (os, arch) = Self::get_platform_info(); - - // Find the appropriate asset based on platform and architecture - let asset_url = self - .find_brave_asset(&release.assets, &os, &arch) - .ok_or(format!( - "No compatible asset found for Brave version {version} on {os}/{arch}" - ))?; - - Ok(asset_url) - } - BrowserType::Zen => { - // For Zen, verify the asset exists and handle different naming patterns - let releases = match self.api_client.fetch_zen_releases_with_caching(true).await { - Ok(releases) => releases, - Err(e) => { - log::error!("Failed to fetch Zen releases: {e}"); - return Err(format!("Failed to fetch Zen releases from GitHub API: {e}. This might be due to GitHub API rate limiting or network issues. Please try again later.").into()); - } - }; - - let release = releases - .iter() - .find(|r| r.tag_name == version) - .ok_or_else(|| { - format!( - "Zen version {} not found. Available versions: {}", - version, - releases - .iter() - .take(5) - .map(|r| r.tag_name.as_str()) - .collect::>() - .join(", ") - ) - })?; - - // Get platform and architecture info - let (os, arch) = Self::get_platform_info(); - - // Find the appropriate asset - let asset_url = self - .find_zen_asset(&release.assets, &os, &arch) - .ok_or_else(|| { - let available_assets: Vec<&str> = - release.assets.iter().map(|a| a.name.as_str()).collect(); - format!( - "No compatible asset found for Zen version {} on {}/{}. Available assets: {}", - version, - os, - arch, - available_assets.join(", ") - ) - })?; - - Ok(asset_url) - } BrowserType::Camoufox => { // For Camoufox, verify the asset exists and find the correct download URL let releases = self @@ -209,10 +175,6 @@ impl Downloader { Ok(download_url) } - _ => { - // For other browsers, use the provided URL - Ok(download_info.url.clone()) - } } } @@ -239,110 +201,6 @@ impl Downloader { (os.to_string(), arch.to_string()) } - /// Find the appropriate Brave asset for the current platform and architecture - fn find_brave_asset( - &self, - assets: &[crate::browser::GithubAsset], - os: &str, - arch: &str, - ) -> Option { - // Brave asset naming patterns: - // Windows: BraveBrowserStandaloneNightlySetup.exe, BraveBrowserStandaloneSilentNightlySetup.exe - // macOS: Brave-Browser-Nightly-universal.dmg, Brave-Browser-Nightly-universal.pkg - // Linux: brave-browser-1.79.119-linux-arm64.zip, brave-browser-1.79.119-linux-amd64.zip - - let asset = match os { - "windows" => { - // For Windows, look for standalone setup EXE (not the auto-updater one) - assets - .iter() - .find(|asset| { - let name = asset.name.to_lowercase(); - name.contains("standalone") && name.ends_with(".exe") && !name.contains("silent") - }) - .or_else(|| { - // Fallback to any EXE if standalone not found - assets.iter().find(|asset| asset.name.ends_with(".exe")) - }) - } - "macos" => { - // For macOS, prefer universal DMG - assets - .iter() - .find(|asset| { - let name = asset.name.to_lowercase(); - name.contains("universal") && name.ends_with(".dmg") - }) - .or_else(|| { - // Fallback to any DMG - assets.iter().find(|asset| asset.name.ends_with(".dmg")) - }) - } - "linux" => { - // For Linux, be strict about architecture matching - same logic as has_compatible_brave_asset - let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" }; - - assets.iter().find(|asset| { - let name = asset.name.to_lowercase(); - name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip") - }) - } - _ => None, - }; - - asset.map(|a| a.browser_download_url.clone()) - } - - /// Find the appropriate Zen asset for the current platform and architecture - fn find_zen_asset( - &self, - assets: &[crate::browser::GithubAsset], - os: &str, - arch: &str, - ) -> Option { - // Zen asset naming patterns: - // Windows: zen.installer.exe, zen.installer-arm64.exe - // macOS: zen.macos-universal.dmg - // Linux: zen.linux-x86_64.tar.xz, zen.linux-aarch64.tar.xz, zen-x86_64.AppImage, zen-aarch64.AppImage - - let asset = match (os, arch) { - ("windows", "x64") => assets - .iter() - .find(|asset| asset.name == "zen.installer.exe"), - ("windows", "arm64") => assets - .iter() - .find(|asset| asset.name == "zen.installer-arm64.exe"), - ("macos", _) => assets - .iter() - .find(|asset| asset.name == "zen.macos-universal.dmg"), - ("linux", "x64") => { - // Prefer tar.xz, fallback to AppImage - assets - .iter() - .find(|asset| asset.name == "zen.linux-x86_64.tar.xz") - .or_else(|| { - assets - .iter() - .find(|asset| asset.name == "zen-x86_64.AppImage") - }) - } - ("linux", "arm64") => { - // Prefer tar.xz, fallback to AppImage - assets - .iter() - .find(|asset| asset.name == "zen.linux-aarch64.tar.xz") - .or_else(|| { - assets - .iter() - .find(|asset| asset.name == "zen-aarch64.AppImage") - }) - } - _ => None, - }; - - asset.map(|a| a.browser_download_url.clone()) - } - /// Find the appropriate Camoufox asset for the current platform and architecture fn find_camoufox_asset( &self, @@ -457,10 +315,6 @@ impl Downloader { .resolve_download_url(browser_type.clone(), version, download_info) .await?; - // Check if this is a twilight release for special handling - let is_twilight = - browser_type == BrowserType::Zen && version.to_lowercase().contains("twilight"); - // Determine if we have a partial file to resume let mut existing_size: u64 = 0; if let Ok(meta) = std::fs::metadata(&file_path) { @@ -555,11 +409,7 @@ impl Downloader { 0.0 }; - let initial_stage = if is_twilight { - "downloading (twilight rolling release)".to_string() - } else { - "downloading".to_string() - }; + let initial_stage = "downloading".to_string(); let progress = DownloadProgress { browser: browser_type.as_str().to_string(), @@ -621,11 +471,7 @@ impl Downloader { None }; - let stage_description = if is_twilight { - "downloading (twilight rolling release)".to_string() - } else { - "downloading".to_string() - }; + let stage_description = "downloading".to_string(); let progress = DownloadProgress { browser: browser_type.as_str().to_string(), @@ -1267,85 +1113,21 @@ pub fn configure_camoufox_search_engine( #[cfg(test)] mod tests { use super::*; - use crate::api_client::ApiClient; - use crate::browser::BrowserType; - use crate::browser_version_manager::DownloadInfo; use tempfile::TempDir; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; - async fn setup_mock_server() -> MockServer { - MockServer::start().await - } - - fn create_test_api_client(server: &MockServer) -> ApiClient { - let base_url = server.uri(); - ApiClient::new_with_base_urls( - base_url.clone(), // firefox_api_base - base_url.clone(), // firefox_dev_api_base - base_url.clone(), // github_api_base - base_url.clone(), // chromium_api_base - ) - } - #[tokio::test] - async fn test_resolve_firefox_download_url() { - let server = setup_mock_server().await; + async fn test_download_file_with_progress() { + let server = MockServer::start().await; + let downloader = Downloader::new_for_test(); - let api_client = create_test_api_client(&server); - let downloader = Downloader::new_with_api_client(api_client); - - let download_info = DownloadInfo { - url: "https://download.mozilla.org/?product=firefox-139.0&os=osx&lang=en-US".to_string(), - filename: "firefox-test.dmg".to_string(), - is_archive: true, - }; - - let result = downloader - .resolve_download_url(BrowserType::Firefox, "139.0", &download_info) - .await; - - assert!(result.is_ok()); - let url = result.unwrap(); - assert_eq!(url, download_info.url); - } - - #[tokio::test] - async fn test_resolve_chromium_download_url() { - let server = setup_mock_server().await; - let api_client = create_test_api_client(&server); - let downloader = Downloader::new_with_api_client(api_client); - - let download_info = DownloadInfo { - url: "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/1465660/chrome-mac.zip".to_string(), - filename: "chromium-test.zip".to_string(), - is_archive: true, - }; - - let result = downloader - .resolve_download_url(BrowserType::Chromium, "1465660", &download_info) - .await; - - assert!(result.is_ok()); - let url = result.unwrap(); - assert_eq!(url, download_info.url); - } - - #[tokio::test] - async fn test_download_browser_with_progress() { - let server = setup_mock_server().await; - let api_client = create_test_api_client(&server); - let downloader = Downloader::new_with_api_client(api_client); - - // Create a temporary directory for the test let temp_dir = TempDir::new().unwrap(); let dest_path = temp_dir.path(); - // Create test file content (simulating a small download) let test_content = b"This is a test file content for download simulation"; - // Mock the download endpoint Mock::given(method("GET")) .and(path("/test-download")) .respond_with( @@ -1357,85 +1139,51 @@ mod tests { .mount(&server) .await; - let download_info = DownloadInfo { - url: format!("{}/test-download", server.uri()), - filename: "test-file.dmg".to_string(), - is_archive: true, - }; - - // Create a mock app handle for testing - let app = tauri::test::mock_app(); - let app_handle = app.handle().clone(); + let download_url = format!("{}/test-download", server.uri()); let result = downloader - .download_browser( - &app_handle, - BrowserType::Firefox, - "139.0", - &download_info, - dest_path, - None, - ) + .download_file(&download_url, dest_path, "test-file.dmg") .await; assert!(result.is_ok()); let downloaded_file = result.unwrap(); assert!(downloaded_file.exists()); - // Verify file content let downloaded_content = std::fs::read(&downloaded_file).unwrap(); assert_eq!(downloaded_content, test_content); } #[tokio::test] - async fn test_download_browser_network_error() { - let server = setup_mock_server().await; - let api_client = create_test_api_client(&server); - let downloader = Downloader::new_with_api_client(api_client); + async fn test_download_file_network_error() { + let server = MockServer::start().await; + let downloader = Downloader::new_for_test(); let temp_dir = TempDir::new().unwrap(); let dest_path = temp_dir.path(); - // Mock a 404 response Mock::given(method("GET")) .and(path("/missing-file")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; - let download_info = DownloadInfo { - url: format!("{}/missing-file", server.uri()), - filename: "missing-file.dmg".to_string(), - is_archive: true, - }; - - let app = tauri::test::mock_app(); - let app_handle = app.handle().clone(); + let download_url = format!("{}/missing-file", server.uri()); let result = downloader - .download_browser( - &app_handle, - BrowserType::Firefox, - "139.0", - &download_info, - dest_path, - None, - ) + .download_file(&download_url, dest_path, "missing-file.dmg") .await; assert!(result.is_err()); } #[tokio::test] - async fn test_download_browser_chunked_response() { - let server = setup_mock_server().await; - let api_client = create_test_api_client(&server); - let downloader = Downloader::new_with_api_client(api_client); + async fn test_download_file_chunked_response() { + let server = MockServer::start().await; + let downloader = Downloader::new_for_test(); let temp_dir = TempDir::new().unwrap(); let dest_path = temp_dir.path(); - // Create larger test content to simulate chunked transfer let test_content = vec![42u8; 1024]; // 1KB of data Mock::given(method("GET")) @@ -1449,24 +1197,10 @@ mod tests { .mount(&server) .await; - let download_info = DownloadInfo { - url: format!("{}/chunked-download", server.uri()), - filename: "chunked-file.dmg".to_string(), - is_archive: true, - }; - - let app = tauri::test::mock_app(); - let app_handle = app.handle().clone(); + let download_url = format!("{}/chunked-download", server.uri()); let result = downloader - .download_browser( - &app_handle, - BrowserType::Chromium, - "1465660", - &download_info, - dest_path, - None, - ) + .download_file(&download_url, dest_path, "chunked-file.dmg") .await; assert!(result.is_ok()); diff --git a/src-tauri/src/extension_manager.rs b/src-tauri/src/extension_manager.rs index 0ab5a8a..106a1c3 100644 --- a/src-tauri/src/extension_manager.rs +++ b/src-tauri/src/extension_manager.rs @@ -829,8 +829,8 @@ impl ExtensionManager { ) -> Result<(), Box> { let group = self.get_group(group_id)?; let browser_type = match browser { - "camoufox" | "firefox" | "firefox-developer" | "zen" => "firefox", - "wayfern" | "chromium" | "brave" => "chromium", + "camoufox" => "firefox", + "wayfern" => "chromium", _ => return Err(format!("Extensions are not supported for browser '{browser}'").into()), }; @@ -871,8 +871,8 @@ impl ExtensionManager { } let browser_type = match profile.browser.as_str() { - "camoufox" | "firefox" | "firefox-developer" | "zen" => "firefox", - "wayfern" | "chromium" | "brave" => "chromium", + "camoufox" => "firefox", + "wayfern" => "chromium", _ => return Ok(Vec::new()), }; diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index aa964a0..c0d432e 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -38,12 +38,7 @@ impl Extractor { "camoufox" } else if dest_dir.to_string_lossy().contains("wayfern") { "wayfern" - } else if dest_dir.to_string_lossy().contains("firefox") { - "firefox" - } else if dest_dir.to_string_lossy().contains("zen") { - "zen" } else { - // For other browsers, assume the structure is already correct return Ok(()); }; @@ -739,57 +734,19 @@ impl Extractor { dest_dir: &Path, browser_type: BrowserType, ) -> Result> { - match browser_type { - BrowserType::Zen => { - // Zen installer EXE needs to be run to install - #[cfg(target_os = "windows")] - { - self.install_zen_windows(exe_path, dest_dir).await - } - #[cfg(not(target_os = "windows"))] - { - Err("Zen EXE installation is only supported on Windows".into()) - } - } - _ => { - // For other browsers (Firefox, TOR, etc.), the EXE is typically just copied - let exe_name = exe_path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("browser.exe"); + { + let _ = browser_type; + let exe_name = exe_path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("browser.exe"); - let dest_path = dest_dir.join(exe_name); - fs::copy(exe_path, &dest_path)?; - Ok(dest_path) - } + let dest_path = dest_dir.join(exe_name); + fs::copy(exe_path, &dest_path)?; + Ok(dest_path) } } - #[cfg(target_os = "windows")] - async fn install_zen_windows( - &self, - installer_path: &Path, - dest_dir: &Path, - ) -> Result> { - // For Zen installer, we need to run it silently - let output = Command::new(installer_path) - .args(["/S", &format!("/D={}", dest_dir.display())]) - .output()?; - - if !output.status.success() { - return Err( - format!( - "Failed to install Zen: {}", - String::from_utf8_lossy(&output.stderr) - ) - .into(), - ); - } - - // Find the installed executable - self.find_extracted_executable(dest_dir).await - } - fn flatten_single_directory_archive( &self, dest_dir: &Path, @@ -954,8 +911,6 @@ impl Extractor { "firefox.exe", "chrome.exe", "chromium.exe", - "zen.exe", - "brave.exe", "camoufox.exe", "wayfern.exe", ]; @@ -1023,8 +978,6 @@ impl Extractor { if file_name.contains("firefox") || file_name.contains("chrome") || file_name.contains("chromium") - || file_name.contains("zen") - || file_name.contains("brave") || file_name.contains("browser") || file_name.contains("camoufox") || file_name.contains("wayfern") @@ -1075,31 +1028,14 @@ impl Extractor { // Enhanced list of common browser executable names let exe_names = [ - // Firefox variants + // Firefox variants (used by Camoufox) "firefox", "firefox-bin", - "firefox-esr", - "firefox-trunk", - // Chrome/Chromium variants + // Chrome/Chromium variants (used by Wayfern) "chrome", - "google-chrome", - "google-chrome-stable", - "google-chrome-beta", - "google-chrome-unstable", "chromium", "chromium-browser", "chromium-bin", - // Zen Browser - "zen", - "zen-browser", - "zen-bin", - // Brave variants - "brave", - "brave-browser", - "brave-browser-stable", - "brave-browser-beta", - "brave-browser-dev", - "brave-bin", // Camoufox variants "camoufox", "camoufox-bin", @@ -1130,17 +1066,12 @@ impl Extractor { "firefox", "chrome", "chromium", - "brave", - "zen", "camoufox", "wayfern", ".", "./", - "firefox", "Browser", "browser", - "opt/google/chrome", - "opt/brave.com/brave", "opt/camoufox", "usr/lib/firefox", "usr/lib/chromium", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 754281f..7e04897 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1068,41 +1068,18 @@ pub fn run() { version_updater::VersionUpdater::run_background_task().await; }); - // TODO(v0.17+): Remove this migration block after a few releases. - // Migrate proxy/VPN worker configs from old proxies/ dir to new proxy_workers/ cache dir. - // Before v0.16, ephemeral worker configs (proxy_*, vpnw_*) lived alongside persistent - // StoredProxy files in proxies/. Now they live in cache_dir/proxy_workers/. + // Auto-start MCP server if it was previously enabled { - let old_dir = crate::app_dirs::proxies_dir(); - let new_dir = crate::app_dirs::proxy_workers_dir(); - if old_dir.exists() { - if let Err(e) = std::fs::create_dir_all(&new_dir) { - log::error!("Failed to create proxy_workers dir: {e}"); - } else if let Ok(entries) = std::fs::read_dir(&old_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if let Some(name) = path.file_name().and_then(|n| n.to_str()) { - if (name.starts_with("proxy_") || name.starts_with("vpnw_")) - && name.ends_with(".json") - { - let dest = new_dir.join(name); - match std::fs::rename(&path, &dest) { - Ok(()) => log::info!("Migrated worker config {name} to proxy_workers/"), - Err(e) => { - // rename fails across filesystems, fall back to copy+delete - if let Ok(content) = std::fs::read(&path) { - if std::fs::write(&dest, &content).is_ok() { - let _ = std::fs::remove_file(&path); - log::info!("Migrated worker config {name} to proxy_workers/ (copy)"); - } - } else { - log::warn!("Failed to migrate worker config {name}: {e}"); - } - } - } - } + let mcp_handle = app.handle().clone(); + let settings_mgr = settings_manager::SettingsManager::instance(); + if let Ok(settings) = settings_mgr.load_settings() { + if settings.mcp_enabled { + tauri::async_runtime::spawn(async move { + match mcp_server::McpServer::instance().start(mcp_handle).await { + Ok(port) => log::info!("MCP server auto-started on port {port}"), + Err(e) => log::warn!("Failed to auto-start MCP server: {e}"), } - } + }); } } } diff --git a/src-tauri/src/mcp_server.rs b/src-tauri/src/mcp_server.rs index c2e9d0d..aff0ff2 100644 --- a/src-tauri/src/mcp_server.rs +++ b/src-tauri/src/mcp_server.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use axum::{ body::Body, extract::State, @@ -833,6 +831,161 @@ impl McpServer { "required": ["profile_id"] }), }, + // Browser interaction tools + McpTool { + name: "navigate".to_string(), + description: "Navigate a running browser profile to a URL".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the running profile" + }, + "url": { + "type": "string", + "description": "The URL to navigate to" + } + }, + "required": ["profile_id", "url"] + }), + }, + McpTool { + name: "screenshot".to_string(), + description: "Take a screenshot of the current page in a running browser profile. Returns base64-encoded image." + .to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the running profile" + }, + "format": { + "type": "string", + "enum": ["png", "jpeg", "webp"], + "description": "Image format (default: png)" + }, + "quality": { + "type": "integer", + "description": "Image quality 0-100 for jpeg/webp (default: 80)" + }, + "full_page": { + "type": "boolean", + "description": "Capture the full scrollable page (default: false)" + } + }, + "required": ["profile_id"] + }), + }, + McpTool { + name: "evaluate_javascript".to_string(), + description: + "Execute JavaScript in the context of the current page and return the result. Works with both static and dynamically-generated content." + .to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the running profile" + }, + "expression": { + "type": "string", + "description": "JavaScript expression to evaluate" + }, + "await_promise": { + "type": "boolean", + "description": "Whether to await the result if it's a Promise (default: false)" + } + }, + "required": ["profile_id", "expression"] + }), + }, + McpTool { + name: "click_element".to_string(), + description: "Click on an element identified by a CSS selector".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the running profile" + }, + "selector": { + "type": "string", + "description": "CSS selector for the element to click" + } + }, + "required": ["profile_id", "selector"] + }), + }, + McpTool { + name: "type_text".to_string(), + description: "Focus an element by CSS selector and type text into it".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the running profile" + }, + "selector": { + "type": "string", + "description": "CSS selector for the input element" + }, + "text": { + "type": "string", + "description": "Text to type into the element" + }, + "clear_first": { + "type": "boolean", + "description": "Clear the input before typing (default: true)" + } + }, + "required": ["profile_id", "selector", "text"] + }), + }, + McpTool { + name: "get_page_content".to_string(), + description: + "Get the content of the current page. Works with both static HTML and JavaScript-rendered content." + .to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the running profile" + }, + "format": { + "type": "string", + "enum": ["html", "text"], + "description": "Content format: 'html' for full HTML, 'text' for visible text only (default: text)" + }, + "selector": { + "type": "string", + "description": "Optional CSS selector to get content of a specific element instead of the whole page" + } + }, + "required": ["profile_id"] + }), + }, + McpTool { + name: "get_page_info".to_string(), + description: "Get metadata about the current page including URL, title, and readiness state" + .to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the running profile" + } + }, + "required": ["profile_id"] + }), + }, ] } @@ -953,6 +1106,14 @@ impl McpServer { // Team lock tools "get_team_locks" => self.handle_get_team_locks().await, "get_team_lock_status" => self.handle_get_team_lock_status(&arguments).await, + // Browser interaction tools + "navigate" => self.handle_navigate(&arguments).await, + "screenshot" => self.handle_screenshot(&arguments).await, + "evaluate_javascript" => self.handle_evaluate_javascript(&arguments).await, + "click_element" => self.handle_click_element(&arguments).await, + "type_text" => self.handle_type_text(&arguments).await, + "get_page_content" => self.handle_get_page_content(&arguments).await, + "get_page_info" => self.handle_get_page_info(&arguments).await, _ => Err(McpError { code: -32602, message: format!("Unknown tool: {tool_name}"), @@ -2469,6 +2630,611 @@ impl McpServer { }] })) } + + // --- CDP utility methods for browser interaction --- + + async fn get_cdp_port_for_profile(&self, profile: &BrowserProfile) -> Result { + let profiles_dir = ProfileManager::instance().get_profiles_dir(); + let profile_path = profile.get_profile_data_path(&profiles_dir); + let profile_path_str = profile_path.to_string_lossy(); + + let port = if profile.browser == "wayfern" { + crate::wayfern_manager::WayfernManager::instance() + .get_cdp_port(&profile_path_str) + .await + } else if profile.browser == "camoufox" { + crate::camoufox_manager::CamoufoxManager::instance() + .get_cdp_port(&profile_path_str) + .await + } else { + None + }; + + port.ok_or_else(|| McpError { + code: -32000, + message: format!( + "No CDP connection available for profile '{}'. Make sure the browser is running.", + profile.name + ), + }) + } + + async fn get_cdp_ws_url(&self, port: u16) -> Result { + let url = format!("http://127.0.0.1:{port}/json"); + let client = reqwest::Client::new(); + let resp = client + .get(&url) + .timeout(std::time::Duration::from_secs(5)) + .send() + .await + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to connect to browser CDP endpoint: {e}"), + })?; + + let targets: Vec = resp.json().await.map_err(|e| McpError { + code: -32000, + message: format!("Failed to parse CDP targets: {e}"), + })?; + + targets + .iter() + .find(|t| t.get("type").and_then(|v| v.as_str()) == Some("page")) + .and_then(|t| t.get("webSocketDebuggerUrl")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .ok_or_else(|| McpError { + code: -32000, + message: "No page target found in browser".to_string(), + }) + } + + async fn send_cdp( + &self, + ws_url: &str, + method: &str, + params: serde_json::Value, + ) -> Result { + use futures_util::sink::SinkExt; + use futures_util::stream::StreamExt; + use tokio_tungstenite::connect_async; + use tokio_tungstenite::tungstenite::Message; + + let (mut ws_stream, _) = connect_async(ws_url).await.map_err(|e| McpError { + code: -32000, + message: format!("Failed to connect to CDP WebSocket: {e}"), + })?; + + let command = serde_json::json!({ + "id": 1, + "method": method, + "params": params + }); + + ws_stream + .send(Message::Text(command.to_string().into())) + .await + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to send CDP command: {e}"), + })?; + + while let Some(msg) = ws_stream.next().await { + let msg = msg.map_err(|e| McpError { + code: -32000, + message: format!("CDP WebSocket error: {e}"), + })?; + if let Message::Text(text) = msg { + let response: serde_json::Value = + serde_json::from_str(text.as_str()).map_err(|e| McpError { + code: -32000, + message: format!("Failed to parse CDP response: {e}"), + })?; + if response.get("id") == Some(&serde_json::json!(1)) { + if let Some(error) = response.get("error") { + return Err(McpError { + code: -32000, + message: format!("CDP error: {error}"), + }); + } + return Ok( + response + .get("result") + .cloned() + .unwrap_or(serde_json::json!({})), + ); + } + } + } + + Err(McpError { + code: -32000, + message: "No response received from CDP".to_string(), + }) + } + + fn get_running_profile(&self, profile_id: &str) -> Result { + let profiles = ProfileManager::instance() + .list_profiles() + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to list profiles: {e}"), + })?; + + let profile = profiles + .into_iter() + .find(|p| p.id.to_string() == profile_id) + .ok_or_else(|| McpError { + code: -32000, + message: format!("Profile not found: {profile_id}"), + })?; + + if profile.browser != "wayfern" && profile.browser != "camoufox" { + return Err(McpError { + code: -32000, + message: "MCP only supports Wayfern and Camoufox profiles".to_string(), + }); + } + + if profile.process_id.is_none() { + return Err(McpError { + code: -32000, + message: format!("Profile '{}' is not running", profile.name), + }); + } + + Ok(profile) + } + + // --- Browser interaction handlers --- + + async fn handle_navigate( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + let url = arguments + .get("url") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing url".to_string(), + })?; + + let profile = self.get_running_profile(profile_id)?; + let cdp_port = self.get_cdp_port_for_profile(&profile).await?; + let ws_url = self.get_cdp_ws_url(cdp_port).await?; + + self + .send_cdp(&ws_url, "Page.navigate", serde_json::json!({ "url": url })) + .await?; + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": format!("Navigated to {url}") + }] + })) + } + + async fn handle_screenshot( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + let format = arguments + .get("format") + .and_then(|v| v.as_str()) + .unwrap_or("png"); + let quality = arguments.get("quality").and_then(|v| v.as_i64()); + let full_page = arguments + .get("full_page") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let profile = self.get_running_profile(profile_id)?; + let cdp_port = self.get_cdp_port_for_profile(&profile).await?; + let ws_url = self.get_cdp_ws_url(cdp_port).await?; + + let mut params = serde_json::json!({ "format": format }); + + if let Some(q) = quality { + params["quality"] = serde_json::json!(q); + } + + if full_page { + let layout = self + .send_cdp(&ws_url, "Page.getLayoutMetrics", serde_json::json!({})) + .await?; + + if let Some(content_size) = layout.get("contentSize") { + params["clip"] = serde_json::json!({ + "x": 0, + "y": 0, + "width": content_size.get("width").and_then(|v| v.as_f64()).unwrap_or(1920.0), + "height": content_size.get("height").and_then(|v| v.as_f64()).unwrap_or(1080.0), + "scale": 1 + }); + params["captureBeyondViewport"] = serde_json::json!(true); + } + } + + let result = self + .send_cdp(&ws_url, "Page.captureScreenshot", params) + .await?; + + let data = result + .get("data") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + + Ok(serde_json::json!({ + "content": [{ + "type": "image", + "data": data, + "mimeType": format!("image/{format}") + }] + })) + } + + async fn handle_evaluate_javascript( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + let expression = arguments + .get("expression") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing expression".to_string(), + })?; + let await_promise = arguments + .get("await_promise") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + let profile = self.get_running_profile(profile_id)?; + let cdp_port = self.get_cdp_port_for_profile(&profile).await?; + let ws_url = self.get_cdp_ws_url(cdp_port).await?; + + let result = self + .send_cdp( + &ws_url, + "Runtime.evaluate", + serde_json::json!({ + "expression": expression, + "returnByValue": true, + "awaitPromise": await_promise, + }), + ) + .await?; + + let value = if let Some(exception) = result.get("exceptionDetails") { + let text = exception + .get("text") + .or_else(|| { + exception + .get("exception") + .and_then(|e| e.get("description")) + }) + .and_then(|v| v.as_str()) + .unwrap_or("Unknown error"); + serde_json::json!({ "error": text }) + } else if let Some(r) = result.get("result") { + let val = r.get("value").cloned().unwrap_or(serde_json::json!(null)); + serde_json::json!({ "value": val, "type": r.get("type") }) + } else { + serde_json::json!({ "value": null }) + }; + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": serde_json::to_string_pretty(&value).unwrap_or_default() + }] + })) + } + + async fn handle_click_element( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + let selector = arguments + .get("selector") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing selector".to_string(), + })?; + + let profile = self.get_running_profile(profile_id)?; + let cdp_port = self.get_cdp_port_for_profile(&profile).await?; + let ws_url = self.get_cdp_ws_url(cdp_port).await?; + + let selector_escaped = selector.replace('\\', "\\\\").replace('\'', "\\'"); + let js = format!( + r#"(() => {{ + const el = document.querySelector('{}'); + if (!el) throw new Error('Element not found: {}'); + el.scrollIntoView({{block: 'center'}}); + el.click(); + return true; + }})()"#, + selector_escaped, selector_escaped + ); + + let result = self + .send_cdp( + &ws_url, + "Runtime.evaluate", + serde_json::json!({ + "expression": js, + "returnByValue": true, + }), + ) + .await?; + + if let Some(exception) = result.get("exceptionDetails") { + let msg = exception + .get("exception") + .and_then(|e| e.get("description")) + .or_else(|| exception.get("text")) + .and_then(|v| v.as_str()) + .unwrap_or("Click failed"); + return Err(McpError { + code: -32000, + message: msg.to_string(), + }); + } + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": format!("Clicked element: {selector}") + }] + })) + } + + async fn handle_type_text( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + let selector = arguments + .get("selector") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing selector".to_string(), + })?; + let text = arguments + .get("text") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing text".to_string(), + })?; + let clear_first = arguments + .get("clear_first") + .and_then(|v| v.as_bool()) + .unwrap_or(true); + + let profile = self.get_running_profile(profile_id)?; + let cdp_port = self.get_cdp_port_for_profile(&profile).await?; + let ws_url = self.get_cdp_ws_url(cdp_port).await?; + + let selector_escaped = selector.replace('\\', "\\\\").replace('\'', "\\'"); + let focus_js = if clear_first { + format!( + r#"(() => {{ + const el = document.querySelector('{}'); + if (!el) throw new Error('Element not found: {}'); + el.scrollIntoView({{block: 'center'}}); + el.focus(); + el.value = ''; + el.dispatchEvent(new Event('input', {{bubbles: true}})); + return true; + }})()"#, + selector_escaped, selector_escaped + ) + } else { + format!( + r#"(() => {{ + const el = document.querySelector('{}'); + if (!el) throw new Error('Element not found: {}'); + el.scrollIntoView({{block: 'center'}}); + el.focus(); + return true; + }})()"#, + selector_escaped, selector_escaped + ) + }; + + let focus_result = self + .send_cdp( + &ws_url, + "Runtime.evaluate", + serde_json::json!({ + "expression": focus_js, + "returnByValue": true, + }), + ) + .await?; + + if let Some(exception) = focus_result.get("exceptionDetails") { + let msg = exception + .get("exception") + .and_then(|e| e.get("description")) + .or_else(|| exception.get("text")) + .and_then(|v| v.as_str()) + .unwrap_or("Focus failed"); + return Err(McpError { + code: -32000, + message: msg.to_string(), + }); + } + + self + .send_cdp( + &ws_url, + "Input.insertText", + serde_json::json!({ "text": text }), + ) + .await?; + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": format!("Typed text into element: {selector}") + }] + })) + } + + async fn handle_get_page_content( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + let format = arguments + .get("format") + .and_then(|v| v.as_str()) + .unwrap_or("text"); + let selector = arguments.get("selector").and_then(|v| v.as_str()); + + let profile = self.get_running_profile(profile_id)?; + let cdp_port = self.get_cdp_port_for_profile(&profile).await?; + let ws_url = self.get_cdp_ws_url(cdp_port).await?; + + let js = if let Some(sel) = selector { + let sel_escaped = sel.replace('\\', "\\\\").replace('\'', "\\'"); + if format == "html" { + format!( + r#"(() => {{ + const el = document.querySelector('{}'); + return el ? el.outerHTML : null; + }})()"#, + sel_escaped + ) + } else { + format!( + r#"(() => {{ + const el = document.querySelector('{}'); + return el ? el.innerText : null; + }})()"#, + sel_escaped + ) + } + } else if format == "html" { + "document.documentElement.outerHTML".to_string() + } else { + "document.body.innerText".to_string() + }; + + let result = self + .send_cdp( + &ws_url, + "Runtime.evaluate", + serde_json::json!({ + "expression": js, + "returnByValue": true, + }), + ) + .await?; + + let content = result + .get("result") + .and_then(|r| r.get("value")) + .and_then(|v| v.as_str()) + .unwrap_or(""); + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": content + }] + })) + } + + async fn handle_get_page_info( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + + let profile = self.get_running_profile(profile_id)?; + let cdp_port = self.get_cdp_port_for_profile(&profile).await?; + let ws_url = self.get_cdp_ws_url(cdp_port).await?; + + let result = self + .send_cdp( + &ws_url, + "Runtime.evaluate", + serde_json::json!({ + "expression": "JSON.stringify({url: location.href, title: document.title, readyState: document.readyState})", + "returnByValue": true, + }), + ) + .await?; + + let info_str = result + .get("result") + .and_then(|r| r.get("value")) + .and_then(|v| v.as_str()) + .unwrap_or("{}"); + + let info: serde_json::Value = serde_json::from_str(info_str).unwrap_or(serde_json::json!({})); + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": serde_json::to_string_pretty(&info).unwrap_or_default() + }] + })) + } } lazy_static::lazy_static! { @@ -2484,8 +3250,8 @@ mod tests { let server = McpServer::new(); let tools = server.get_tools(); - // Should have at least 34 tools (26 + 6 extension tools + 2 team lock tools) - assert!(tools.len() >= 34); + // Should have at least 41 tools (34 + 7 browser interaction tools) + assert!(tools.len() >= 41); // Check tool names let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); @@ -2532,6 +3298,14 @@ mod tests { // Team lock tools assert!(tool_names.contains(&"get_team_locks")); assert!(tool_names.contains(&"get_team_lock_status")); + // Browser interaction tools + assert!(tool_names.contains(&"navigate")); + assert!(tool_names.contains(&"screenshot")); + assert!(tool_names.contains(&"evaluate_javascript")); + assert!(tool_names.contains(&"click_element")); + assert!(tool_names.contains(&"type_text")); + assert!(tool_names.contains(&"get_page_content")); + assert!(tool_names.contains(&"get_page_info")); } #[test] diff --git a/src-tauri/src/platform_browser.rs b/src-tauri/src/platform_browser.rs index 0acdd9a..9814225 100644 --- a/src-tauri/src/platform_browser.rs +++ b/src-tauri/src/platform_browser.rs @@ -5,6 +5,7 @@ use std::process::Command; // Platform-specific modules #[cfg(target_os = "macos")] +#[allow(dead_code)] pub mod macos { use super::*; use sysinfo::{Pid, System}; @@ -468,6 +469,7 @@ end try } #[cfg(target_os = "windows")] +#[allow(dead_code)] pub mod windows { use super::*; @@ -680,6 +682,7 @@ pub mod windows { } #[cfg(target_os = "linux")] +#[allow(dead_code)] pub mod linux { use super::*; diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index 3f28619..3e4216a 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -1242,10 +1242,7 @@ impl ProfileManager { let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match - if profile.browser == "firefox" - || profile.browser == "firefox-developer" - || profile.browser == "zen" - { + if profile.browser == "camoufox" { arg == profile_data_path_str || arg == format!("-profile={profile_data_path_str}") || (arg == "-profile" @@ -1253,7 +1250,7 @@ impl ProfileManager { .iter() .any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str)) } else { - // For Chromium-based browsers, check for user-data-dir + // For Chromium-based browsers (Wayfern), check for user-data-dir arg.contains(&format!("--user-data-dir={profile_data_path_str}")) || arg == profile_data_path_str } @@ -1262,7 +1259,6 @@ impl ProfileManager { if profile_path_match { is_running = true; found_pid = Some(pid); - // Found existing browser process } } } @@ -1275,16 +1271,12 @@ impl ProfileManager { // Check if this is the right browser executable first let exe_name = process.name().to_string_lossy().to_lowercase(); let is_correct_browser = match profile.browser.as_str() { - "firefox" => { - exe_name.contains("firefox") - && !exe_name.contains("developer") - && !exe_name.contains("camoufox") + "camoufox" => exe_name.contains("camoufox") || exe_name.contains("firefox"), + "wayfern" => { + exe_name.contains("wayfern") + || exe_name.contains("chromium") + || exe_name.contains("chrome") } - "firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"), - "zen" => exe_name.contains("zen"), - "chromium" => exe_name.contains("chromium"), - "brave" => exe_name.contains("brave"), - // Camoufox is handled via CamoufoxManager, not PID-based checking _ => false, }; @@ -1300,13 +1292,6 @@ impl ProfileManager { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match if profile.browser == "camoufox" { - // Camoufox uses user_data_dir like Chromium browsers - arg.contains(&format!("--user-data-dir={profile_data_path_str}")) - || arg == profile_data_path_str - } else if profile.browser == "firefox" - || profile.browser == "firefox-developer" - || profile.browser == "zen" - { arg == profile_data_path_str || arg == format!("-profile={profile_data_path_str}") || (arg == "-profile" @@ -1314,7 +1299,7 @@ impl ProfileManager { .iter() .any(|s2| s2.to_str().unwrap_or("") == profile_data_path_str)) } else { - // For Chromium-based browsers, check for user-data-dir + // For Chromium-based browsers (Wayfern), check for user-data-dir arg.contains(&format!("--user-data-dir={profile_data_path_str}")) || arg == profile_data_path_str } diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 7e14782..9555ea8 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -4,22 +4,38 @@ use std::collections::HashSet; use std::fs::{self, create_dir_all}; use std::path::{Path, PathBuf}; -use crate::browser::BrowserType; +use crate::camoufox_manager::CamoufoxConfig; use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; +use crate::profile::types::{get_host_os, BrowserProfile, SyncMode}; use crate::profile::ProfileManager; +use crate::proxy_manager::PROXY_MANAGER; +use crate::wayfern_manager::WayfernConfig; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DetectedProfile { pub browser: String, + pub mapped_browser: String, pub name: String, pub path: String, pub description: String, } +fn map_browser_type(browser: &str) -> &str { + match browser { + "firefox" | "firefox-developer" | "zen" => "camoufox", + "chromium" | "brave" => "wayfern", + "camoufox" => "camoufox", + "wayfern" => "wayfern", + _ => "wayfern", + } +} + pub struct ProfileImporter { base_dirs: BaseDirs, downloaded_browsers_registry: &'static DownloadedBrowsersRegistry, profile_manager: &'static ProfileManager, + camoufox_manager: &'static crate::camoufox_manager::CamoufoxManager, + wayfern_manager: &'static crate::wayfern_manager::WayfernManager, } impl ProfileImporter { @@ -28,6 +44,8 @@ impl ProfileImporter { base_dirs: BaseDirs::new().expect("Failed to get base directories"), downloaded_browsers_registry: DownloadedBrowsersRegistry::instance(), profile_manager: ProfileManager::instance(), + camoufox_manager: crate::camoufox_manager::CamoufoxManager::instance(), + wayfern_manager: crate::wayfern_manager::WayfernManager::instance(), } } @@ -35,31 +53,18 @@ impl ProfileImporter { &PROFILE_IMPORTER } - /// Detect existing browser profiles on the system pub fn detect_existing_profiles( &self, ) -> Result, Box> { let mut detected_profiles = Vec::new(); - // Detect Firefox profiles detected_profiles.extend(self.detect_firefox_profiles()?); - - // Detect Chrome profiles detected_profiles.extend(self.detect_chrome_profiles()?); - - // Detect Brave profiles detected_profiles.extend(self.detect_brave_profiles()?); - - // Detect Firefox Developer Edition profiles detected_profiles.extend(self.detect_firefox_developer_profiles()?); - - // Detect Chromium profiles detected_profiles.extend(self.detect_chromium_profiles()?); - - // Detect Zen Browser profiles detected_profiles.extend(self.detect_zen_browser_profiles()?); - // Remove duplicates based on path let mut seen_paths = HashSet::new(); let unique_profiles: Vec = detected_profiles .into_iter() @@ -69,7 +74,6 @@ impl ProfileImporter { Ok(unique_profiles) } - /// Detect Firefox profiles fn detect_firefox_profiles(&self) -> Result, Box> { let mut profiles = Vec::new(); @@ -84,12 +88,10 @@ impl ProfileImporter { #[cfg(target_os = "windows")] { - // Primary location in AppData\Roaming let app_data = self.base_dirs.data_dir(); let firefox_dir = app_data.join("Mozilla/Firefox/Profiles"); profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?); - // Also check AppData\Local for portable installations let local_app_data = self.base_dirs.data_local_dir(); let firefox_local_dir = local_app_data.join("Mozilla/Firefox/Profiles"); if firefox_local_dir.exists() { @@ -106,7 +108,6 @@ impl ProfileImporter { Ok(profiles) } - /// Detect Firefox Developer Edition profiles fn detect_firefox_developer_profiles( &self, ) -> Result, Box> { @@ -114,13 +115,11 @@ impl ProfileImporter { #[cfg(target_os = "macos")] { - // Firefox Developer Edition on macOS uses separate profile directories let firefox_dev_alt_dir = self .base_dirs .home_dir() .join("Library/Application Support/Firefox Developer Edition/Profiles"); - // Only scan the dedicated dev edition directory if it exists, otherwise skip to avoid duplicates if firefox_dev_alt_dir.exists() { profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_alt_dir, "firefox-developer")?); } @@ -129,7 +128,6 @@ impl ProfileImporter { #[cfg(target_os = "windows")] { let app_data = self.base_dirs.data_dir(); - // Firefox Developer Edition on Windows typically uses separate directories let firefox_dev_dir = app_data.join("Mozilla/Firefox Developer Edition/Profiles"); if firefox_dev_dir.exists() { profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_dir, "firefox-developer")?); @@ -138,7 +136,6 @@ impl ProfileImporter { #[cfg(target_os = "linux")] { - // Firefox Developer Edition on Linux uses separate directories let firefox_dev_dir = self .base_dirs .home_dir() @@ -151,7 +148,6 @@ impl ProfileImporter { Ok(profiles) } - /// Detect Chrome profiles fn detect_chrome_profiles(&self) -> Result, Box> { let mut profiles = Vec::new(); @@ -180,7 +176,6 @@ impl ProfileImporter { Ok(profiles) } - /// Detect Chromium profiles fn detect_chromium_profiles(&self) -> Result, Box> { let mut profiles = Vec::new(); @@ -209,7 +204,6 @@ impl ProfileImporter { Ok(profiles) } - /// Detect Brave profiles fn detect_brave_profiles(&self) -> Result, Box> { let mut profiles = Vec::new(); @@ -241,7 +235,6 @@ impl ProfileImporter { Ok(profiles) } - /// Detect Zen Browser profiles fn detect_zen_browser_profiles( &self, ) -> Result, Box> { @@ -272,7 +265,6 @@ impl ProfileImporter { Ok(profiles) } - /// Scan Firefox-style profiles directory fn scan_firefox_profiles_dir( &self, profiles_dir: &Path, @@ -284,7 +276,6 @@ impl ProfileImporter { return Ok(profiles); } - // Read profiles.ini file if it exists let profiles_ini = profiles_dir .parent() .unwrap_or(profiles_dir) @@ -295,7 +286,6 @@ impl ProfileImporter { } } - // Also scan directory for any profile folders not in profiles.ini if let Ok(entries) = fs::read_dir(profiles_dir) { for entry in entries.flatten() { let path = entry.path(); @@ -307,11 +297,11 @@ impl ProfileImporter { .and_then(|n| n.to_str()) .unwrap_or("Unknown Profile"); - // Check if this profile was already found in profiles.ini let already_added = profiles.iter().any(|p| p.path == path.to_string_lossy()); if !already_added { profiles.push(DetectedProfile { browser: browser_type.to_string(), + mapped_browser: map_browser_type(browser_type).to_string(), name: format!( "{} Profile - {}", self.get_browser_display_name(browser_type), @@ -329,7 +319,6 @@ impl ProfileImporter { Ok(profiles) } - /// Parse Firefox profiles.ini file fn parse_firefox_profiles_ini( &self, content: &str, @@ -346,7 +335,6 @@ impl ProfileImporter { let line = line.trim(); if line.starts_with('[') && line.ends_with(']') { - // Save previous profile if complete if !current_section.is_empty() && current_section.starts_with("Profile") && !profile_path.is_empty() @@ -370,6 +358,7 @@ impl ProfileImporter { profiles.push(DetectedProfile { browser: browser_type.to_string(), + mapped_browser: map_browser_type(browser_type).to_string(), name: display_name, path: full_path.to_string_lossy().to_string(), description: format!("Profile: {profile_name}"), @@ -377,7 +366,6 @@ impl ProfileImporter { } } - // Start new section current_section = line[1..line.len() - 1].to_string(); profile_name.clear(); profile_path.clear(); @@ -398,7 +386,6 @@ impl ProfileImporter { } } - // Handle last profile if !current_section.is_empty() && current_section.starts_with("Profile") && !profile_path.is_empty() @@ -422,6 +409,7 @@ impl ProfileImporter { profiles.push(DetectedProfile { browser: browser_type.to_string(), + mapped_browser: map_browser_type(browser_type).to_string(), name: display_name, path: full_path.to_string_lossy().to_string(), description: format!("Profile: {profile_name}"), @@ -432,7 +420,6 @@ impl ProfileImporter { Ok(profiles) } - /// Scan Chrome-style profiles directory fn scan_chrome_profiles_dir( &self, browser_dir: &Path, @@ -444,11 +431,11 @@ impl ProfileImporter { return Ok(profiles); } - // Check for Default profile let default_profile = browser_dir.join("Default"); if default_profile.exists() && default_profile.join("Preferences").exists() { profiles.push(DetectedProfile { browser: browser_type.to_string(), + mapped_browser: map_browser_type(browser_type).to_string(), name: format!( "{} - Default Profile", self.get_browser_display_name(browser_type) @@ -458,7 +445,6 @@ impl ProfileImporter { }); } - // Check for Profile X directories if let Ok(entries) = fs::read_dir(browser_dir) { for entry in entries.flatten() { let path = entry.path(); @@ -466,9 +452,10 @@ impl ProfileImporter { let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); if dir_name.starts_with("Profile ") && path.join("Preferences").exists() { - let profile_number = &dir_name[8..]; // Remove "Profile " prefix + let profile_number = &dir_name[8..]; profiles.push(DetectedProfile { browser: browser_type.to_string(), + mapped_browser: map_browser_type(browser_type).to_string(), name: format!( "{} - Profile {}", self.get_browser_display_name(browser_type), @@ -485,7 +472,6 @@ impl ProfileImporter { Ok(profiles) } - /// Get browser display name fn get_browser_display_name(&self, browser_type: &str) -> &str { match browser_type { "firefox" => "Firefox", @@ -493,28 +479,36 @@ impl ProfileImporter { "chromium" => "Chrome/Chromium", "brave" => "Brave", "zen" => "Zen Browser", + "camoufox" => "Camoufox", + "wayfern" => "Wayfern", _ => "Unknown Browser", } } - /// Import a profile from an existing browser profile - pub fn import_profile( + #[allow(clippy::too_many_arguments)] + pub async fn import_profile( &self, + app_handle: &tauri::AppHandle, source_path: &str, browser_type: &str, new_profile_name: &str, + proxy_id: Option, + camoufox_config: Option, + wayfern_config: Option, ) -> Result<(), Box> { - // Validate that source path exists let source_path = Path::new(source_path); if !source_path.exists() { return Err("Source profile path does not exist".into()); } - // Validate browser type - let _browser_type = BrowserType::from_str(browser_type) - .map_err(|_| format!("Invalid browser type: {browser_type}"))?; + let mapped = map_browser_type(browser_type); + + if let Some(ref pid) = proxy_id { + if PROXY_MANAGER.is_cloud_or_derived(pid) || pid == crate::proxy_manager::CLOUD_PROXY_ID { + crate::cloud_auth::CLOUD_AUTH.sync_cloud_proxy().await; + } + } - // Check if a profile with this name already exists let existing_profiles = self.profile_manager.list_profiles()?; if existing_profiles .iter() @@ -523,7 +517,6 @@ impl ProfileImporter { return Err(format!("Profile with name '{new_profile_name}' already exists").into()); } - // Generate UUID for new profile and create the directory structure let profile_id = uuid::Uuid::new_v4(); let profiles_dir = self.profile_manager.get_profiles_dir(); let new_profile_uuid_dir = profiles_dir.join(profile_id.to_string()); @@ -532,32 +525,227 @@ impl ProfileImporter { create_dir_all(&new_profile_uuid_dir)?; create_dir_all(&new_profile_data_dir)?; - // Copy all files from source to destination profile subdirectory Self::copy_directory_recursive(source_path, &new_profile_data_dir)?; - // Create the profile metadata without overwriting the imported data - // We need to find a suitable version for this browser type - let available_versions = self.get_default_version_for_browser(browser_type)?; + let version = self.get_default_version_for_browser(mapped)?; - let profile = crate::profile::BrowserProfile { + let final_camoufox_config = if mapped == "camoufox" { + let mut config = camoufox_config.unwrap_or_default(); + + if config.executable_path.is_none() { + let mut browser_dir = self.profile_manager.get_binaries_dir(); + browser_dir.push(mapped); + browser_dir.push(&version); + + #[cfg(target_os = "macos")] + let binary_path = browser_dir + .join("Camoufox.app") + .join("Contents") + .join("MacOS") + .join("camoufox"); + + #[cfg(target_os = "windows")] + let binary_path = browser_dir.join("camoufox.exe"); + + #[cfg(target_os = "linux")] + let binary_path = browser_dir.join("camoufox"); + + config.executable_path = Some(binary_path.to_string_lossy().to_string()); + } + + if let Some(ref proxy_id_val) = proxy_id { + if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_val) { + let proxy_url = if let (Some(username), Some(password)) = + (&proxy_settings.username, &proxy_settings.password) + { + format!( + "{}://{}:{}@{}:{}", + proxy_settings.proxy_type.to_lowercase(), + username, + password, + proxy_settings.host, + proxy_settings.port + ) + } else { + format!( + "{}://{}:{}", + proxy_settings.proxy_type.to_lowercase(), + proxy_settings.host, + proxy_settings.port + ) + }; + config.proxy = Some(proxy_url); + } + } + + if config.fingerprint.is_none() { + let temp_profile = BrowserProfile { + id: uuid::Uuid::new_v4(), + name: new_profile_name.to_string(), + browser: mapped.to_string(), + version: version.clone(), + proxy_id: proxy_id.clone(), + vpn_id: None, + process_id: None, + last_launch: None, + release_type: "stable".to_string(), + camoufox_config: None, + wayfern_config: None, + group_id: None, + tags: Vec::new(), + note: None, + sync_mode: SyncMode::Disabled, + encryption_salt: None, + last_sync: None, + host_os: None, + ephemeral: false, + extension_group_id: None, + proxy_bypass_rules: Vec::new(), + created_by_id: None, + created_by_email: None, + }; + + match self + .camoufox_manager + .generate_fingerprint_config(app_handle, &temp_profile, &config) + .await + { + Ok(fp) => config.fingerprint = Some(fp), + Err(e) => { + return Err( + format!( + "Failed to generate fingerprint for imported profile '{new_profile_name}': {e}" + ) + .into(), + ); + } + } + } + + config.proxy = None; + Some(config) + } else { + None + }; + + let final_wayfern_config = if mapped == "wayfern" { + let mut config = wayfern_config.unwrap_or_default(); + + if config.executable_path.is_none() { + let mut browser_dir = self.profile_manager.get_binaries_dir(); + browser_dir.push(mapped); + browser_dir.push(&version); + + #[cfg(target_os = "macos")] + let binary_path = browser_dir + .join("Chromium.app") + .join("Contents") + .join("MacOS") + .join("Chromium"); + + #[cfg(target_os = "windows")] + let binary_path = browser_dir.join("chrome.exe"); + + #[cfg(target_os = "linux")] + let binary_path = browser_dir.join("chrome"); + + config.executable_path = Some(binary_path.to_string_lossy().to_string()); + } + + if let Some(ref proxy_id_val) = proxy_id { + if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_val) { + let proxy_url = if let (Some(username), Some(password)) = + (&proxy_settings.username, &proxy_settings.password) + { + format!( + "{}://{}:{}@{}:{}", + proxy_settings.proxy_type.to_lowercase(), + username, + password, + proxy_settings.host, + proxy_settings.port + ) + } else { + format!( + "{}://{}:{}", + proxy_settings.proxy_type.to_lowercase(), + proxy_settings.host, + proxy_settings.port + ) + }; + config.proxy = Some(proxy_url); + } + } + + if config.fingerprint.is_none() { + let temp_profile = BrowserProfile { + id: uuid::Uuid::new_v4(), + name: new_profile_name.to_string(), + browser: mapped.to_string(), + version: version.clone(), + proxy_id: proxy_id.clone(), + vpn_id: None, + process_id: None, + last_launch: None, + release_type: "stable".to_string(), + camoufox_config: None, + wayfern_config: None, + group_id: None, + tags: Vec::new(), + note: None, + sync_mode: SyncMode::Disabled, + encryption_salt: None, + last_sync: None, + host_os: None, + ephemeral: false, + extension_group_id: None, + proxy_bypass_rules: Vec::new(), + created_by_id: None, + created_by_email: None, + }; + + match self + .wayfern_manager + .generate_fingerprint_config(app_handle, &temp_profile, &config) + .await + { + Ok(fp) => config.fingerprint = Some(fp), + Err(e) => { + return Err( + format!( + "Failed to generate fingerprint for imported profile '{new_profile_name}': {e}" + ) + .into(), + ); + } + } + } + + config.proxy = None; + Some(config) + } else { + None + }; + + let profile = BrowserProfile { id: profile_id, name: new_profile_name.to_string(), - browser: browser_type.to_string(), - version: available_versions, - proxy_id: None, + browser: mapped.to_string(), + version, + proxy_id, vpn_id: None, process_id: None, last_launch: None, release_type: "stable".to_string(), - camoufox_config: None, - wayfern_config: None, + camoufox_config: final_camoufox_config, + wayfern_config: final_wayfern_config, group_id: None, tags: Vec::new(), note: None, - sync_mode: crate::profile::types::SyncMode::Disabled, + sync_mode: SyncMode::Disabled, encryption_salt: None, last_sync: None, - host_os: Some(crate::profile::types::get_host_os()), + host_os: Some(get_host_os()), ephemeral: false, extension_group_id: None, proxy_bypass_rules: Vec::new(), @@ -565,7 +753,6 @@ impl ProfileImporter { created_by_email: None, }; - // Save the profile metadata self.profile_manager.save_profile(&profile)?; log::info!( @@ -577,12 +764,10 @@ impl ProfileImporter { Ok(()) } - /// Get a default version for a browser type fn get_default_version_for_browser( &self, browser_type: &str, ) -> Result> { - // Check if any version of the browser is downloaded let downloaded_versions = self .downloaded_browsers_registry .get_downloaded_versions(browser_type); @@ -591,15 +776,16 @@ impl ProfileImporter { return Ok(version.clone()); } - // If no downloaded versions found, return an error - Err(format!( - "No downloaded versions found for browser '{}'. Please download a version of {} first before importing profiles.", - browser_type, - self.get_browser_display_name(browser_type) - ).into()) + Err( + format!( + "No downloaded versions found for browser '{}'. Please download a version of {} first before importing profiles.", + browser_type, + self.get_browser_display_name(browser_type) + ) + .into(), + ) } - /// Recursively copy directory contents pub fn copy_directory_recursive( source: &Path, destination: &Path, @@ -624,7 +810,6 @@ impl ProfileImporter { } } -// Tauri commands #[tauri::command] pub async fn detect_existing_profiles() -> Result, String> { let importer = ProfileImporter::instance(); @@ -635,17 +820,41 @@ pub async fn detect_existing_profiles() -> Result, String> #[tauri::command] pub async fn import_browser_profile( + app_handle: tauri::AppHandle, source_path: String, browser_type: String, new_profile_name: String, + proxy_id: Option, + camoufox_config: Option, + wayfern_config: Option, ) -> Result<(), String> { + let fingerprint_os = camoufox_config + .as_ref() + .and_then(|c| c.os.as_deref()) + .or_else(|| wayfern_config.as_ref().and_then(|c| c.os.as_deref())); + + if !crate::cloud_auth::CLOUD_AUTH + .is_fingerprint_os_allowed(fingerprint_os) + .await + { + return Err("Fingerprint OS spoofing requires an active Pro subscription".to_string()); + } + let importer = ProfileImporter::instance(); importer - .import_profile(&source_path, &browser_type, &new_profile_name) + .import_profile( + &app_handle, + &source_path, + &browser_type, + &new_profile_name, + proxy_id, + camoufox_config, + wayfern_config, + ) + .await .map_err(|e| format!("Failed to import profile: {e}")) } -// Global singleton instance lazy_static::lazy_static! { static ref PROFILE_IMPORTER: ProfileImporter = ProfileImporter::new(); } @@ -658,10 +867,7 @@ mod tests { fn create_test_profile_importer() -> (ProfileImporter, TempDir) { let temp_dir = TempDir::new().expect("Failed to create temp directory"); - - // Set up a temporary home directory for testing env::set_var("HOME", temp_dir.path()); - let importer = ProfileImporter::new(); (importer, temp_dir) } @@ -669,7 +875,6 @@ mod tests { #[test] fn test_profile_importer_creation() { let (_importer, _temp_dir) = create_test_profile_importer(); - // Test passes if no panic occurs } #[test] @@ -693,19 +898,25 @@ mod tests { ); } + #[test] + fn test_map_browser_type() { + assert_eq!(map_browser_type("firefox"), "camoufox"); + assert_eq!(map_browser_type("firefox-developer"), "camoufox"); + assert_eq!(map_browser_type("zen"), "camoufox"); + assert_eq!(map_browser_type("chromium"), "wayfern"); + assert_eq!(map_browser_type("brave"), "wayfern"); + assert_eq!(map_browser_type("camoufox"), "camoufox"); + assert_eq!(map_browser_type("wayfern"), "wayfern"); + assert_eq!(map_browser_type("something_else"), "wayfern"); + } + #[test] fn test_detect_existing_profiles_no_panic() { let (importer, _temp_dir) = create_test_profile_importer(); - // This should not panic even if no browser profiles exist let result = importer.detect_existing_profiles(); assert!(result.is_ok(), "detect_existing_profiles should not fail"); - let _profiles = result.unwrap(); - // We can't assert specific profiles since they depend on the system - // but we can verify the result is a valid Vec - // We can't assert specific profiles since they depend on the system - // but we can verify the result is a valid Vec (length check is always true for Vec, but shows intent) } #[test] @@ -764,12 +975,10 @@ mod tests { fn test_parse_firefox_profiles_ini_valid() { let (importer, temp_dir) = create_test_profile_importer(); - // Create a mock profile directory let profiles_dir = temp_dir.path().join("profiles"); let profile_dir = profiles_dir.join("test.profile"); fs::create_dir_all(&profile_dir).expect("Should create profile directory"); - // Create a prefs.js file to make it look like a valid profile let prefs_file = profile_dir.join("prefs.js"); fs::write(&prefs_file, "// Firefox preferences").expect("Should create prefs.js"); @@ -788,31 +997,27 @@ Path=test.profile assert_eq!(profiles.len(), 1, "Should find one profile"); assert_eq!(profiles[0].name, "Firefox - Test Profile"); assert_eq!(profiles[0].browser, "firefox"); + assert_eq!(profiles[0].mapped_browser, "camoufox"); } #[test] fn test_copy_directory_recursive() { let temp_dir = TempDir::new().expect("Failed to create temp directory"); - // Create source directory structure let source_dir = temp_dir.path().join("source"); let source_subdir = source_dir.join("subdir"); fs::create_dir_all(&source_subdir).expect("Should create source directories"); - // Create some test files let source_file1 = source_dir.join("file1.txt"); let source_file2 = source_subdir.join("file2.txt"); fs::write(&source_file1, "content1").expect("Should create file1"); fs::write(&source_file2, "content2").expect("Should create file2"); - // Create destination directory let dest_dir = temp_dir.path().join("dest"); - // Copy recursively let result = ProfileImporter::copy_directory_recursive(&source_dir, &dest_dir); assert!(result.is_ok(), "Should copy directory successfully"); - // Verify files were copied let dest_file1 = dest_dir.join("file1.txt"); let dest_file2 = dest_dir.join("subdir").join("file2.txt"); @@ -830,8 +1035,7 @@ Path=test.profile fn test_get_default_version_for_browser_no_versions() { let (importer, _temp_dir) = create_test_profile_importer(); - // This should fail since no versions are downloaded in test environment - let result = importer.get_default_version_for_browser("firefox"); + let result = importer.get_default_version_for_browser("camoufox"); assert!( result.is_err(), "Should fail when no versions are available" diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 78e6c45..0ca9ee3 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -734,11 +734,17 @@ pub async fn save_app_settings( .await .map_err(|e| format!("Failed to store API token: {e}"))?; } else { - let token = manager - .generate_api_token(&app_handle) - .await - .map_err(|e| format!("Failed to generate API token: {e}"))?; - settings.api_token = Some(token); + // Check if a token already exists on disk before generating a new one + let existing = manager.get_api_token(&app_handle).await.ok().flatten(); + if let Some(t) = existing { + settings.api_token = Some(t); + } else { + let token = manager + .generate_api_token(&app_handle) + .await + .map_err(|e| format!("Failed to generate API token: {e}"))?; + settings.api_token = Some(token); + } } } @@ -758,11 +764,17 @@ pub async fn save_app_settings( .await .map_err(|e| format!("Failed to store MCP token: {e}"))?; } else { - let token = manager - .generate_mcp_token(&app_handle) - .await - .map_err(|e| format!("Failed to generate MCP token: {e}"))?; - settings.mcp_token = Some(token); + // Check if a token already exists on disk before generating a new one + let existing = manager.get_mcp_token(&app_handle).await.ok().flatten(); + if let Some(t) = existing { + settings.mcp_token = Some(t); + } else { + let token = manager + .generate_mcp_token(&app_handle) + .await + .map_err(|e| format!("Failed to generate MCP token: {e}"))?; + settings.mcp_token = Some(token); + } } } diff --git a/src-tauri/src/version_updater.rs b/src-tauri/src/version_updater.rs index a42ad48..61ae265 100644 --- a/src-tauri/src/version_updater.rs +++ b/src-tauri/src/version_updater.rs @@ -143,12 +143,7 @@ impl VersionUpdater { pub async fn check_and_run_startup_update( &self, ) -> Result<(), Box> { - // Only run if an update is actually needed - if !Self::should_run_background_update() { - log::debug!("No startup version update needed"); - return Ok(()); - } - + // Always check for updates on launch if let Some(ref app_handle) = self.app_handle { log::info!("Running startup version update..."); diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index 215c1ee..a47dc43 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -783,6 +783,25 @@ impl WayfernManager { Ok(()) } + pub async fn get_cdp_port(&self, profile_path: &str) -> Option { + let inner = self.inner.lock().await; + let target_path = std::path::Path::new(profile_path) + .canonicalize() + .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()); + + for instance in inner.instances.values() { + if let Some(path) = &instance.profile_path { + let instance_path = std::path::Path::new(path) + .canonicalize() + .unwrap_or_else(|_| std::path::Path::new(path).to_path_buf()); + if instance_path == target_path { + return instance.cdp_port; + } + } + } + None + } + pub async fn find_wayfern_by_profile(&self, profile_path: &str) -> Option { use sysinfo::{ProcessRefreshKind, RefreshKind, System}; diff --git a/src/app/page.tsx b/src/app/page.tsx index a5fa4b8..7a10936 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -57,14 +57,7 @@ import type { WayfernConfig, } from "@/types"; -type BrowserTypeString = - | "firefox" - | "firefox-developer" - | "chromium" - | "brave" - | "zen" - | "camoufox" - | "wayfern"; +type BrowserTypeString = "camoufox" | "wayfern"; interface PendingUrl { id: string; @@ -943,37 +936,6 @@ export default function Home() { profiles.length, ]); - // Show deprecation warning for unsupported profiles (with names) - useEffect(() => { - if (profiles.length === 0) return; - - const deprecatedProfiles = profiles.filter( - (p) => p.release_type === "nightly" && p.browser !== "firefox-developer", - ); - - if (deprecatedProfiles.length > 0) { - const deprecatedNames = deprecatedProfiles.map((p) => p.name).join(", "); - - // Use a stable id to avoid duplicate toasts on re-renders - showToast({ - id: "deprecated-profiles-warning", - type: "error", - title: "Some profiles will be deprecated soon", - description: `The following profiles will be deprecated soon: ${deprecatedNames}. Nightly profiles (except Firefox Developers Edition) will be removed in upcoming versions. Please check GitHub for migration instructions.`, - duration: 15000, - action: { - label: "Learn more", - onClick: () => { - const event = new CustomEvent("url-open-request", { - detail: "https://github.com/zhom/donutbrowser/discussions/66", - }); - window.dispatchEvent(event); - }, - }, - }); - } - }, [profiles]); - // Show warning for non-wayfern/camoufox profiles (support ending March 15, 2026) useEffect(() => { if (profiles.length === 0) return; @@ -1163,6 +1125,7 @@ export default function Home() { onClose={() => { setImportProfileDialogOpen(false); }} + crossOsUnlocked={crossOsUnlocked} /> - {/* Commercial Trial Modal - shown once when trial expires */} + {/* Commercial Trial Modal - shown once when trial expires (skip for paid users) */} diff --git a/src/components/cookie-management-dialog.tsx b/src/components/cookie-management-dialog.tsx index e0cdfb6..65c8649 100644 --- a/src/components/cookie-management-dialog.tsx +++ b/src/components/cookie-management-dialog.tsx @@ -462,8 +462,8 @@ export function CookieManagementDialog({ {importResult && (
-
-
+
+
Successfully imported {importResult.cookies_imported}{" "} cookies ({importResult.cookies_replaced} replaced)
diff --git a/src/components/create-group-dialog.tsx b/src/components/create-group-dialog.tsx index 41db785..21d3c21 100644 --- a/src/components/create-group-dialog.tsx +++ b/src/components/create-group-dialog.tsx @@ -91,7 +91,7 @@ export function CreateGroupDialog({
{error && ( -
+
{error}
)} diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index a9b707a..fcdec2d 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -67,14 +67,7 @@ const getCurrentOS = (): CamoufoxOS => { import { RippleButton } from "./ui/ripple"; -type BrowserTypeString = - | "firefox" - | "firefox-developer" - | "chromium" - | "brave" - | "zen" - | "camoufox" - | "wayfern"; +type BrowserTypeString = "camoufox" | "wayfern"; interface CreateProfileDialogProps { isOpen: boolean; @@ -103,24 +96,12 @@ interface BrowserOption { const browserOptions: BrowserOption[] = [ { - value: "firefox", - label: "Firefox", + value: "camoufox", + label: "Camoufox", }, { - value: "firefox-developer", - label: "Firefox Developer Edition", - }, - { - value: "chromium", - label: "Chromium", - }, - { - value: "brave", - label: "Brave", - }, - { - value: "zen", - label: "Zen Browser", + value: "wayfern", + label: "Wayfern", }, ]; @@ -254,23 +235,9 @@ export function CreateProfileDialog({ // Only update state if this browser is still the one we're loading if (loadingBrowserRef.current === browser) { - // Filter to enforce stable-only creation, except Firefox Developer (nightly-only) - if (browser === "camoufox" || browser === "wayfern") { - const filtered: BrowserReleaseTypes = {}; - if (rawReleaseTypes.stable) - filtered.stable = rawReleaseTypes.stable; - setReleaseTypes(filtered); - } else if (browser === "firefox-developer") { - const filtered: BrowserReleaseTypes = {}; - if (rawReleaseTypes.nightly) - filtered.nightly = rawReleaseTypes.nightly; - setReleaseTypes(filtered); - } else { - const filtered: BrowserReleaseTypes = {}; - if (rawReleaseTypes.stable) - filtered.stable = rawReleaseTypes.stable; - setReleaseTypes(filtered); - } + const filtered: BrowserReleaseTypes = {}; + if (rawReleaseTypes.stable) filtered.stable = rawReleaseTypes.stable; + setReleaseTypes(filtered); setReleaseTypesError(null); } } catch (error) { @@ -282,11 +249,7 @@ export function CreateProfileDialog({ if (loadingBrowserRef.current === browser && downloaded.length > 0) { const latest = downloaded[0]; const fallback: BrowserReleaseTypes = {}; - if (browser === "firefox-developer") { - fallback.nightly = latest; - } else { - fallback.stable = latest; - } + fallback.stable = latest; setReleaseTypes(fallback); setReleaseTypesError(null); } else if (loadingBrowserRef.current === browser) { @@ -351,17 +314,9 @@ export function CreateProfileDialog({ // Helper function to get the best available version respecting rules const getBestAvailableVersion = useCallback( - (browserType?: string) => { + (_browserType?: string) => { if (!releaseTypes) return null; - // Firefox Developer Edition: nightly-only - if (browserType === "firefox-developer" && releaseTypes.nightly) { - return { - version: releaseTypes.nightly, - releaseType: "nightly" as const, - }; - } - // All others: stable-only if (releaseTypes.stable) { return { version: releaseTypes.stable, releaseType: "stable" as const }; } @@ -379,11 +334,9 @@ export function CreateProfileDialog({ const browserDownloaded = downloadedVersionsMap[browserType ?? ""] ?? []; if (browserDownloaded.length > 0) { const fallbackVersion = browserDownloaded[0]; - const releaseType = - browserType === "firefox-developer" ? "nightly" : "stable"; return { version: fallbackVersion, - releaseType: releaseType as "stable" | "nightly", + releaseType: "stable" as const, }; } return null; @@ -772,8 +725,8 @@ export function CreateProfileDialog({ {!isLoadingReleaseTypes && !releaseTypesError && !getBestAvailableVersion("wayfern") && ( -
-

+

+

Wayfern is not available on your platform yet.

@@ -874,8 +827,8 @@ export function CreateProfileDialog({ {!isLoadingReleaseTypes && !releaseTypesError && !getBestAvailableVersion("camoufox") && ( -
-

+

+

Camoufox is not available on your platform yet.

@@ -933,7 +886,7 @@ export function CreateProfileDialog({ )} {crossOsUnlocked && ( - + {t("createProfile.camoufoxWarning")} diff --git a/src/components/custom-toast.tsx b/src/components/custom-toast.tsx index d0c10ad..cafa7a2 100644 --- a/src/components/custom-toast.tsx +++ b/src/components/custom-toast.tsx @@ -347,7 +347,7 @@ export function UnifiedToast(props: ToastProps) { <> {stage === "extracting" && (

- Extracting browser files... + Extracting browser files... Please do not close the app.

)} {stage === "verifying" && ( diff --git a/src/components/data-table-action-bar.tsx b/src/components/data-table-action-bar.tsx index 527fb3b..871f9ab 100644 --- a/src/components/data-table-action-bar.tsx +++ b/src/components/data-table-action-bar.tsx @@ -117,7 +117,7 @@ function DataTableActionBarAction({ {trigger}

{tooltip}

@@ -155,7 +155,7 @@ function DataTableActionBarSelection({

Clear selection

diff --git a/src/components/delete-group-dialog.tsx b/src/components/delete-group-dialog.tsx index cc2a549..6bab110 100644 --- a/src/components/delete-group-dialog.tsx +++ b/src/components/delete-group-dialog.tsx @@ -162,7 +162,7 @@ export function DeleteGroupDialog({ @@ -181,7 +181,7 @@ export function DeleteGroupDialog({ )} {error && ( -
+
{error}
)} diff --git a/src/components/edit-group-dialog.tsx b/src/components/edit-group-dialog.tsx index bcf276b..02aa9bb 100644 --- a/src/components/edit-group-dialog.tsx +++ b/src/components/edit-group-dialog.tsx @@ -101,7 +101,7 @@ export function EditGroupDialog({
{error && ( -
+
{error}
)} diff --git a/src/components/extension-group-assignment-dialog.tsx b/src/components/extension-group-assignment-dialog.tsx index 7eebf9b..eda6ff8 100644 --- a/src/components/extension-group-assignment-dialog.tsx +++ b/src/components/extension-group-assignment-dialog.tsx @@ -160,7 +160,7 @@ export function ExtensionGroupAssignmentDialog({
{error && ( -
+
{error}
)} diff --git a/src/components/extension-management-dialog.tsx b/src/components/extension-management-dialog.tsx index 6a2e188..e337e53 100644 --- a/src/components/extension-management-dialog.tsx +++ b/src/components/extension-management-dialog.tsx @@ -55,10 +55,10 @@ function getSyncStatusDot( switch (status) { case "syncing": - return { color: "bg-yellow-500", tooltip: "Syncing...", animate: true }; + return { color: "bg-warning", tooltip: "Syncing...", animate: true }; case "synced": return { - color: "bg-green-500", + color: "bg-success", tooltip: item.last_sync ? `Synced ${new Date(item.last_sync * 1000).toLocaleString()}` : "Synced", @@ -66,18 +66,22 @@ function getSyncStatusDot( }; case "waiting": return { - color: "bg-yellow-500", + color: "bg-warning", tooltip: "Waiting to sync", animate: false, }; case "error": return { - color: "bg-red-500", + color: "bg-destructive", tooltip: "Sync error", animate: false, }; default: - return { color: "bg-gray-400", tooltip: "Not synced", animate: false }; + return { + color: "bg-muted-foreground", + tooltip: "Not synced", + animate: false, + }; } } diff --git a/src/components/group-assignment-dialog.tsx b/src/components/group-assignment-dialog.tsx index 1bdb1c7..dd62e8f 100644 --- a/src/components/group-assignment-dialog.tsx +++ b/src/components/group-assignment-dialog.tsx @@ -176,7 +176,7 @@ export function GroupAssignmentDialog({
{error && ( -
+
{error}
)} diff --git a/src/components/group-management-dialog.tsx b/src/components/group-management-dialog.tsx index 2aa0ebe..3478484 100644 --- a/src/components/group-management-dialog.tsx +++ b/src/components/group-management-dialog.tsx @@ -49,10 +49,10 @@ function getSyncStatusDot( switch (status) { case "syncing": - return { color: "bg-yellow-500", tooltip: "Syncing...", animate: true }; + return { color: "bg-warning", tooltip: "Syncing...", animate: true }; case "synced": return { - color: "bg-green-500", + color: "bg-success", tooltip: group.last_sync ? `Synced ${new Date(group.last_sync * 1000).toLocaleString()}` : "Synced", @@ -60,18 +60,22 @@ function getSyncStatusDot( }; case "waiting": return { - color: "bg-yellow-500", + color: "bg-warning", tooltip: "Waiting to sync", animate: false, }; case "error": return { - color: "bg-red-500", + color: "bg-destructive", tooltip: errorMessage ? `Sync error: ${errorMessage}` : "Sync error", animate: false, }; default: - return { color: "bg-gray-400", tooltip: "Not synced", animate: false }; + return { + color: "bg-muted-foreground", + tooltip: "Not synced", + animate: false, + }; } } @@ -252,7 +256,7 @@ export function GroupManagementDialog({
{error && ( -
+
{error}
)} diff --git a/src/components/import-profile-dialog.tsx b/src/components/import-profile-dialog.tsx index 62c3810..943e1bf 100644 --- a/src/components/import-profile-dialog.tsx +++ b/src/components/import-profile-dialog.tsx @@ -2,10 +2,12 @@ import { invoke } from "@tauri-apps/api/core"; import { open } from "@tauri-apps/plugin-dialog"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { FaFolder } from "react-icons/fa"; import { toast } from "sonner"; import { LoadingButton } from "@/components/loading-button"; +import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -23,19 +25,29 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { WayfernConfigForm } from "@/components/wayfern-config-form"; import { useBrowserSupport } from "@/hooks/use-browser-support"; +import { useProxyEvents } from "@/hooks/use-proxy-events"; import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; -import type { DetectedProfile } from "@/types"; +import type { CamoufoxConfig, DetectedProfile, WayfernConfig } from "@/types"; import { RippleButton } from "./ui/ripple"; +const getMappedBrowser = (browser: string): "camoufox" | "wayfern" => { + if (["firefox", "firefox-developer", "zen"].includes(browser)) + return "camoufox"; + return "wayfern"; +}; + interface ImportProfileDialogProps { isOpen: boolean; onClose: () => void; + crossOsUnlocked?: boolean; } export function ImportProfileDialog({ isOpen, onClose, + crossOsUnlocked, }: ImportProfileDialogProps) { const [detectedProfiles, setDetectedProfiles] = useState( [], @@ -45,6 +57,12 @@ export function ImportProfileDialog({ const [importMode, setImportMode] = useState<"auto-detect" | "manual">( "auto-detect", ); + const [currentStep, setCurrentStep] = useState<"select" | "configure">( + "select", + ); + const [camoufoxConfig, setCamoufoxConfig] = useState({}); + const [wayfernConfig, setWayfernConfig] = useState({}); + const [selectedProxyId, setSelectedProxyId] = useState(); // Auto-detect state const [selectedDetectedProfile, setSelectedDetectedProfile] = useState< @@ -61,6 +79,7 @@ export function ImportProfileDialog({ const { supportedBrowsers, isLoading: isLoadingSupport } = useBrowserSupport(); + const { storedProxies } = useProxyEvents(); const importableBrowsers = supportedBrowsers; @@ -72,14 +91,11 @@ export function ImportProfileDialog({ ); setDetectedProfiles(profiles); - // Auto-switch to manual mode if no profiles detected if (profiles.length === 0) { setImportMode("manual"); } else { - // Auto-select first profile if available setSelectedDetectedProfile(profiles[0].path); - // Generate default name from the detected profile const profile = profiles[0]; const browserName = getBrowserDisplayName(profile.browser); const defaultName = `Imported ${browserName} Profile`; @@ -93,6 +109,10 @@ export function ImportProfileDialog({ } }, []); + const selectedProfile = detectedProfiles.find( + (p) => p.path === selectedDetectedProfile, + ); + const handleBrowseFolder = async () => { try { const selected = await open({ @@ -110,40 +130,65 @@ export function ImportProfileDialog({ } }; - const handleAutoDetectImport = useCallback(async () => { - if (!selectedDetectedProfile || !autoDetectProfileName.trim()) { - toast.error("Please select a profile and provide a name"); - return; + const handleImport = useCallback(async () => { + let sourcePath: string; + let browserType: string; + let newProfileName: string; + + if (importMode === "auto-detect") { + if (!selectedDetectedProfile || !autoDetectProfileName.trim()) { + toast.error("Please select a profile and provide a name"); + return; + } + const profile = detectedProfiles.find( + (p) => p.path === selectedDetectedProfile, + ); + if (!profile) { + toast.error("Selected profile not found"); + return; + } + sourcePath = profile.path; + browserType = profile.browser; + newProfileName = autoDetectProfileName.trim(); + } else { + if ( + !manualBrowserType || + !manualProfilePath.trim() || + !manualProfileName.trim() + ) { + toast.error("Please fill in all fields"); + return; + } + sourcePath = manualProfilePath.trim(); + browserType = manualBrowserType; + newProfileName = manualProfileName.trim(); } - const profile = detectedProfiles.find( - (p) => p.path === selectedDetectedProfile, - ); - if (!profile) { - toast.error("Selected profile not found"); - return; - } + const mappedBrowser = + importMode === "auto-detect" && selectedProfile + ? (selectedProfile.mapped_browser as "camoufox" | "wayfern") + : getMappedBrowser(browserType); setIsImporting(true); try { await invoke("import_browser_profile", { - sourcePath: profile.path, - browserType: profile.browser, - newProfileName: autoDetectProfileName.trim(), + sourcePath, + browserType, + newProfileName, + proxyId: selectedProxyId ?? null, + camoufoxConfig: mappedBrowser === "camoufox" ? camoufoxConfig : null, + wayfernConfig: mappedBrowser === "wayfern" ? wayfernConfig : null, }); - toast.success( - `Successfully imported profile "${autoDetectProfileName.trim()}"`, - ); + toast.success(`Successfully imported profile "${newProfileName}"`); onClose(); } catch (error) { console.error("Failed to import profile:", error); const errorMessage = error instanceof Error ? error.message : String(error); - // Check if error is about browser not being downloaded if (errorMessage.includes("No downloaded versions found")) { - const browserDisplayName = getBrowserDisplayName(profile.browser); + const browserDisplayName = getBrowserDisplayName(browserType); toast.error( `${browserDisplayName} is not installed. Please download ${browserDisplayName} first from the main window, then try importing again.`, { @@ -157,63 +202,30 @@ export function ImportProfileDialog({ setIsImporting(false); } }, [ + importMode, selectedDetectedProfile, autoDetectProfileName, detectedProfiles, + manualBrowserType, + manualProfilePath, + manualProfileName, + selectedProxyId, + camoufoxConfig, + wayfernConfig, onClose, + selectedProfile, ]); - const handleManualImport = useCallback(async () => { - if ( - !manualBrowserType || - !manualProfilePath.trim() || - !manualProfileName.trim() - ) { - toast.error("Please fill in all fields"); - return; - } - - setIsImporting(true); - try { - await invoke("import_browser_profile", { - sourcePath: manualProfilePath.trim(), - browserType: manualBrowserType, - newProfileName: manualProfileName.trim(), - }); - - toast.success( - `Successfully imported profile "${manualProfileName.trim()}"`, - ); - onClose(); - } catch (error) { - console.error("Failed to import profile:", error); - const errorMessage = - error instanceof Error ? error.message : String(error); - - // Check if error is about browser not being downloaded - if (errorMessage.includes("No downloaded versions found")) { - const browserDisplayName = getBrowserDisplayName(manualBrowserType); - toast.error( - `${browserDisplayName} is not installed. Please download ${browserDisplayName} first from the main window, then try importing again.`, - { - duration: 8000, - }, - ); - } else { - toast.error(`Failed to import profile: ${errorMessage}`); - } - } finally { - setIsImporting(false); - } - }, [manualBrowserType, manualProfilePath, manualProfileName, onClose]); - const handleClose = () => { + setCurrentStep("select"); + setCamoufoxConfig({}); + setWayfernConfig({}); + setSelectedProxyId(undefined); setSelectedDetectedProfile(null); setAutoDetectProfileName(""); setManualBrowserType(null); setManualProfilePath(""); setManualProfileName(""); - // Only reset to auto-detect if there are profiles available if (detectedProfiles.length > 0) { setImportMode("auto-detect"); } else { @@ -222,7 +234,6 @@ export function ImportProfileDialog({ onClose(); }; - // Update auto-detect profile name when selection changes useEffect(() => { if (selectedDetectedProfile) { const profile = detectedProfiles.find( @@ -236,9 +247,38 @@ export function ImportProfileDialog({ } }, [selectedDetectedProfile, detectedProfiles]); - const selectedProfile = detectedProfiles.find( - (p) => p.path === selectedDetectedProfile, - ); + const currentMappedBrowser = useMemo(() => { + if (importMode === "auto-detect" && selectedProfile) { + return selectedProfile.mapped_browser as "camoufox" | "wayfern"; + } + if (importMode === "manual" && manualBrowserType) { + return manualBrowserType as "camoufox" | "wayfern"; + } + return null; + }, [importMode, selectedProfile, manualBrowserType]); + + const canProceedToNext = useMemo(() => { + if (importMode === "auto-detect") { + return ( + !isLoading && + !!selectedDetectedProfile && + !!autoDetectProfileName.trim() + ); + } + return ( + !!manualBrowserType && + !!manualProfilePath.trim() && + !!manualProfileName.trim() + ); + }, [ + importMode, + isLoading, + selectedDetectedProfile, + autoDetectProfileName, + manualBrowserType, + manualProfilePath, + manualProfileName, + ]); useEffect(() => { if (isOpen) { @@ -254,247 +294,322 @@ export function ImportProfileDialog({
- {/* Mode Selection */} -
- { - setImportMode("auto-detect"); - }} - className="flex-1" - disabled={isLoading} - > - Auto-Detect - - { - setImportMode("manual"); - }} - className="flex-1" - disabled={isLoading} - > - Manual Import - -
+ {currentStep === "select" && ( + <> +
+ { + setImportMode("auto-detect"); + }} + className="flex-1" + disabled={isLoading} + > + Auto-Detect + + { + setImportMode("manual"); + }} + className="flex-1" + disabled={isLoading} + > + Manual Import + +
- {/* Auto-Detect Mode */} - {importMode === "auto-detect" && ( -
-

Detected Browser Profiles

- - {isLoading ? ( -
-

- Scanning for browser profiles... -

-
- ) : detectedProfiles.length === 0 ? ( -
-

- No browser profiles found on your system. -

-

- Try the manual import option if you have profiles in custom - locations. -

-
- ) : ( + {importMode === "auto-detect" && (
-
- - -
+

+ Detected Browser Profiles +

- {selectedProfile && ( -
-

- Path:{" "} - {selectedProfile.path} -

-

- Browser:{" "} - {getBrowserDisplayName(selectedProfile.browser)} + {isLoading ? ( +

+

+ Scanning for browser profiles...

- )} + ) : detectedProfiles.length === 0 ? ( +
+

+ No browser profiles found on your system. +

+

+ Try the manual import option if you have profiles in + custom locations. +

+
+ ) : ( +
+
+ + +
-
- - { - setAutoDetectProfileName(e.target.value); - }} - placeholder="Enter a name for the imported profile" - /> + {selectedProfile && ( +
+

+ Path:{" "} + {selectedProfile.path} +

+

+ Browser:{" "} + {getBrowserDisplayName(selectedProfile.browser)} +

+
+ )} + +
+ + { + setAutoDetectProfileName(e.target.value); + }} + placeholder="Enter a name for the imported profile" + /> +
+
+ )} +
+ )} + + {importMode === "manual" && ( +
+

Manual Profile Import

+ +
+
+ + +
+ +
+ +
+ { + setManualProfilePath(e.target.value); + }} + placeholder="Enter the full path to the profile folder" + /> + +
+

+ Example paths: +
+ macOS: ~/Library/Application + Support/Firefox/Profiles/xxx.default +
+ Windows: %APPDATA%\Mozilla\Firefox\Profiles\xxx.default +
+ Linux: ~/.mozilla/firefox/xxx.default +

+
+ +
+ + { + setManualProfileName(e.target.value); + }} + placeholder="Enter a name for the imported profile" + /> +
)} -
+ )} - {/* Manual Import Mode */} - {importMode === "manual" && ( + {currentStep === "configure" && currentMappedBrowser && (
-

Manual Profile Import

+ + + This profile will be imported as a{" "} + {getBrowserDisplayName(currentMappedBrowser)}{" "} + profile. + + -
-
- - -
- -
- -
- { - setManualProfilePath(e.target.value); - }} - placeholder="Enter the full path to the profile folder" - /> - -
-

- Example paths: -
- macOS: ~/Library/Application - Support/Firefox/Profiles/xxx.default -
- Windows: %APPDATA%\Mozilla\Firefox\Profiles\xxx.default -
- Linux: ~/.mozilla/firefox/xxx.default -

-
- -
- - { - setManualProfileName(e.target.value); - }} - placeholder="Enter a name for the imported profile" - /> -
+
+ +
+ + {currentMappedBrowser === "camoufox" ? ( + { + setCamoufoxConfig((prev) => ({ ...prev, [key]: value })); + }} + isCreating={true} + crossOsUnlocked={crossOsUnlocked} + limitedMode={!crossOsUnlocked} + /> + ) : ( + { + setWayfernConfig((prev) => ({ ...prev, [key]: value })); + }} + isCreating={true} + crossOsUnlocked={crossOsUnlocked} + limitedMode={!crossOsUnlocked} + /> + )}
)}
- - Cancel - - {importMode === "auto-detect" ? ( - { - void handleAutoDetectImport(); - }} - disabled={ - !selectedDetectedProfile || - !autoDetectProfileName.trim() || - isLoading - } - > - Import - + {currentStep === "select" ? ( + <> + + Cancel + + { + setCurrentStep("configure"); + }} + > + Next + + ) : ( - { - void handleManualImport(); - }} - disabled={ - !manualBrowserType || - !manualProfilePath.trim() || - !manualProfileName.trim() - } - > - Import - + <> + { + setCurrentStep("select"); + }} + > + Back + + { + void handleImport(); + }} + > + Import + + )} diff --git a/src/components/integrations-dialog.tsx b/src/components/integrations-dialog.tsx index 2c90d74..6c3c3f6 100644 --- a/src/components/integrations-dialog.tsx +++ b/src/components/integrations-dialog.tsx @@ -320,7 +320,7 @@ export function IntegrationsDialog({

Allow AI assistants like Claude Desktop to control browsers. {!termsAccepted && ( - + (Accept Wayfern terms in Settings first) )} diff --git a/src/components/permission-dialog.tsx b/src/components/permission-dialog.tsx index 474a125..eda2fe3 100644 --- a/src/components/permission-dialog.tsx +++ b/src/components/permission-dialog.tsx @@ -116,7 +116,7 @@ export function PermissionDialog({

-
+
{getPermissionIcon(permissionType)}
@@ -129,8 +129,8 @@ export function PermissionDialog({
{isCurrentPermissionGranted && ( -
-

+

+

✅ Permission granted! Browsers launched from Donut Browser can now access your {permissionType}.

@@ -138,8 +138,8 @@ export function PermissionDialog({ )} {!isCurrentPermissionGranted && ( -
-

+

+

⚠️ Permission not granted. Click the button below to request access to your {permissionType}.

diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 2a47294..4d2f0a0 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -234,21 +234,21 @@ function getProfileSyncStatusDot( switch (status) { case "syncing": return { - color: "bg-yellow-500", + color: "bg-warning", tooltip: "Syncing...", animate: true, encrypted, }; case "waiting": return { - color: "bg-yellow-500", + color: "bg-warning", tooltip: "Waiting to sync", animate: false, encrypted, }; case "synced": return { - color: "bg-green-500", + color: "bg-success", tooltip: profile.last_sync ? `Synced ${new Date(profile.last_sync * 1000).toLocaleString()}` : "Synced", @@ -257,7 +257,7 @@ function getProfileSyncStatusDot( }; case "error": return { - color: "bg-red-500", + color: "bg-destructive", tooltip: errorMessage ? `Sync error: ${errorMessage}` : "Sync error", animate: false, encrypted, @@ -265,7 +265,7 @@ function getProfileSyncStatusDot( case "disabled": if (profile.last_sync) { return { - color: "bg-gray-400", + color: "bg-muted-foreground", tooltip: `Sync disabled, last sync ${formatRelativeTime(profile.last_sync)}`, animate: false, encrypted: false, diff --git a/src/components/proxy-assignment-dialog.tsx b/src/components/proxy-assignment-dialog.tsx index 4841790..6c0abf3 100644 --- a/src/components/proxy-assignment-dialog.tsx +++ b/src/components/proxy-assignment-dialog.tsx @@ -276,7 +276,7 @@ export function ProxyAssignmentDialog({
{error && ( -
+
{error}
)} diff --git a/src/components/proxy-import-dialog.tsx b/src/components/proxy-import-dialog.tsx index 0c4a66a..086b561 100644 --- a/src/components/proxy-import-dialog.tsx +++ b/src/components/proxy-import-dialog.tsx @@ -429,14 +429,14 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
Imported: - + {importResult.imported_count}
{importResult.skipped_count > 0 && (
Skipped (duplicates): - + {importResult.skipped_count}
@@ -444,7 +444,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) { {importResult.errors.length > 0 && (
Errors: - + {importResult.errors.length}
@@ -459,7 +459,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) { {importResult.errors.map((error, i) => (
{error}
diff --git a/src/components/proxy-management-dialog.tsx b/src/components/proxy-management-dialog.tsx index b75b0ec..4a75368 100644 --- a/src/components/proxy-management-dialog.tsx +++ b/src/components/proxy-management-dialog.tsx @@ -59,10 +59,10 @@ function getSyncStatusDot( switch (status) { case "syncing": - return { color: "bg-yellow-500", tooltip: "Syncing...", animate: true }; + return { color: "bg-warning", tooltip: "Syncing...", animate: true }; case "synced": return { - color: "bg-green-500", + color: "bg-success", tooltip: item.last_sync ? `Synced ${new Date(item.last_sync * 1000).toLocaleString()}` : "Synced", @@ -70,18 +70,22 @@ function getSyncStatusDot( }; case "waiting": return { - color: "bg-yellow-500", + color: "bg-warning", tooltip: "Waiting to sync", animate: false, }; case "error": return { - color: "bg-red-500", + color: "bg-destructive", tooltip: errorMessage ? `Sync error: ${errorMessage}` : "Sync error", animate: false, }; default: - return { color: "bg-gray-400", tooltip: "Not synced", animate: false }; + return { + color: "bg-muted-foreground", + tooltip: "Not synced", + animate: false, + }; } } diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index 14a2ecc..31405ca 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -170,7 +170,7 @@ export function SettingsDialog({ const getStatusBadge = useCallback((isGranted: boolean) => { if (isGranted) { return ( - + Granted ); @@ -1018,7 +1018,7 @@ export function SettingsDialog({
) : (
-

+

Trial expired

diff --git a/src/components/sync-config-dialog.tsx b/src/components/sync-config-dialog.tsx index 8cb1f35..24bbc37 100644 --- a/src/components/sync-config-dialog.tsx +++ b/src/components/sync-config-dialog.tsx @@ -269,7 +269,7 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) { {isLoggedIn && user ? (

-
+
{t("sync.cloud.connected")}
@@ -530,13 +530,13 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) { )} {connectionStatus === "connected" && (
-
+
{t("sync.status.connected")}
)} {connectionStatus === "error" && (
-
+
{t("sync.status.disconnected")}
)} diff --git a/src/components/vpn-check-button.tsx b/src/components/vpn-check-button.tsx index 8691d2a..7e06744 100644 --- a/src/components/vpn-check-button.tsx +++ b/src/components/vpn-check-button.tsx @@ -75,7 +75,7 @@ export function VpnCheckButton({ {isCurrentlyChecking ? (
) : result?.is_valid ? ( - + ) : result && !result.is_valid ? ( ) : ( diff --git a/src/components/vpn-import-dialog.tsx b/src/components/vpn-import-dialog.tsx index 196f425..43f9197 100644 --- a/src/components/vpn-import-dialog.tsx +++ b/src/components/vpn-import-dialog.tsx @@ -295,13 +295,13 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) { {step === "vpn-result" && vpnImportResult && (
{vpnImportResult.success ? (
- +
-
+
VPN Imported Successfully
@@ -311,10 +311,10 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
) : (
-
+
Import Failed
-
+
{vpnImportResult.error}
diff --git a/src/hooks/use-version-updater.ts b/src/hooks/use-version-updater.ts index 1b01439..98d1412 100644 --- a/src/hooks/use-version-updater.ts +++ b/src/hooks/use-version-updater.ts @@ -3,11 +3,9 @@ import { listen } from "@tauri-apps/api/event"; import { useCallback, useEffect, useRef, useState } from "react"; import { getBrowserDisplayName } from "@/lib/browser-utils"; import { - dismissToast, showAutoUpdateToast, showErrorToast, showSuccessToast, - showUnifiedVersionUpdateToast, } from "@/lib/toast-utils"; interface VersionUpdateProgress { @@ -76,53 +74,13 @@ export function useVersionUpdater() { if (progress.status === "updating") { setIsUpdating(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, - }, - onCancel: () => dismissToast("unified-version-update"), - }); } else if (progress.status === "completed") { setIsUpdating(false); setUpdateProgress(null); - dismissToast("unified-version-update"); - - if (progress.new_versions_found > 0) { - showSuccessToast("Browser versions updated successfully", { - duration: 5000, - description: - "Auto-downloads will start shortly for available updates.", - }); - } else { - showSuccessToast("No new browser versions found", { - duration: 3000, - description: "All browser versions are up to date", - }); - } - - // Refresh status void loadUpdateStatus(); } else if (progress.status === "error") { setIsUpdating(false); setUpdateProgress(null); - dismissToast("unified-version-update"); - - showErrorToast("Failed to update browser versions", { - duration: 6000, - description: "Check your internet connection and try again", - }); } }, ); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a86990f..f42efa2 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -541,11 +541,6 @@ "unknownError": "An unknown error occurred. Please try again." }, "browser": { - "firefox": "Firefox", - "firefoxDeveloper": "Firefox Developer Edition", - "chromium": "Chromium", - "brave": "Brave", - "zen": "Zen Browser", "camoufox": "Camoufox", "wayfern": "Wayfern" }, diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 7c6c276..c517f79 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -541,11 +541,6 @@ "unknownError": "Ocurrió un error desconocido. Por favor intenta de nuevo." }, "browser": { - "firefox": "Firefox", - "firefoxDeveloper": "Firefox Developer Edition", - "chromium": "Chromium", - "brave": "Brave", - "zen": "Zen Browser", "camoufox": "Camoufox", "wayfern": "Wayfern" }, diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index e273f5c..ba5714d 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -541,11 +541,6 @@ "unknownError": "Une erreur inconnue s'est produite. Veuillez réessayer." }, "browser": { - "firefox": "Firefox", - "firefoxDeveloper": "Firefox Developer Edition", - "chromium": "Chromium", - "brave": "Brave", - "zen": "Zen Browser", "camoufox": "Camoufox", "wayfern": "Wayfern" }, diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 1debdd1..a089c90 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -541,11 +541,6 @@ "unknownError": "不明なエラーが発生しました。もう一度お試しください。" }, "browser": { - "firefox": "Firefox", - "firefoxDeveloper": "Firefox Developer Edition", - "chromium": "Chromium", - "brave": "Brave", - "zen": "Zen Browser", "camoufox": "Camoufox", "wayfern": "Wayfern" }, diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 2b6a506..2df31d5 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -541,11 +541,6 @@ "unknownError": "Ocorreu um erro desconhecido. Por favor, tente novamente." }, "browser": { - "firefox": "Firefox", - "firefoxDeveloper": "Firefox Developer Edition", - "chromium": "Chromium", - "brave": "Brave", - "zen": "Zen Browser", "camoufox": "Camoufox", "wayfern": "Wayfern" }, diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 5b1e531..1427132 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -541,11 +541,6 @@ "unknownError": "Произошла неизвестная ошибка. Попробуйте снова." }, "browser": { - "firefox": "Firefox", - "firefoxDeveloper": "Firefox Developer Edition", - "chromium": "Chromium", - "brave": "Brave", - "zen": "Zen Browser", "camoufox": "Camoufox", "wayfern": "Wayfern" }, diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d1c48f3..9d474ff 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -541,11 +541,6 @@ "unknownError": "发生未知错误。请重试。" }, "browser": { - "firefox": "Firefox", - "firefoxDeveloper": "Firefox 开发者版", - "chromium": "Chromium", - "brave": "Brave", - "zen": "Zen Browser", "camoufox": "Camoufox", "wayfern": "Wayfern" }, diff --git a/src/lib/browser-utils.ts b/src/lib/browser-utils.ts index e278983..7652c23 100644 --- a/src/lib/browser-utils.ts +++ b/src/lib/browser-utils.ts @@ -15,11 +15,6 @@ import { */ export function getBrowserDisplayName(browserType: string): string { const browserNames: Record = { - firefox: "Firefox", - "firefox-developer": "Firefox Developer Edition", - zen: "Zen Browser", - brave: "Brave", - chromium: "Chromium", camoufox: "Camoufox", wayfern: "Wayfern", }; diff --git a/src/lib/themes.ts b/src/lib/themes.ts index b6bc474..1d60fa0 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -15,6 +15,10 @@ export interface ThemeColors extends Record { "--accent-foreground": string; "--destructive": string; "--destructive-foreground": string; + "--success": string; + "--success-foreground": string; + "--warning": string; + "--warning-foreground": string; "--border": string; "--chart-1": string; "--chart-2": string; @@ -50,6 +54,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#1a1b26", "--destructive": "#f7768e", "--destructive-foreground": "#1a1b26", + "--success": "#9ece6a", + "--success-foreground": "#1a1b26", + "--warning": "#e0af68", + "--warning-foreground": "#1a1b26", "--border": "#3b4261", "--chart-1": "#7aa2f7", "--chart-2": "#9ece6a", @@ -78,6 +86,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#282a36", "--destructive": "#ff5555", "--destructive-foreground": "#f8f8f2", + "--success": "#50fa7b", + "--success-foreground": "#282a36", + "--warning": "#ffb86c", + "--warning-foreground": "#282a36", "--border": "#6272a4", "--chart-1": "#bd93f9", "--chart-2": "#50fa7b", @@ -106,6 +118,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#273136", "--destructive": "#ff819f", "--destructive-foreground": "#273136", + "--success": "#a8c97f", + "--success-foreground": "#273136", + "--warning": "#e6c07b", + "--warning-foreground": "#273136", "--border": "#304e37", "--chart-1": "#7eb08a", "--chart-2": "#d2b48c", @@ -134,6 +150,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#f7f7f8", "--destructive": "#ef4444", "--destructive-foreground": "#f7f7f8", + "--success": "#22c55e", + "--success-foreground": "#17191e", + "--warning": "#f59e0b", + "--warning-foreground": "#17191e", "--border": "#2a2e39", "--chart-1": "#5755d9", "--chart-2": "#0ea5e9", @@ -162,6 +182,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#0a0e14", "--destructive": "#f07178", "--destructive-foreground": "#b3b1ad", + "--success": "#c2d94c", + "--success-foreground": "#0a0e14", + "--warning": "#ffb454", + "--warning-foreground": "#0a0e14", "--border": "#1f2430", "--chart-1": "#39bae6", "--chart-2": "#c2d94c", @@ -190,6 +214,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#fafafa", "--destructive": "#f07178", "--destructive-foreground": "#fafafa", + "--success": "#86b300", + "--success-foreground": "#fafafa", + "--warning": "#fa8d3e", + "--warning-foreground": "#fafafa", "--border": "#e7eaed", "--chart-1": "#399ee6", "--chart-2": "#86b300", @@ -218,6 +246,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#eff1f5", "--destructive": "#d20f39", "--destructive-foreground": "#eff1f5", + "--success": "#40a02b", + "--success-foreground": "#eff1f5", + "--warning": "#df8e1d", + "--warning-foreground": "#eff1f5", "--border": "#9ca0b0", "--chart-1": "#1e66f5", "--chart-2": "#40a02b", @@ -246,6 +278,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#303446", "--destructive": "#e78284", "--destructive-foreground": "#303446", + "--success": "#a6d189", + "--success-foreground": "#303446", + "--warning": "#e5c890", + "--warning-foreground": "#303446", "--border": "#737994", "--chart-1": "#8caaee", "--chart-2": "#a6d189", @@ -274,6 +310,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#24273a", "--destructive": "#ed8796", "--destructive-foreground": "#24273a", + "--success": "#a6da95", + "--success-foreground": "#24273a", + "--warning": "#eed49f", + "--warning-foreground": "#24273a", "--border": "#6e738d", "--chart-1": "#8aadf4", "--chart-2": "#a6da95", @@ -302,6 +342,10 @@ export const THEMES: Theme[] = [ "--accent-foreground": "#1e1e2e", "--destructive": "#f38ba8", "--destructive-foreground": "#1e1e2e", + "--success": "#a6e3a1", + "--success-foreground": "#1e1e2e", + "--warning": "#f9e2af", + "--warning-foreground": "#1e1e2e", "--border": "#585b70", "--chart-1": "#89b4fa", "--chart-2": "#a6e3a1", @@ -330,6 +374,10 @@ export const THEME_VARIABLES: Array<{ key: keyof ThemeColors; label: string }> = { key: "--accent-foreground", label: "Accent FG" }, { key: "--destructive", label: "Destructive" }, { key: "--destructive-foreground", label: "Destructive FG" }, + { key: "--success", label: "Success" }, + { key: "--success-foreground", label: "Success FG" }, + { key: "--warning", label: "Warning" }, + { key: "--warning-foreground", label: "Warning FG" }, { key: "--border", label: "Border" }, { key: "--chart-1", label: "Chart 1" }, { key: "--chart-2", label: "Chart 2" }, diff --git a/src/lib/toast-utils.ts b/src/lib/toast-utils.ts index cbd4bc1..86dd9d5 100644 --- a/src/lib/toast-utils.ts +++ b/src/lib/toast-utils.ts @@ -82,13 +82,10 @@ export function showToast(props: ToastProps & { id?: string }) { duration = 10000; break; case "download": - // Only keep infinite for active downloading, others get shorter durations - if ("stage" in props && props.stage === "downloading") { - duration = Number.POSITIVE_INFINITY; - } else if ("stage" in props && props.stage === "completed") { + if ("stage" in props && props.stage === "completed") { duration = 3000; } else { - duration = 20000; + duration = Number.POSITIVE_INFINITY; } break; case "success": diff --git a/src/styles/globals.css b/src/styles/globals.css index 96d2d04..82ee3b9 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -28,6 +28,11 @@ --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); @@ -63,6 +68,11 @@ --accent: oklch(0.967 0.001 286.375); --accent-foreground: oklch(0.21 0.006 285.885); --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.985 0 0); + --success: oklch(0.6 0.2 145); + --success-foreground: oklch(0.985 0 0); + --warning: oklch(0.75 0.15 75); + --warning-foreground: oklch(0.141 0.005 285.823); --border: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32); --ring: oklch(0.705 0.015 286.067); @@ -102,6 +112,11 @@ --accent: oklch(0.274 0.006 286.033); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); + --destructive-foreground: oklch(0.985 0 0); + --success: oklch(0.7 0.2 145); + --success-foreground: oklch(0.141 0.005 285.823); + --warning: oklch(0.8 0.15 75); + --warning-foreground: oklch(0.141 0.005 285.823); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.552 0.016 285.938); diff --git a/src/types.ts b/src/types.ts index c0b6ef9..0220ada 100644 --- a/src/types.ts +++ b/src/types.ts @@ -161,6 +161,7 @@ export interface DetectedProfile { name: string; path: string; description: string; + mapped_browser: string; } export interface BrowserReleaseTypes {