From d48e26c7eb808737ebf92a504f93d258334c9f33 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Mon, 11 Aug 2025 05:42:17 +0400 Subject: [PATCH] refactor: cleanup and better brave release fetching --- .vscode/settings.json | 1 + src-tauri/src/api_client.rs | 506 +++++++++++------------ src-tauri/src/browser_runner.rs | 36 -- src-tauri/src/browser_version_manager.rs | 129 ++++-- src-tauri/src/camoufox.rs | 25 -- src-tauri/src/extraction.rs | 28 -- src-tauri/src/lib.rs | 11 +- src-tauri/src/profile_importer.rs | 152 +------ src-tauri/src/proxy_manager.rs | 7 - src-tauri/src/version_updater.rs | 16 +- src-tauri/tests/common/mod.rs | 74 ---- src/app/page.tsx | 19 - src/components/change-version-dialog.tsx | 274 ------------ src/components/create-profile-dialog.tsx | 129 +++--- src/components/import-profile-dialog.tsx | 7 +- src/components/profile-data-table.tsx | 15 - src/components/release-type-selector.tsx | 3 - 17 files changed, 413 insertions(+), 1019 deletions(-) delete mode 100644 src/components/change-version-dialog.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ce83b9..7843b0b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -108,6 +108,7 @@ "pathex", "pathlib", "peerconnection", + "pids", "pixbuf", "plasmohq", "platformdirs", diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs index 809550d..301a39b 100644 --- a/src-tauri/src/api_client.rs +++ b/src-tauri/src/api_client.rs @@ -34,6 +34,12 @@ pub enum PreReleaseKind { impl VersionComponent { pub fn parse(version: &str) -> Self { let version = version.trim(); + // Normalize common tag prefixes like 'v1.2.3' -> '1.2.3' + let version = if version.starts_with('v') || version.starts_with('V') { + &version[1..] + } else { + version + }; // Handle special case for Zen Browser twilight releases if version.to_lowercase() == "twilight" { @@ -218,8 +224,11 @@ pub fn sort_versions(versions: &mut [String]) { // Helper function to sort GitHub releases pub fn sort_github_releases(releases: &mut [GithubRelease]) { releases.sort_by(|a, b| { - let version_a = VersionComponent::parse(&a.tag_name); - let version_b = VersionComponent::parse(&b.tag_name); + // Normalize tags like "v1.81.9" -> "1.81.9" for correct ordering + let tag_a = a.tag_name.trim_start_matches('v'); + let tag_b = b.tag_name.trim_start_matches('v'); + let version_a = VersionComponent::parse(tag_a); + let version_b = VersionComponent::parse(tag_b); version_b.cmp(&version_a) // Descending order (newest first) }); } @@ -242,12 +251,22 @@ pub fn is_browser_version_nightly( version.to_lowercase() == "twilight" } "brave" => { - // For Brave Browser, only releases titled "Release" are stable, everything else is nightly + // For Brave Browser, only releases whose name starts with "Release" (case-insensitive) are stable. if let Some(name) = release_name { - !name.starts_with("Release") - } else { - true + let normalized = name.trim_start().to_ascii_lowercase(); + return !normalized.starts_with("release"); } + + // Fallback: try cached GitHub releases + if let Some(releases) = ApiClient::instance().get_cached_github_releases("brave") { + if let Some(found) = releases.iter().find(|r| r.tag_name == version) { + let normalized = found.name.trim_start().to_ascii_lowercase(); + return !normalized.starts_with("release"); + } + } + + // Last resort: when no name available, treat as nightly (non-Release) + true } "firefox" | "firefox-developer" => { // For Firefox, use the category from the API response to determine stability @@ -295,7 +314,7 @@ pub struct BrowserRelease { #[derive(Debug, Serialize, Deserialize)] struct CachedVersionData { - versions: Vec, + releases: Vec, timestamp: u64, } @@ -327,6 +346,65 @@ impl ApiClient { } } + async fn fetch_github_releases_multiple_pages( + &self, + base_releases_url: &str, + ) -> Result, Box> { + let mut all_releases: Vec = Vec::new(); + + // For now, only fetch 1 page + for page in 1..=1 { + let url = format!("{base_releases_url}?per_page=100&page={page}"); + let response = self + .client + .get(&url) + .header( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", + ) + .send() + .await?; + + if !response.status().is_success() { + // If the first page fails, propagate error; otherwise stop pagination + if page == 1 { + return Err( + format!( + "GitHub API returned status for page {}: {}", + page, + response.status() + ) + .into(), + ); + } else { + break; + } + } + + let text = response.text().await?; + let mut page_releases: Vec = serde_json::from_str(&text).map_err(|e| { + eprintln!("Failed to parse GitHub API response (page {page}): {e}"); + eprintln!( + "Response text (first 500 chars): {}", + if text.len() > 500 { + &text[..500] + } else { + &text + } + ); + format!("Failed to parse GitHub API response: {e}") + })?; + + if page_releases.is_empty() { + break; + } + + all_releases.append(&mut page_releases); + } + + Ok(all_releases) + } + pub fn instance() -> &'static ApiClient { &API_CLIENT } @@ -374,7 +452,7 @@ impl ApiClient { current_time - timestamp < cache_duration } - pub fn load_cached_versions(&self, browser: &str) -> Option> { + pub fn load_cached_versions(&self, browser: &str) -> Option> { let cache_dir = Self::get_cache_dir().ok()?; let cache_file = cache_dir.join(format!("{browser}_versions.json")); @@ -383,11 +461,27 @@ impl ApiClient { } let content = fs::read_to_string(&cache_file).ok()?; - let cached_data: CachedVersionData = serde_json::from_str(&content).ok()?; + if let Ok(cached) = serde_json::from_str::(&content) { + // Always return cached releases regardless of age - they're always valid + println!("Using cached versions for {browser}"); + return Some(cached.releases); + } - // Always return cached versions regardless of age - they're always valid - println!("Using cached versions for {browser}"); - Some(cached_data.versions) + // Backward compatibility: legacy caches stored just an array of version strings + if let Ok(legacy_versions) = serde_json::from_str::>(&content) { + println!("Using legacy cached versions for {browser}; upgrading in-memory"); + let releases: Vec = legacy_versions + .into_iter() + .map(|version| BrowserRelease { + is_prerelease: is_browser_version_nightly(browser, &version, None), + version, + date: "".to_string(), + }) + .collect(); + return Some(releases); + } + + None } pub fn is_cache_expired(&self, browser: &str) -> bool { @@ -418,19 +512,19 @@ impl ApiClient { pub fn save_cached_versions( &self, browser: &str, - versions: &[String], + releases: &[BrowserRelease], ) -> Result<(), Box> { let cache_dir = Self::get_cache_dir()?; let cache_file = cache_dir.join(format!("{browser}_versions.json")); let cached_data = CachedVersionData { - versions: versions.to_vec(), + releases: releases.to_vec(), timestamp: Self::get_current_timestamp(), }; let content = serde_json::to_string_pretty(&cached_data)?; fs::write(&cache_file, content)?; - println!("Cached {} versions for {}", versions.len(), browser); + println!("Cached {} versions for {}", releases.len(), browser); Ok(()) } @@ -450,6 +544,11 @@ impl ApiClient { Some(cached_data.releases) } + /// Public accessor for cached GitHub releases (used by other modules for classification) + pub fn get_cached_github_releases(&self, browser: &str) -> Option> { + self.load_cached_github_releases(browser) + } + fn save_cached_github_releases( &self, browser: &str, @@ -475,19 +574,8 @@ impl ApiClient { ) -> Result, Box> { // Check cache first (unless bypassing) if !no_caching { - if let Some(cached_versions) = self.load_cached_versions("firefox") { - return Ok( - cached_versions - .into_iter() - .map(|version| { - BrowserRelease { - version: version.clone(), - date: "".to_string(), // Cache doesn't store dates - is_prerelease: is_browser_version_nightly("firefox", &version, None), - } - }) - .collect(), - ); + if let Some(cached_releases) = self.load_cached_versions("firefox") { + return Ok(cached_releases); } } @@ -533,12 +621,9 @@ impl ApiClient { version_b.cmp(&version_a) }); - // Extract versions for caching - let versions: Vec = releases.iter().map(|r| r.version.clone()).collect(); - // Cache the results (unless bypassing cache) if !no_caching { - if let Err(e) = self.save_cached_versions("firefox", &versions) { + if let Err(e) = self.save_cached_versions("firefox", &releases) { eprintln!("Failed to cache Firefox versions: {e}"); } } @@ -552,19 +637,8 @@ impl ApiClient { ) -> Result, Box> { // Check cache first (unless bypassing) if !no_caching { - if let Some(cached_versions) = self.load_cached_versions("firefox-developer") { - return Ok( - cached_versions - .into_iter() - .map(|version| { - BrowserRelease { - version: version.clone(), - date: "".to_string(), // Cache doesn't store dates - is_prerelease: is_browser_version_nightly("firefox-developer", &version, None), - } - }) - .collect(), - ); + if let Some(cached_releases) = self.load_cached_versions("firefox-developer") { + return Ok(cached_releases); } } @@ -616,12 +690,9 @@ impl ApiClient { version_b.cmp(&version_a) }); - // Extract versions for caching - let versions: Vec = releases.iter().map(|r| r.version.clone()).collect(); - // Cache the results (unless bypassing cache) if !no_caching { - if let Err(e) = self.save_cached_versions("firefox-developer", &versions) { + if let Err(e) = self.save_cached_versions("firefox-developer", &releases) { eprintln!("Failed to cache Firefox Developer versions: {e}"); } } @@ -640,43 +711,12 @@ impl ApiClient { } } - println!("Fetching Mullvad releases from GitHub API..."); - let url = format!( - "{}/repos/mullvad/mullvad-browser/releases?per_page=100", + println!("Fetching Mullvad releases from GitHub API"); + let base_url = format!( + "{}/repos/mullvad/mullvad-browser/releases", self.github_api_base ); - - let response = self - .client - .get(url) - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") - .send() - .await?; - - if !response.status().is_success() { - return Err(format!("GitHub API returned status: {}", response.status()).into()); - } - - // Get the response text first for better error reporting - let response_text = response.text().await?; - - // Try to parse the JSON with better error handling - let releases: Vec = match serde_json::from_str(&response_text) { - Ok(releases) => releases, - Err(e) => { - eprintln!("Failed to parse GitHub API response for Mullvad releases:"); - eprintln!("Error: {e}"); - eprintln!( - "Response text (first 500 chars): {}", - if response_text.len() > 500 { - &response_text[..500] - } else { - &response_text - } - ); - return Err(format!("Failed to parse GitHub API response: {e}").into()); - } - }; + let releases = self.fetch_github_releases_multiple_pages(&base_url).await?; let mut releases: Vec = releases .into_iter() @@ -710,43 +750,13 @@ impl ApiClient { } } - println!("Fetching Zen releases from GitHub API..."); - let url = format!( - "{}/repos/zen-browser/desktop/releases?per_page=100", + println!("Fetching Zen releases from GitHub API"); + let base_url = format!( + "{}/repos/zen-browser/desktop/releases", self.github_api_base ); - - let response = self - .client - .get(url) - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") - .send() - .await?; - - if !response.status().is_success() { - return Err(format!("GitHub API returned status: {}", response.status()).into()); - } - - // Get the response text first for better error reporting - let response_text = response.text().await?; - - // Try to parse the JSON with better error handling - let mut releases: Vec = match serde_json::from_str(&response_text) { - Ok(releases) => releases, - Err(e) => { - eprintln!("Failed to parse GitHub API response for Zen releases:"); - eprintln!("Error: {e}"); - eprintln!( - "Response text (first 500 chars): {}", - if response_text.len() > 500 { - &response_text[..500] - } else { - &response_text - } - ); - return Err(format!("Failed to parse GitHub API response: {e}").into()); - } - }; + let mut releases: Vec = + self.fetch_github_releases_multiple_pages(&base_url).await?; // Check for twilight updates and mark alpha releases for release in &mut releases { @@ -791,55 +801,25 @@ impl ApiClient { } } - println!("Fetching Brave releases from GitHub API..."); - let url = format!( - "{}/repos/brave/brave-browser/releases?per_page=100", + println!("Fetching Brave releases from GitHub API"); + let base_url = format!( + "{}/repos/brave/brave-browser/releases", self.github_api_base ); - - let response = self - .client - .get(url) - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") - .send() - .await?; - - if !response.status().is_success() { - return Err(format!("GitHub API returned status: {}", response.status()).into()); - } - - // Get the response text first for better error reporting - let response_text = response.text().await?; - - // Try to parse the JSON with better error handling - let releases: Vec = match serde_json::from_str(&response_text) { - Ok(releases) => releases, - Err(e) => { - eprintln!("Failed to parse GitHub API response for Brave releases:"); - eprintln!("Error: {e}"); - eprintln!( - "Response text (first 500 chars): {}", - if response_text.len() > 500 { - &response_text[..500] - } else { - &response_text - } - ); - return Err(format!("Failed to parse GitHub API response: {e}").into()); - } - }; + let releases: Vec = self.fetch_github_releases_multiple_pages(&base_url).await?; // Get platform info to filter appropriate releases - let (os, arch) = Self::get_platform_info(); + let (os, _) = Self::get_platform_info(); // Filter releases that have assets compatible with the current platform let mut filtered_releases: Vec = releases .into_iter() .filter_map(|mut release| { // Check if this release has compatible assets for the current platform - let has_compatible_asset = Self::has_compatible_brave_asset(&release.assets, &os, &arch); + let has_compatible_asset = Self::has_compatible_brave_asset(&release.assets, &os); if has_compatible_asset { + println!("release.name: {:?}", release.name); // Use the centralized nightly detection function release.is_nightly = is_browser_version_nightly("brave", &release.tag_name, Some(&release.name)); @@ -853,11 +833,8 @@ impl ApiClient { // Sort releases using the new version sorting system sort_github_releases(&mut filtered_releases); - // Cache the results (unless bypassing cache) - if !no_caching { - if let Err(e) = self.save_cached_github_releases("brave", &filtered_releases) { - eprintln!("Failed to cache Brave releases: {e}"); - } + if let Err(e) = self.save_cached_github_releases("brave", &filtered_releases) { + eprintln!("Failed to cache Brave releases: {e}"); } Ok(filtered_releases) @@ -889,11 +866,7 @@ impl ApiClient { }) } - fn has_compatible_brave_asset( - assets: &[crate::browser::GithubAsset], - os: &str, - arch: &str, - ) -> bool { + fn has_compatible_brave_asset(assets: &[crate::browser::GithubAsset], os: &str) -> bool { match os { "windows" => { // For Windows, look for standalone setup EXE (not the auto-updater one) @@ -910,12 +883,9 @@ impl ApiClient { }) || assets.iter().any(|asset| asset.name.ends_with(".dmg")) } "linux" => { - // For Linux, be strict about architecture matching - only allow assets that explicitly match the current architecture - let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" }; - if assets.iter().any(|asset| { let name = asset.name.to_lowercase(); - name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip") + name.contains("lin") }) { return true; } @@ -979,19 +949,8 @@ impl ApiClient { ) -> Result, Box> { // Check cache first (unless bypassing) if !no_caching { - if let Some(cached_versions) = self.load_cached_versions("chromium") { - return Ok( - cached_versions - .into_iter() - .map(|version| { - BrowserRelease { - version: version.clone(), - date: "".to_string(), // Cache doesn't store dates - is_prerelease: false, // Chromium versions are generally stable builds - } - }) - .collect(), - ); + if let Some(cached_releases) = self.load_cached_versions("chromium") { + return Ok(cached_releases); } } @@ -1010,23 +969,24 @@ impl ApiClient { } } + // Convert to BrowserRelease objects + let releases: Vec = versions + .into_iter() + .map(|version| BrowserRelease { + version: version.clone(), + date: "".to_string(), + is_prerelease: false, + }) + .collect(); + // Cache the results (unless bypassing cache) if !no_caching { - if let Err(e) = self.save_cached_versions("chromium", &versions) { + if let Err(e) = self.save_cached_versions("chromium", &releases) { eprintln!("Failed to cache Chromium versions: {e}"); } } - Ok( - versions - .into_iter() - .map(|version| BrowserRelease { - version: version.clone(), - date: "".to_string(), - is_prerelease: false, - }) - .collect(), - ) + Ok(releases) } pub async fn fetch_camoufox_releases_with_caching( @@ -1044,43 +1004,9 @@ impl ApiClient { } } - println!("Fetching Camoufox releases from GitHub API..."); - let url = format!( - "{}/repos/daijro/camoufox/releases?per_page=100", - self.github_api_base - ); - - let response = self - .client - .get(url) - .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") - .send() - .await?; - - if !response.status().is_success() { - return Err(format!("GitHub API returned status: {}", response.status()).into()); - } - - // Get the response text first for better error reporting - let response_text = response.text().await?; - - // Try to parse the JSON with better error handling - let releases: Vec = match serde_json::from_str(&response_text) { - Ok(releases) => releases, - Err(e) => { - eprintln!("Failed to parse GitHub API response for Camoufox releases:"); - eprintln!("Error: {e}"); - eprintln!( - "Response text (first 500 chars): {}", - if response_text.len() > 500 { - &response_text[..500] - } else { - &response_text - } - ); - return Err(format!("Failed to parse GitHub API response: {e}").into()); - } - }; + println!("Fetching Camoufox releases from GitHub API"); + let base_url = format!("{}/repos/daijro/camoufox/releases", self.github_api_base); + let releases: Vec = self.fetch_github_releases_multiple_pages(&base_url).await?; println!( "Fetched {} total Camoufox releases from GitHub", @@ -1157,19 +1083,8 @@ impl ApiClient { ) -> Result, Box> { // Check cache first (unless bypassing) if !no_caching { - if let Some(cached_versions) = self.load_cached_versions("tor-browser") { - return Ok( - cached_versions - .into_iter() - .map(|version| { - BrowserRelease { - version: version.clone(), - date: "".to_string(), // Cache doesn't store dates - is_prerelease: is_browser_version_nightly("tor-browser", &version, None), - } - }) - .collect(), - ); + if let Some(cached_releases) = self.load_cached_versions("tor-browser") { + return Ok(cached_releases); } } @@ -1225,25 +1140,24 @@ impl ApiClient { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } + // Convert to BrowserRelease objects + let releases: Vec = version_strings + .into_iter() + .map(|version| BrowserRelease { + version: version.clone(), + date: "".to_string(), // TOR archive doesn't provide structured dates + is_prerelease: false, // Assume all archived versions are stable + }) + .collect(); + // Cache the results (unless bypassing cache) if !no_caching { - if let Err(e) = self.save_cached_versions("tor-browser", &version_strings) { + if let Err(e) = self.save_cached_versions("tor-browser", &releases) { eprintln!("Failed to cache TOR versions: {e}"); } } - Ok( - version_strings - .into_iter() - .map(|version| { - BrowserRelease { - version: version.clone(), - date: "".to_string(), // TOR archive doesn't provide structured dates - is_prerelease: false, // Assume all archived versions are stable - } - }) - .collect(), - ) + Ok(releases) } async fn check_tor_version_has_macos( @@ -1970,6 +1884,84 @@ mod tests { assert!(result.is_err()); } + #[tokio::test] + async fn test_mullvad_pagination_two_pages() { + let server = setup_mock_server().await; + let client = create_test_client(&server); + + // Page 1 response with Link: rel="next" header + let mock_page1 = r#"[ + { + "tag_name": "100.0", + "name": "Mullvad Browser 100.0", + "prerelease": false, + "published_at": "2024-07-01T00:00:00Z", + "assets": [ + { "name": "mullvad-browser-macos-100.0.dmg", "browser_download_url": "https://example.com/100.0.dmg", "size": 1 } + ] + } + ]"#; + + // Page 2 response + let mock_page2 = r#"[ + { + "tag_name": "99.0", + "name": "Mullvad Browser 99.0", + "prerelease": false, + "published_at": "2024-06-01T00:00:00Z", + "assets": [ + { "name": "mullvad-browser-macos-99.0.dmg", "browser_download_url": "https://example.com/99.0.dmg", "size": 1 } + ] + } + ]"#; + + // Mock page 1 + Mock::given(method("GET")) + .and(path("/repos/mullvad/mullvad-browser/releases")) + .and(query_param("per_page", "100")) + .and(query_param("page", "1")) + .respond_with( + ResponseTemplate::new(200) + .set_body_string(mock_page1) + .insert_header("content-type", "application/json") + .insert_header( + "link", + format!( + "<{}?per_page=100&page=2>; rel=\"next\", <{}?per_page=100&page=2>; rel=\"last\"", + server.uri().to_string() + "/repos/mullvad/mullvad-browser/releases", + server.uri().to_string() + "/repos/mullvad/mullvad-browser/releases" + ), + ), + ) + .mount(&server) + .await; + + // Mock page 2 + Mock::given(method("GET")) + .and(path("/repos/mullvad/mullvad-browser/releases")) + .and(query_param("per_page", "100")) + .and(query_param("page", "2")) + .respond_with( + ResponseTemplate::new(200) + .set_body_string(mock_page2) + .insert_header("content-type", "application/json"), + ) + .mount(&server) + .await; + + let result = client.fetch_mullvad_releases_with_caching(true).await; + + assert!(result.is_ok()); + let releases = result.unwrap(); + // We currently only fetch 1 page intentionally; ensure we at least got page 1 + assert_eq!( + releases.len(), + 1, + "Should fetch only the first page of results" + ); + assert_eq!(releases[0].tag_name, "100.0"); + } + #[test] fn test_camoufox_beta_version_parsing() { // Test specific Camoufox beta versions that are causing issues diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 0ae9706..755f5ad 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -38,20 +38,6 @@ impl BrowserRunner { &BROWSER_RUNNER } - // Start periodic cleanup of dead proxies - #[allow(dead_code)] - pub fn start_proxy_cleanup_task(&self, app_handle: tauri::AppHandle) { - tokio::spawn(async move { - let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30)); - loop { - interval.tick().await; - if let Err(e) = PROXY_MANAGER.cleanup_dead_proxies(app_handle.clone()).await { - println!("Warning: Failed to cleanup dead proxies: {e}"); - } - } - }); - } - // Helper function to check if a process matches TOR/Mullvad browser fn is_tor_or_mullvad_browser( &self, @@ -1581,17 +1567,6 @@ pub async fn update_profile_proxy( .map_err(|e| format!("Failed to update profile: {e}")) } -#[tauri::command] -pub fn update_profile_version( - profile_name: String, - version: String, -) -> Result { - let profile_manager = ProfileManager::instance(); - profile_manager - .update_profile_version(&profile_name, &version) - .map_err(|e| format!("Failed to update profile version: {e}")) -} - #[tauri::command] pub async fn check_browser_status( app_handle: tauri::AppHandle, @@ -1802,17 +1777,6 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result Result { - let service = BrowserVersionManager::instance(); - service - .get_browser_release_types(&browser_str) - .await - .map_err(|e| format!("Failed to get release types: {e}")) -} - #[tauri::command] pub async fn check_missing_binaries() -> Result, String> { let browser_runner = BrowserRunner::instance(); diff --git a/src-tauri/src/browser_version_manager.rs b/src-tauri/src/browser_version_manager.rs index 238044b..cd46f56 100644 --- a/src-tauri/src/browser_version_manager.rs +++ b/src-tauri/src/browser_version_manager.rs @@ -116,7 +116,16 @@ impl BrowserVersionManager { /// Get cached browser versions immediately (returns None if no cache exists) pub fn get_cached_browser_versions(&self, browser: &str) -> Option> { - self.api_client.load_cached_versions(browser) + if browser == "brave" { + return ApiClient::instance() + .get_cached_github_releases("brave") + .map(|releases| releases.into_iter().map(|r| r.tag_name).collect()); + } + + self + .api_client + .load_cached_versions(browser) + .map(|releases| releases.into_iter().map(|r| r.version).collect()) } /// Get cached detailed browser version information immediately @@ -124,17 +133,29 @@ impl BrowserVersionManager { &self, browser: &str, ) -> Option> { - let cached_versions = self.api_client.load_cached_versions(browser)?; + if browser == "brave" { + if let Some(releases) = ApiClient::instance().get_cached_github_releases("brave") { + let detailed_info: Vec = releases + .into_iter() + .map(|r| BrowserVersionInfo { + version: r.tag_name, + is_prerelease: r.is_nightly, + date: r.published_at, + }) + .collect(); + return Some(detailed_info); + } + } + + let cached_releases = self.api_client.load_cached_versions(browser)?; // Convert cached versions to detailed info (without dates since cache doesn't store them) - let detailed_info: Vec = cached_versions + let detailed_info: Vec = cached_releases .into_iter() - .map(|version| { - BrowserVersionInfo { - version: version.clone(), - is_prerelease: crate::api_client::is_browser_version_nightly(browser, &version, None), - date: "".to_string(), // Cache doesn't store dates - } + .map(|r| BrowserVersionInfo { + version: r.version, + is_prerelease: r.is_prerelease, + date: r.date, }) .collect(); @@ -153,15 +174,6 @@ impl BrowserVersionManager { ) -> Result> { // Try to get from cache first if let Some(cached_versions) = self.get_cached_browser_versions_detailed(browser) { - // For Chromium, only return stable since all releases are stable - if browser == "chromium" { - let latest_stable = cached_versions.first().map(|v| v.version.clone()); - return Ok(BrowserReleaseTypes { - stable: latest_stable, - nightly: None, - }); - } - let latest_stable = cached_versions .iter() .find(|v| !v.is_prerelease) @@ -178,17 +190,6 @@ impl BrowserVersionManager { }); } - // Fallback to fetching if not cached - // For Chromium, only return stable since all releases are stable - if browser == "chromium" { - let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?; - let latest_stable = detailed_versions.first().map(|v| v.version.clone()); - return Ok(BrowserReleaseTypes { - stable: latest_stable, - nightly: None, - }); - } - let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?; let latest_stable = detailed_versions @@ -230,7 +231,7 @@ impl BrowserVersionManager { .api_client .load_cached_versions(browser) .unwrap_or_default(); - let existing_set: HashSet = existing_versions.into_iter().collect(); + let existing_set: HashSet = existing_versions.into_iter().map(|r| r.version).collect(); // Fetch fresh versions from API let fresh_versions = match browser { @@ -262,10 +263,18 @@ impl BrowserVersionManager { crate::api_client::sort_versions(&mut merged_versions); // Save the merged cache (unless explicitly bypassing cache) - if !no_caching { + if !no_caching && browser != "brave" { + let merged_releases: Vec = merged_versions + .iter() + .map(|v| BrowserRelease { + version: v.clone(), + date: "".to_string(), + is_prerelease: crate::api_client::is_browser_version_nightly(browser, v, None), + }) + .collect(); if let Err(e) = self .api_client - .save_cached_versions(browser, &merged_versions) + .save_cached_versions(browser, &merged_releases) { eprintln!("Failed to save merged cache for {browser}: {e}"); } @@ -498,7 +507,7 @@ impl BrowserVersionManager { .api_client .load_cached_versions(browser) .unwrap_or_default(); - let existing_set: HashSet = existing_versions.into_iter().collect(); + let existing_set: HashSet = existing_versions.into_iter().map(|r| r.version).collect(); // Fetch new versions (always bypass cache for background updates) let new_versions = self.fetch_browser_versions(browser, true).await?; @@ -515,7 +524,15 @@ impl BrowserVersionManager { sort_versions(&mut all_versions); // Save the updated cache - if let Err(e) = self.api_client.save_cached_versions(browser, &all_versions) { + let releases: Vec = all_versions + .iter() + .map(|v| BrowserRelease { + version: v.clone(), + date: "".to_string(), + is_prerelease: crate::api_client::is_browser_version_nightly(browser, v, None), + }) + .collect(); + if let Err(e) = self.api_client.save_cached_versions(browser, &releases) { eprintln!("Failed to save updated cache for {browser}: {e}"); } @@ -893,6 +910,20 @@ impl BrowserVersionManager { no_caching: bool, ) -> Result, Box> { let releases = self.fetch_brave_releases_detailed(no_caching).await?; + // Persist a lightweight versions cache with accurate prerelease info for Brave + let converted: Vec = releases + .iter() + .map(|r| BrowserRelease { + version: r.tag_name.clone(), + date: r.published_at.clone(), + is_prerelease: r.is_nightly, + }) + .collect(); + // Always save so that other callers without release_name can classify correctly + if let Err(e) = self.api_client.save_cached_versions("brave", &converted) { + eprintln!("Failed to persist Brave versions cache: {e}"); + } + Ok(releases.into_iter().map(|r| r.tag_name).collect()) } @@ -900,10 +931,25 @@ impl BrowserVersionManager { &self, no_caching: bool, ) -> Result, Box> { - self + let releases = self .api_client .fetch_brave_releases_with_caching(no_caching) - .await + .await?; + + // Save a parallel versions cache for Brave with accurate prerelease flags + let converted: Vec = releases + .iter() + .map(|r| BrowserRelease { + version: r.tag_name.clone(), + date: r.published_at.clone(), + is_prerelease: r.is_nightly, + }) + .collect(); + if let Err(e) = self.api_client.save_cached_versions("brave", &converted) { + eprintln!("Failed to persist Brave versions cache: {e}"); + } + + Ok(releases) } async fn fetch_chromium_versions( @@ -961,6 +1007,17 @@ impl BrowserVersionManager { } } +#[tauri::command] +pub async fn get_browser_release_types( + browser_str: String, +) -> Result { + let service = BrowserVersionManager::instance(); + service + .get_browser_release_types(&browser_str) + .await + .map_err(|e| format!("Failed to get release types: {e}")) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/camoufox.rs b/src-tauri/src/camoufox.rs index 3274ebe..98fa908 100644 --- a/src-tauri/src/camoufox.rs +++ b/src-tauri/src/camoufox.rs @@ -81,19 +81,6 @@ impl CamoufoxNodecarLauncher { &CAMOUFOX_NODECAR_LAUNCHER } - /// Create a test configuration - #[allow(dead_code)] - pub fn create_test_config() -> CamoufoxConfig { - CamoufoxConfig { - screen_max_width: Some(1440), - screen_max_height: Some(900), - screen_min_width: Some(800), - screen_min_height: Some(600), - geoip: Some(serde_json::Value::Bool(true)), - ..Default::default() - } - } - /// Generate Camoufox fingerprint configuration during profile creation pub async fn generate_fingerprint_config( &self, @@ -484,18 +471,6 @@ impl CamoufoxNodecarLauncher { mod tests { use super::*; - #[test] - fn test_camoufox_config_creation() { - let test_config = CamoufoxNodecarLauncher::create_test_config(); - - // Verify test config has expected values - assert_eq!(test_config.screen_max_width, Some(1440)); - assert_eq!(test_config.screen_max_height, Some(900)); - assert_eq!(test_config.screen_min_width, Some(800)); - assert_eq!(test_config.screen_min_height, Some(600)); - assert_eq!(test_config.geoip, Some(serde_json::Value::Bool(true))); - } - #[test] fn test_default_config() { let default_config = CamoufoxConfig::default(); diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index e7c1953..7c3b519 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -1138,34 +1138,6 @@ impl Extractor { false } - /// Set executable permissions on Unix-like systems for extracted binaries - #[cfg(unix)] - #[allow(dead_code)] - async fn set_executable_permissions( - &self, - path: &Path, - ) -> Result<(), Box> { - use std::os::unix::fs::PermissionsExt; - - if path.exists() { - let mut permissions = path.metadata()?.permissions(); - // Set executable permissions for owner, group, and others if they have read permission - let current_mode = permissions.mode(); - let new_mode = current_mode | 0o111; // Add execute permission - permissions.set_mode(new_mode); - std::fs::set_permissions(path, permissions)?; - } - Ok(()) - } - - #[cfg(not(unix))] - async fn set_executable_permissions( - &self, - _path: &Path, - ) -> Result<(), Box> { - Ok(()) - } - /// Set executable permissions recursively for all files in a directory #[cfg(unix)] async fn set_executable_permissions_recursive( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6a930d0..287b65f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -34,10 +34,10 @@ use browser_runner::{ check_browser_exists, check_browser_status, check_missing_binaries, check_missing_geoip_database, create_browser_profile_new, delete_profile, download_browser, ensure_all_binaries_exist, fetch_browser_versions_cached_first, fetch_browser_versions_with_count, - fetch_browser_versions_with_count_cached_first, get_browser_release_types, - get_downloaded_browser_versions, get_supported_browsers, is_browser_supported_on_platform, - kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile, - update_camoufox_config, update_profile_proxy, update_profile_version, + fetch_browser_versions_with_count_cached_first, get_downloaded_browser_versions, + get_supported_browsers, is_browser_supported_on_platform, kill_browser_profile, + launch_browser_profile, list_browser_profiles, rename_profile, update_camoufox_config, + update_profile_proxy, }; use settings_manager::{ @@ -71,6 +71,8 @@ use group_manager::{ use geoip_downloader::GeoIPDownloader; +use browser_version_manager::get_browser_release_types; + // Trait to extend WebviewWindow with transparent titlebar functionality pub trait WindowExt { #[cfg(target_os = "macos")] @@ -506,7 +508,6 @@ pub fn run() { get_downloaded_browser_versions, get_browser_release_types, update_profile_proxy, - update_profile_version, check_browser_status, kill_browser_profile, rename_profile, diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index e1db147..c09b9b2 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -51,14 +51,11 @@ impl ProfileImporter { // Detect Chromium profiles detected_profiles.extend(self.detect_chromium_profiles()?); - // Detect Mullvad Browser profiles - detected_profiles.extend(self.detect_mullvad_browser_profiles()?); - // Detect Zen Browser profiles detected_profiles.extend(self.detect_zen_browser_profiles()?); - // Detect TOR Browser profiles - detected_profiles.extend(self.detect_tor_browser_profiles()?); + // NOTE: Mullvad and Tor Browser profile imports are no longer supported. + // We intentionally do not detect these profiles to avoid offering them in the UI. // Remove duplicates based on path let mut seen_paths = HashSet::new(); @@ -242,45 +239,6 @@ impl ProfileImporter { Ok(profiles) } - /// Detect Mullvad Browser profiles - fn detect_mullvad_browser_profiles( - &self, - ) -> Result, Box> { - let mut profiles = Vec::new(); - - #[cfg(target_os = "macos")] - { - let mullvad_dir = self - .base_dirs - .home_dir() - .join("Library/Application Support/MullvadBrowser/Profiles"); - profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?); - } - - #[cfg(target_os = "windows")] - { - // Primary location in AppData\Roaming - let app_data = self.base_dirs.data_dir(); - let mullvad_dir = app_data.join("MullvadBrowser/Profiles"); - profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?); - - // Also check common installation locations - let local_app_data = self.base_dirs.data_local_dir(); - let mullvad_local_dir = local_app_data.join("MullvadBrowser/Profiles"); - if mullvad_local_dir.exists() { - profiles.extend(self.scan_firefox_profiles_dir(&mullvad_local_dir, "mullvad-browser")?); - } - } - - #[cfg(target_os = "linux")] - { - let mullvad_dir = self.base_dirs.home_dir().join(".mullvad-browser"); - profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?); - } - - Ok(profiles) - } - /// Detect Zen Browser profiles fn detect_zen_browser_profiles( &self, @@ -312,107 +270,6 @@ impl ProfileImporter { Ok(profiles) } - /// Detect TOR Browser profiles - fn detect_tor_browser_profiles( - &self, - ) -> Result, Box> { - let mut profiles = Vec::new(); - - #[cfg(target_os = "macos")] - { - // TOR Browser on macOS is typically in Applications - let tor_dir = self - .base_dirs - .home_dir() - .join("Library/Application Support/TorBrowser-Data/Browser/profile.default"); - - if tor_dir.exists() { - profiles.push(DetectedProfile { - browser: "tor-browser".to_string(), - name: "TOR Browser - Default Profile".to_string(), - path: tor_dir.to_string_lossy().to_string(), - description: "Default TOR Browser profile".to_string(), - }); - } - } - - #[cfg(target_os = "windows")] - { - // Check common TOR Browser installation locations on Windows - let possible_paths = [ - // Default installation in user directory - ( - "Desktop", - "Desktop/Tor Browser/Browser/TorBrowser/Data/Browser/profile.default", - ), - // AppData locations - ( - "AppData/Roaming", - "TorBrowser/Browser/TorBrowser/Data/Browser/profile.default", - ), - ( - "AppData/Local", - "TorBrowser/Browser/TorBrowser/Data/Browser/profile.default", - ), - ]; - - let home_dir = self.base_dirs.home_dir(); - - for (location_name, relative_path) in &possible_paths { - let tor_dir = home_dir.join(relative_path); - if tor_dir.exists() { - profiles.push(DetectedProfile { - browser: "tor-browser".to_string(), - name: format!("TOR Browser - {} Profile", location_name), - path: tor_dir.to_string_lossy().to_string(), - description: format!("TOR Browser profile from {}", location_name), - }); - } - } - - // Also check AppData directories if available - let app_data = self.base_dirs.data_dir(); - let tor_app_data = - app_data.join("TorBrowser/Browser/TorBrowser/Data/Browser/profile.default"); - if tor_app_data.exists() { - profiles.push(DetectedProfile { - browser: "tor-browser".to_string(), - name: "TOR Browser - AppData Profile".to_string(), - path: tor_app_data.to_string_lossy().to_string(), - description: "TOR Browser profile from AppData".to_string(), - }); - } - } - - #[cfg(target_os = "linux")] - { - // Common TOR Browser locations on Linux - let possible_paths = [ - ".local/share/torbrowser/tbb/x86_64/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default", - "tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default", - ".tor-browser/Browser/TorBrowser/Data/Browser/profile.default", - "Downloads/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default", - ]; - - let home_dir = self.base_dirs.home_dir(); - - for relative_path in &possible_paths { - let tor_dir = home_dir.join(relative_path); - if tor_dir.exists() { - profiles.push(DetectedProfile { - browser: "tor-browser".to_string(), - name: "TOR Browser - Default Profile".to_string(), - path: tor_dir.to_string_lossy().to_string(), - description: "TOR Browser profile".to_string(), - }); - break; // Only add the first one found to avoid duplicates - } - } - } - - Ok(profiles) - } - /// Scan Firefox-style profiles directory fn scan_firefox_profiles_dir( &self, @@ -647,6 +504,11 @@ impl ProfileImporter { browser_type: &str, new_profile_name: &str, ) -> Result<(), Box> { + // Disable imports for Mullvad and Tor browsers + if browser_type == "mullvad-browser" || browser_type == "tor-browser" { + return Err("Importing Mullvad Browser or Tor Browser profiles is not supported".into()); + } + // Validate that source path exists let source_path = Path::new(source_path); if !source_path.exists() { diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index 3d2a553..e78a977 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -467,13 +467,6 @@ impl ProxyManager { Ok(dead_pids) } - - // Get all active proxy PIDs for monitoring - #[allow(dead_code)] - pub fn get_active_proxy_pids(&self) -> Vec { - let proxies = self.active_proxies.lock().unwrap(); - proxies.keys().copied().collect() - } } // Create a singleton instance of the proxy manager diff --git a/src-tauri/src/version_updater.rs b/src-tauri/src/version_updater.rs index 640880c..93f18f7 100644 --- a/src-tauri/src/version_updater.rs +++ b/src-tauri/src/version_updater.rs @@ -41,7 +41,7 @@ impl Default for BackgroundUpdateState { fn default() -> Self { Self { last_update_time: 0, - update_interval_hours: 3, + update_interval_hours: 12, } } } @@ -642,20 +642,6 @@ mod tests { assert!(timestamp2 >= timestamp1, "Timestamp should not decrease"); } - #[test] - fn test_background_update_state_default() { - let state = BackgroundUpdateState::default(); - - assert_eq!( - state.last_update_time, 0, - "Default last update time should be 0" - ); - assert_eq!( - state.update_interval_hours, 3, - "Default update interval should be 3 hours" - ); - } - #[test] fn test_get_version_updater_singleton() { let updater1 = get_version_updater(); diff --git a/src-tauri/tests/common/mod.rs b/src-tauri/tests/common/mod.rs index edd0f34..2641435 100644 --- a/src-tauri/tests/common/mod.rs +++ b/src-tauri/tests/common/mod.rs @@ -52,26 +52,6 @@ impl TestUtils { Ok(nodecar_binary) } - /// Get the appropriate build target for the current platform - #[allow(dead_code)] - fn get_build_target() -> &'static str { - if cfg!(target_arch = "aarch64") && cfg!(target_os = "macos") { - "build:mac-aarch64" - } else if cfg!(target_arch = "x86_64") && cfg!(target_os = "macos") { - "build:mac-x86_64" - } else if cfg!(target_arch = "x86_64") && cfg!(target_os = "linux") { - "build:linux-x64" - } else if cfg!(target_arch = "aarch64") && cfg!(target_os = "linux") { - "build:linux-arm64" - } else if cfg!(target_arch = "x86_64") && cfg!(target_os = "windows") { - "build:win-x64" - } else if cfg!(target_arch = "aarch64") && cfg!(target_os = "windows") { - "build:win-arm64" - } else { - panic!("Unsupported target architecture for nodecar build") - } - } - /// Execute a nodecar command with timeout pub async fn execute_nodecar_command( binary_path: &PathBuf, @@ -150,58 +130,4 @@ impl TestUtils { println!("Test process cleanup completed"); Ok(()) } - - /// Clean up all running nodecar processes (proxies and camoufox instances) - /// WARNING: This will stop ALL processes, including those from actual app usage - #[allow(dead_code)] - pub async fn cleanup_all_nodecar_processes( - nodecar_path: &PathBuf, - ) -> Result<(), Box> { - println!("WARNING: Cleaning up ALL nodecar processes..."); - - // Get list of all proxies and stop them individually - let proxy_list_args = ["proxy", "list"]; - if let Ok(list_output) = Self::execute_nodecar_command(nodecar_path, &proxy_list_args).await { - if list_output.status.success() { - let list_stdout = String::from_utf8(list_output.stdout)?; - if let Ok(proxies) = serde_json::from_str::(&list_stdout) { - if let Some(proxy_array) = proxies.as_array() { - for proxy in proxy_array { - if let Some(proxy_id) = proxy["id"].as_str() { - let stop_args = ["proxy", "stop", "--id", proxy_id]; - let _ = Self::execute_nodecar_command(nodecar_path, &stop_args).await; - println!("Stopped proxy: {proxy_id}"); - } - } - } - } - } - } - - // Get list of all camoufox instances and stop them individually - let camoufox_list_args = ["camoufox", "list"]; - if let Ok(list_output) = Self::execute_nodecar_command(nodecar_path, &camoufox_list_args).await - { - if list_output.status.success() { - let list_stdout = String::from_utf8(list_output.stdout)?; - if let Ok(instances) = serde_json::from_str::(&list_stdout) { - if let Some(instance_array) = instances.as_array() { - for instance in instance_array { - if let Some(instance_id) = instance["id"].as_str() { - let stop_args = ["camoufox", "stop", "--id", instance_id]; - let _ = Self::execute_nodecar_command(nodecar_path, &stop_args).await; - println!("Stopped camoufox instance: {instance_id}"); - } - } - } - } - } - } - - // Give processes time to clean up - tokio::time::sleep(Duration::from_secs(2)).await; - - println!("Nodecar process cleanup completed"); - Ok(()) - } } diff --git a/src/app/page.tsx b/src/app/page.tsx index 790bb0a..8070434 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,7 +5,6 @@ import { listen } from "@tauri-apps/api/event"; import { getCurrent } from "@tauri-apps/plugin-deep-link"; import { useCallback, useEffect, useRef, useState } from "react"; import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog"; -import { ChangeVersionDialog } from "@/components/change-version-dialog"; import { CreateProfileDialog } from "@/components/create-profile-dialog"; import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; import { GroupAssignmentDialog } from "@/components/group-assignment-dialog"; @@ -46,7 +45,6 @@ export default function Home() { const [error, setError] = useState(null); const [proxyDialogOpen, setProxyDialogOpen] = useState(false); const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false); - const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const [settingsDialogOpen, setSettingsDialogOpen] = useState(false); const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false); const [proxyManagementDialogOpen, setProxyManagementDialogOpen] = @@ -65,8 +63,6 @@ export default function Home() { const [pendingUrls, setPendingUrls] = useState([]); const [currentProfileForProxy, setCurrentProfileForProxy] = useState(null); - const [currentProfileForVersionChange, setCurrentProfileForVersionChange] = - useState(null); const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] = useState(null); const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false); @@ -358,11 +354,6 @@ export default function Home() { setProxyDialogOpen(true); }, []); - const openChangeVersionDialog = useCallback((profile: BrowserProfile) => { - setCurrentProfileForVersionChange(profile); - setChangeVersionDialogOpen(true); - }, []); - const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => { setCurrentProfileForCamoufoxConfig(profile); setCamoufoxConfigDialogOpen(true); @@ -797,7 +788,6 @@ export default function Home() { onProxySettings={openProxyDialog} onDeleteProfile={handleDeleteProfile} onRenameProfile={handleRenameProfile} - onChangeVersion={openChangeVersionDialog} onConfigureCamoufox={handleConfigureCamoufox} runningProfiles={runningProfiles} isUpdating={isUpdating} @@ -836,15 +826,6 @@ export default function Home() { }} /> - { - setChangeVersionDialogOpen(false); - }} - profile={currentProfileForVersionChange} - onVersionChanged={() => void loadProfiles()} - /> - { diff --git a/src/components/change-version-dialog.tsx b/src/components/change-version-dialog.tsx deleted file mode 100644 index bf99923..0000000 --- a/src/components/change-version-dialog.tsx +++ /dev/null @@ -1,274 +0,0 @@ -"use client"; - -import { invoke } from "@tauri-apps/api/core"; -import { useCallback, useEffect, useState } from "react"; -import { LoadingButton } from "@/components/loading-button"; -import { ReleaseTypeSelector } from "@/components/release-type-selector"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Label } from "@/components/ui/label"; -import { useBrowserDownload } from "@/hooks/use-browser-download"; -import { getBrowserDisplayName } from "@/lib/browser-utils"; -import type { BrowserProfile, BrowserReleaseTypes } from "@/types"; -import { RippleButton } from "./ui/ripple"; - -interface ChangeVersionDialogProps { - isOpen: boolean; - onClose: () => void; - profile: BrowserProfile | null; - onVersionChanged: () => void; -} - -export function ChangeVersionDialog({ - isOpen, - onClose, - profile, - onVersionChanged, -}: ChangeVersionDialogProps) { - const [selectedReleaseType, setSelectedReleaseType] = useState< - "stable" | "nightly" | null - >(null); - const [releaseTypes, setReleaseTypes] = useState({}); - const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); - // Nightly switching is disabled for non-nightly profiles (except Firefox Developer), - // so downgrade warnings are no longer applicable. - - const { - downloadedVersions, - isBrowserDownloading, - loadDownloadedVersions, - downloadBrowser, - isVersionDownloaded, - } = useBrowserDownload(); - - const loadReleaseTypes = useCallback( - async (browser: string) => { - setIsLoadingReleaseTypes(true); - try { - const releaseTypes = await invoke( - "get_browser_release_types", - { browserStr: browser }, - ); - // Filter nightly visibility based on rules: - // - Firefox Developer Edition: allow nightly only - // - If profile is currently nightly: allow both stable and nightly - // - Otherwise: allow stable only - const filtered: BrowserReleaseTypes = {}; - if (profile?.browser === "firefox-developer") { - if (releaseTypes.nightly) filtered.nightly = releaseTypes.nightly; - } else if (profile?.release_type === "nightly") { - if (releaseTypes.stable) filtered.stable = releaseTypes.stable; - if (releaseTypes.nightly) filtered.nightly = releaseTypes.nightly; - } else { - if (releaseTypes.stable) filtered.stable = releaseTypes.stable; - } - setReleaseTypes(filtered); - } catch (error) { - console.error("Failed to load release types:", error); - } finally { - setIsLoadingReleaseTypes(false); - } - }, - [profile?.browser, profile?.release_type], - ); - - const handleDownload = useCallback(async () => { - if (!profile || !selectedReleaseType) return; - - const version = - selectedReleaseType === "stable" - ? releaseTypes.stable - : releaseTypes.nightly; - if (!version) return; - - await downloadBrowser(profile.browser, version); - }, [profile, selectedReleaseType, downloadBrowser, releaseTypes]); - - const handleVersionChange = useCallback(async () => { - if (!profile || !selectedReleaseType) return; - - const version = - selectedReleaseType === "stable" - ? releaseTypes.stable - : releaseTypes.nightly; - if (!version) return; - - setIsUpdating(true); - try { - await invoke("update_profile_version", { - profileName: profile.name, - version, - }); - onVersionChanged(); - onClose(); - } catch (error) { - console.error("Failed to update profile version:", error); - } finally { - setIsUpdating(false); - } - }, [profile, selectedReleaseType, releaseTypes, onVersionChanged, onClose]); - - const selectedVersion = - selectedReleaseType === "stable" - ? releaseTypes.stable - : releaseTypes.nightly; - - const canUpdate = - profile && - selectedReleaseType && - selectedReleaseType !== profile.release_type && - selectedVersion && - isVersionDownloaded(selectedVersion); - - useEffect(() => { - if (isOpen && profile) { - // Set current release type based on profile - setSelectedReleaseType(profile.release_type as "stable" | "nightly"); - void loadReleaseTypes(profile.browser); - void loadDownloadedVersions(profile.browser); - } - }, [isOpen, profile, loadDownloadedVersions, loadReleaseTypes]); - - if (!profile) return null; - - return ( - - - - Change Release Type - - -
-
- -
{profile.name}
-
- -
- -
- {profile.release_type} ({profile.version}) -
-
- - {!releaseTypes.stable && !releaseTypes.nightly ? ( - - - No releases are available for{" "} - {getBrowserDisplayName(profile.browser)}. - - - ) : !releaseTypes.stable || !releaseTypes.nightly ? ( -
- - - Only {profile.release_type} releases are available for{" "} - {getBrowserDisplayName(profile.browser)}. - - -
- - {isLoadingReleaseTypes ? ( -
- Loading release types... -
- ) : ( -
- {selectedReleaseType && - selectedReleaseType !== profile.release_type && - selectedVersion && - !isVersionDownloaded(selectedVersion) && ( - - - You must download{" "} - {getBrowserDisplayName(profile.browser)}{" "} - {selectedVersion} before switching to this release - type. Use the download button above to get the - latest version. - - - )} - - { - void handleDownload(); - }} - placeholder="Select release type..." - downloadedVersions={downloadedVersions} - /> -
- )} -
-
- ) : ( -
- - {isLoadingReleaseTypes ? ( -
- Loading release types... -
- ) : ( -
- {selectedReleaseType && - selectedReleaseType !== profile.release_type && - selectedVersion && - !isVersionDownloaded(selectedVersion) && ( - - - You must download{" "} - {getBrowserDisplayName(profile.browser)}{" "} - {selectedVersion} before switching to this release - type. Use the download button above to get the latest - version. - - - )} - - { - void handleDownload(); - }} - placeholder="Select release type..." - downloadedVersions={downloadedVersions} - /> -
- )} -
- )} - - {/* Nightly switching disabled in UI; no downgrade warning needed. */} -
- - - - Cancel - - { - void handleVersionChange(); - }} - disabled={!canUpdate} - > - {isUpdating ? "Updating..." : "Update Release Type"} - - -
-
- ); -} diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index 83b686f..de16826 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -58,44 +58,36 @@ interface CreateProfileDialogProps { interface BrowserOption { value: BrowserTypeString; label: string; - description: string; } const browserOptions: BrowserOption[] = [ { value: "firefox", label: "Firefox", - description: "Mozilla's main web browser", }, { value: "firefox-developer", label: "Firefox Developer Edition", - description: "Browser for developers with cutting-edge features", }, { value: "chromium", label: "Chromium", - description: "Open-source version of Chrome", }, { value: "brave", label: "Brave", - description: "Privacy-focused browser with ad blocking", }, { value: "zen", label: "Zen Browser", - description: "Beautiful, customizable Firefox-based browser", }, { value: "mullvad-browser", label: "Mullvad Browser", - description: "TOR Browser fork by Mullvad VPN", }, { value: "tor-browser", label: "Tor Browser", - description: "Browse anonymously through the Tor network", }, ]; @@ -128,15 +120,11 @@ export function CreateProfileDialog({ geoip: true, // Default to automatic geoip }); - // Common states - const [availableReleaseTypes, setAvailableReleaseTypes] = - useState({}); - const [camoufoxReleaseTypes, setCamoufoxReleaseTypes] = - useState({}); const [supportedBrowsers, setSupportedBrowsers] = useState([]); const [storedProxies, setStoredProxies] = useState([]); const [showProxyForm, setShowProxyForm] = useState(false); const [isCreating, setIsCreating] = useState(false); + const [releaseTypes, setReleaseTypes] = useState(); const loadingBrowserRef = useRef(null); // Use the browser download hook @@ -185,7 +173,7 @@ export function CreateProfileDialog({ loadingBrowserRef.current = browser; try { - const releaseTypes = await invoke( + const rawReleaseTypes = await invoke( "get_browser_release_types", { browserStr: browser }, ); @@ -195,16 +183,19 @@ export function CreateProfileDialog({ // Filter to enforce stable-only creation, except Firefox Developer (nightly-only) if (browser === "camoufox") { const filtered: BrowserReleaseTypes = {}; - if (releaseTypes.stable) filtered.stable = releaseTypes.stable; - setCamoufoxReleaseTypes(filtered); + if (rawReleaseTypes.stable) + filtered.stable = rawReleaseTypes.stable; + setReleaseTypes(filtered); } else if (browser === "firefox-developer") { const filtered: BrowserReleaseTypes = {}; - if (releaseTypes.nightly) filtered.nightly = releaseTypes.nightly; - setAvailableReleaseTypes(filtered); + if (rawReleaseTypes.nightly) + filtered.nightly = rawReleaseTypes.nightly; + setReleaseTypes(filtered); } else { const filtered: BrowserReleaseTypes = {}; - if (releaseTypes.stable) filtered.stable = releaseTypes.stable; - setAvailableReleaseTypes(filtered); + if (rawReleaseTypes.stable) + filtered.stable = rawReleaseTypes.stable; + setReleaseTypes(filtered); } // Load downloaded versions for this browser @@ -246,14 +237,16 @@ export function CreateProfileDialog({ // Cancel any previous loading loadingBrowserRef.current = null; // Clear previous release types immediately to prevent showing stale data - setAvailableReleaseTypes({}); + setReleaseTypes({}); void loadReleaseTypes(selectedBrowser); } }, [selectedBrowser, loadReleaseTypes]); // Helper function to get the best available version respecting rules const getBestAvailableVersion = useCallback( - (releaseTypes: BrowserReleaseTypes, browserType?: string) => { + (browserType?: string) => { + if (!releaseTypes) return null; + // Firefox Developer Edition: nightly-only if (browserType === "firefox-developer" && releaseTypes.nightly) { return { @@ -267,13 +260,11 @@ export function CreateProfileDialog({ } return null; }, - [], + [releaseTypes], ); const handleDownload = async (browserStr: string) => { - const releaseTypes = - browserStr === "camoufox" ? camoufoxReleaseTypes : availableReleaseTypes; - const bestVersion = getBestAvailableVersion(releaseTypes, browserStr); + const bestVersion = getBestAvailableVersion(browserStr); if (!bestVersion) { console.error("No version available for download"); @@ -299,10 +290,7 @@ export function CreateProfileDialog({ } // Use the best available version (stable preferred, nightly as fallback) - const bestVersion = getBestAvailableVersion( - availableReleaseTypes, - selectedBrowser, - ); + const bestVersion = getBestAvailableVersion(selectedBrowser); if (!bestVersion) { console.error("No version available"); return; @@ -318,10 +306,7 @@ export function CreateProfileDialog({ }); } else { // Anti-detect tab - always use Camoufox with best available version - const bestCamoufoxVersion = getBestAvailableVersion( - camoufoxReleaseTypes, - "camoufox", - ); + const bestCamoufoxVersion = getBestAvailableVersion("camoufox"); if (!bestCamoufoxVersion) { console.error("No Camoufox version available"); return; @@ -358,8 +343,7 @@ export function CreateProfileDialog({ setProfileName(""); setSelectedBrowser(null); setSelectedProxyId(undefined); - setAvailableReleaseTypes({}); - setCamoufoxReleaseTypes({}); + setReleaseTypes({}); setCamoufoxConfig({ geoip: true, // Reset to automatic geoip }); @@ -374,19 +358,10 @@ export function CreateProfileDialog({ // Check if browser version is downloaded and available const isBrowserVersionAvailable = useCallback( (browserStr: string) => { - const releaseTypes = - browserStr === "camoufox" - ? camoufoxReleaseTypes - : availableReleaseTypes; - const bestVersion = getBestAvailableVersion(releaseTypes, browserStr); + const bestVersion = getBestAvailableVersion(browserStr); return bestVersion && isVersionDownloaded(bestVersion.version); }, - [ - camoufoxReleaseTypes, - availableReleaseTypes, - isVersionDownloaded, - getBestAvailableVersion, - ], + [isVersionDownloaded, getBestAvailableVersion], ); // Check if browser is currently downloading @@ -411,6 +386,20 @@ export function CreateProfileDialog({ isBrowserVersionAvailable, ]); + useEffect(() => { + console.log( + selectedBrowser, + selectedBrowser && isBrowserCurrentlyDownloading(selectedBrowser), + selectedBrowser && isBrowserVersionAvailable(selectedBrowser), + selectedBrowser && getBestAvailableVersion(selectedBrowser), + ); + }, [ + selectedBrowser, + isBrowserCurrentlyDownloading, + isBrowserVersionAvailable, + getBestAvailableVersion, + ]); + return ( @@ -478,17 +467,12 @@ export function CreateProfileDialog({
{!isBrowserCurrentlyDownloading(selectedBrowser) && !isBrowserVersionAvailable(selectedBrowser) && - getBestAvailableVersion( - availableReleaseTypes, - selectedBrowser, - ) && ( + getBestAvailableVersion(selectedBrowser) && (

{(() => { - const bestVersion = getBestAvailableVersion( - availableReleaseTypes, - selectedBrowser, - ); + const bestVersion = + getBestAvailableVersion(selectedBrowser); return `Latest version (${bestVersion?.version}) needs to be downloaded`; })()}

@@ -510,10 +494,8 @@ export function CreateProfileDialog({ isBrowserVersionAvailable(selectedBrowser) && (
{(() => { - const bestVersion = getBestAvailableVersion( - availableReleaseTypes, - selectedBrowser, - ); + const bestVersion = + getBestAvailableVersion(selectedBrowser); return `✓ Latest version (${bestVersion?.version}) is available`; })()}
@@ -521,10 +503,8 @@ export function CreateProfileDialog({ {isBrowserCurrentlyDownloading(selectedBrowser) && (
{(() => { - const bestVersion = getBestAvailableVersion( - availableReleaseTypes, - selectedBrowser, - ); + const bestVersion = + getBestAvailableVersion(selectedBrowser); return `Downloading version (${bestVersion?.version})...`; })()}
@@ -539,17 +519,12 @@ export function CreateProfileDialog({ {/* Camoufox Download Status */} {!isBrowserCurrentlyDownloading("camoufox") && !isBrowserVersionAvailable("camoufox") && - getBestAvailableVersion( - camoufoxReleaseTypes, - "camoufox", - ) && ( + getBestAvailableVersion("camoufox") && (

{(() => { - const bestVersion = getBestAvailableVersion( - camoufoxReleaseTypes, - "camoufox", - ); + const bestVersion = + getBestAvailableVersion("camoufox"); return `Camoufox version (${bestVersion?.version}) needs to be downloaded`; })()}

@@ -571,10 +546,8 @@ export function CreateProfileDialog({ isBrowserVersionAvailable("camoufox") && (
{(() => { - const bestVersion = getBestAvailableVersion( - camoufoxReleaseTypes, - "camoufox", - ); + const bestVersion = + getBestAvailableVersion("camoufox"); return `✓ Camoufox version (${bestVersion?.version}) is available`; })()}
@@ -582,10 +555,8 @@ export function CreateProfileDialog({ {isBrowserCurrentlyDownloading("camoufox") && (
{(() => { - const bestVersion = getBestAvailableVersion( - camoufoxReleaseTypes, - "camoufox", - ); + const bestVersion = + getBestAvailableVersion("camoufox"); return `Downloading Camoufox version (${bestVersion?.version})...`; })()}
diff --git a/src/components/import-profile-dialog.tsx b/src/components/import-profile-dialog.tsx index 076e1c4..5228e76 100644 --- a/src/components/import-profile-dialog.tsx +++ b/src/components/import-profile-dialog.tsx @@ -64,6 +64,11 @@ export function ImportProfileDialog({ const { supportedBrowsers, isLoading: isLoadingSupport } = useBrowserSupport(); + // Exclude browsers that are no longer supported for import + const importableBrowsers = supportedBrowsers.filter( + (b) => b !== "mullvad-browser" && b !== "tor-browser", + ); + const loadDetectedProfiles = useCallback(async () => { setIsLoading(true); try { @@ -410,7 +415,7 @@ export function ImportProfileDialog({ /> - {supportedBrowsers.map((browser) => { + {importableBrowsers.map((browser) => { const IconComponent = getBrowserIcon(browser); return ( diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 62069f6..9d06339 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -64,7 +64,6 @@ interface ProfilesDataTableProps { onProxySettings: (profile: BrowserProfile) => void; onDeleteProfile: (profile: BrowserProfile) => void | Promise; onRenameProfile: (oldName: string, newName: string) => Promise; - onChangeVersion: (profile: BrowserProfile) => void; onConfigureCamoufox?: (profile: BrowserProfile) => void; runningProfiles: Set; isUpdating: (browser: string) => boolean; @@ -83,7 +82,6 @@ export function ProfilesDataTable({ onProxySettings, onDeleteProfile, onRenameProfile, - onChangeVersion, onConfigureCamoufox, runningProfiles, isUpdating, @@ -764,18 +762,6 @@ export function ProfilesDataTable({ Configure Camoufox )} - {!["chromium", "zen", "camoufox"].includes( - profile.browser, - ) && ( - { - onChangeVersion(profile); - }} - disabled={isDisabled} - > - Switch Release - - )} { setProfileToRename(profile); @@ -815,7 +801,6 @@ export function ProfilesDataTable({ onLaunchProfile, onKillProfile, onConfigureCamoufox, - onChangeVersion, onAssignProfilesToGroup, isUpdating, launchingProfiles, diff --git a/src/components/release-type-selector.tsx b/src/components/release-type-selector.tsx index 3e811ac..515f3ba 100644 --- a/src/components/release-type-selector.tsx +++ b/src/components/release-type-selector.tsx @@ -35,7 +35,6 @@ export function ReleaseTypeSelector({ selectedReleaseType, onReleaseTypeSelect, availableReleaseTypes, - // browser prop removed; callers control availableReleaseTypes isDownloading, onDownload, placeholder = "Select release type...", @@ -44,8 +43,6 @@ export function ReleaseTypeSelector({ }: ReleaseTypeSelectorProps) { const [popoverOpen, setPopoverOpen] = useState(false); - // Nightly visibility is controlled by callers. This component will render - // whichever options are provided via availableReleaseTypes. const releaseOptions = [ ...(availableReleaseTypes.stable ? [{ type: "stable" as const, version: availableReleaseTypes.stable }]