refactor: cleanup and better brave release fetching

This commit is contained in:
zhom
2025-08-11 05:42:17 +04:00
parent b7b75ec3d8
commit d48e26c7eb
17 changed files with 413 additions and 1019 deletions
+1
View File
@@ -108,6 +108,7 @@
"pathex",
"pathlib",
"peerconnection",
"pids",
"pixbuf",
"plasmohq",
"platformdirs",
+249 -257
View File
@@ -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<String>,
releases: Vec<BrowserRelease>,
timestamp: u64,
}
@@ -327,6 +346,65 @@ impl ApiClient {
}
}
async fn fetch_github_releases_multiple_pages(
&self,
base_releases_url: &str,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
let mut all_releases: Vec<GithubRelease> = 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<GithubRelease> = 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<Vec<String>> {
pub fn load_cached_versions(&self, browser: &str) -> Option<Vec<BrowserRelease>> {
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::<CachedVersionData>(&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::<Vec<String>>(&content) {
println!("Using legacy cached versions for {browser}; upgrading in-memory");
let releases: Vec<BrowserRelease> = 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<dyn std::error::Error + Send + Sync>> {
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<Vec<GithubRelease>> {
self.load_cached_github_releases(browser)
}
fn save_cached_github_releases(
&self,
browser: &str,
@@ -475,19 +574,8 @@ impl ApiClient {
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
// 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<String> = 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<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
// 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<String> = 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<GithubRelease> = 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<GithubRelease> = 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<GithubRelease> = 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<GithubRelease> =
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<GithubRelease> = 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<GithubRelease> = 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<GithubRelease> = 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<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
// 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<BrowserRelease> = 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<GithubRelease> = 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<GithubRelease> = self.fetch_github_releases_multiple_pages(&base_url).await?;
println!(
"Fetched {} total Camoufox releases from GitHub",
@@ -1157,19 +1083,8 @@ impl ApiClient {
) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
// 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<BrowserRelease> = 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
-36
View File
@@ -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<BrowserProfile, String> {
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<Vec<String
Ok(registry.get_downloaded_versions(&browser_str))
}
#[tauri::command]
pub async fn get_browser_release_types(
browser_str: String,
) -> Result<crate::browser_version_manager::BrowserReleaseTypes, String> {
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<Vec<(String, String, String)>, String> {
let browser_runner = BrowserRunner::instance();
+93 -36
View File
@@ -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<Vec<String>> {
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<Vec<BrowserVersionInfo>> {
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<BrowserVersionInfo> = 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<BrowserVersionInfo> = cached_versions
let detailed_info: Vec<BrowserVersionInfo> = 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<BrowserReleaseTypes, Box<dyn std::error::Error + Send + Sync>> {
// 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<String> = existing_versions.into_iter().collect();
let existing_set: HashSet<String> = 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<BrowserRelease> = 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<String> = existing_versions.into_iter().collect();
let existing_set: HashSet<String> = 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<BrowserRelease> = 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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let releases = self.fetch_brave_releases_detailed(no_caching).await?;
// Persist a lightweight versions cache with accurate prerelease info for Brave
let converted: Vec<BrowserRelease> = 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<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
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<BrowserRelease> = 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<crate::browser_version_manager::BrowserReleaseTypes, String> {
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::*;
-25
View File
@@ -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();
-28
View File
@@ -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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
Ok(())
}
/// Set executable permissions recursively for all files in a directory
#[cfg(unix)]
async fn set_executable_permissions_recursive(
+6 -5
View File
@@ -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,
+7 -145
View File
@@ -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<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
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<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
// 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() {
-7
View File
@@ -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<u32> {
let proxies = self.active_proxies.lock().unwrap();
proxies.keys().copied().collect()
}
}
// Create a singleton instance of the proxy manager
+1 -15
View File
@@ -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();
-74
View File
@@ -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<dyn std::error::Error + Send + Sync>> {
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::<serde_json::Value>(&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::<serde_json::Value>(&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(())
}
}
-19
View File
@@ -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<string | null>(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<PendingUrl[]>([]);
const [currentProfileForProxy, setCurrentProfileForProxy] =
useState<BrowserProfile | null>(null);
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
useState<BrowserProfile | null>(null);
const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] =
useState<BrowserProfile | null>(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() {
}}
/>
<ChangeVersionDialog
isOpen={changeVersionDialogOpen}
onClose={() => {
setChangeVersionDialogOpen(false);
}}
profile={currentProfileForVersionChange}
onVersionChanged={() => void loadProfiles()}
/>
<ImportProfileDialog
isOpen={importProfileDialogOpen}
onClose={() => {
-274
View File
@@ -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<BrowserReleaseTypes>({});
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<BrowserReleaseTypes>(
"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 (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Change Release Type</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label className="text-sm font-medium">Profile:</Label>
<div className="p-2 text-sm rounded bg-muted">{profile.name}</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">Current Release:</Label>
<div className="p-2 text-sm capitalize rounded bg-muted">
{profile.release_type} ({profile.version})
</div>
</div>
{!releaseTypes.stable && !releaseTypes.nightly ? (
<Alert>
<AlertDescription>
No releases are available for{" "}
{getBrowserDisplayName(profile.browser)}.
</AlertDescription>
</Alert>
) : !releaseTypes.stable || !releaseTypes.nightly ? (
<div className="space-y-4">
<Alert>
<AlertDescription>
Only {profile.release_type} releases are available for{" "}
{getBrowserDisplayName(profile.browser)}.
</AlertDescription>
</Alert>
<div className="grid gap-2">
<Label>New Release Type</Label>
{isLoadingReleaseTypes ? (
<div className="text-sm text-muted-foreground">
Loading release types...
</div>
) : (
<div className="space-y-4">
{selectedReleaseType &&
selectedReleaseType !== profile.release_type &&
selectedVersion &&
!isVersionDownloaded(selectedVersion) && (
<Alert>
<AlertDescription>
You must download{" "}
{getBrowserDisplayName(profile.browser)}{" "}
{selectedVersion} before switching to this release
type. Use the download button above to get the
latest version.
</AlertDescription>
</Alert>
)}
<ReleaseTypeSelector
selectedReleaseType={selectedReleaseType}
onReleaseTypeSelect={setSelectedReleaseType}
availableReleaseTypes={releaseTypes}
isDownloading={isBrowserDownloading(profile.browser)}
onDownload={() => {
void handleDownload();
}}
placeholder="Select release type..."
downloadedVersions={downloadedVersions}
/>
</div>
)}
</div>
</div>
) : (
<div className="grid gap-2">
<Label>New Release Type</Label>
{isLoadingReleaseTypes ? (
<div className="text-sm text-muted-foreground">
Loading release types...
</div>
) : (
<div className="space-y-4">
{selectedReleaseType &&
selectedReleaseType !== profile.release_type &&
selectedVersion &&
!isVersionDownloaded(selectedVersion) && (
<Alert>
<AlertDescription>
You must download{" "}
{getBrowserDisplayName(profile.browser)}{" "}
{selectedVersion} before switching to this release
type. Use the download button above to get the latest
version.
</AlertDescription>
</Alert>
)}
<ReleaseTypeSelector
selectedReleaseType={selectedReleaseType}
onReleaseTypeSelect={setSelectedReleaseType}
availableReleaseTypes={releaseTypes}
isDownloading={isBrowserDownloading(profile.browser)}
onDownload={() => {
void handleDownload();
}}
placeholder="Select release type..."
downloadedVersions={downloadedVersions}
/>
</div>
)}
</div>
)}
{/* Nightly switching disabled in UI; no downgrade warning needed. */}
</div>
<DialogFooter>
<RippleButton variant="outline" onClick={onClose}>
Cancel
</RippleButton>
<LoadingButton
isLoading={isUpdating}
onClick={() => {
void handleVersionChange();
}}
disabled={!canUpdate}
>
{isUpdating ? "Updating..." : "Update Release Type"}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+50 -79
View File
@@ -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<BrowserReleaseTypes>({});
const [camoufoxReleaseTypes, setCamoufoxReleaseTypes] =
useState<BrowserReleaseTypes>({});
const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]);
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
const [showProxyForm, setShowProxyForm] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>();
const loadingBrowserRef = useRef<string | null>(null);
// Use the browser download hook
@@ -185,7 +173,7 @@ export function CreateProfileDialog({
loadingBrowserRef.current = browser;
try {
const releaseTypes = await invoke<BrowserReleaseTypes>(
const rawReleaseTypes = await invoke<BrowserReleaseTypes>(
"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 (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="w-full max-h-[90vh] flex flex-col">
@@ -478,17 +467,12 @@ export function CreateProfileDialog({
<div className="space-y-3">
{!isBrowserCurrentlyDownloading(selectedBrowser) &&
!isBrowserVersionAvailable(selectedBrowser) &&
getBestAvailableVersion(
availableReleaseTypes,
selectedBrowser,
) && (
getBestAvailableVersion(selectedBrowser) && (
<div className="flex gap-3 items-center">
<p className="text-sm text-muted-foreground">
{(() => {
const bestVersion = getBestAvailableVersion(
availableReleaseTypes,
selectedBrowser,
);
const bestVersion =
getBestAvailableVersion(selectedBrowser);
return `Latest version (${bestVersion?.version}) needs to be downloaded`;
})()}
</p>
@@ -510,10 +494,8 @@ export function CreateProfileDialog({
isBrowserVersionAvailable(selectedBrowser) && (
<div className="text-sm text-muted-foreground">
{(() => {
const bestVersion = getBestAvailableVersion(
availableReleaseTypes,
selectedBrowser,
);
const bestVersion =
getBestAvailableVersion(selectedBrowser);
return `✓ Latest version (${bestVersion?.version}) is available`;
})()}
</div>
@@ -521,10 +503,8 @@ export function CreateProfileDialog({
{isBrowserCurrentlyDownloading(selectedBrowser) && (
<div className="text-sm text-muted-foreground">
{(() => {
const bestVersion = getBestAvailableVersion(
availableReleaseTypes,
selectedBrowser,
);
const bestVersion =
getBestAvailableVersion(selectedBrowser);
return `Downloading version (${bestVersion?.version})...`;
})()}
</div>
@@ -539,17 +519,12 @@ export function CreateProfileDialog({
{/* Camoufox Download Status */}
{!isBrowserCurrentlyDownloading("camoufox") &&
!isBrowserVersionAvailable("camoufox") &&
getBestAvailableVersion(
camoufoxReleaseTypes,
"camoufox",
) && (
getBestAvailableVersion("camoufox") && (
<div className="flex gap-3 items-center p-3 rounded-md border">
<p className="text-sm text-muted-foreground">
{(() => {
const bestVersion = getBestAvailableVersion(
camoufoxReleaseTypes,
"camoufox",
);
const bestVersion =
getBestAvailableVersion("camoufox");
return `Camoufox version (${bestVersion?.version}) needs to be downloaded`;
})()}
</p>
@@ -571,10 +546,8 @@ export function CreateProfileDialog({
isBrowserVersionAvailable("camoufox") && (
<div className="p-3 text-sm rounded-md border text-muted-foreground">
{(() => {
const bestVersion = getBestAvailableVersion(
camoufoxReleaseTypes,
"camoufox",
);
const bestVersion =
getBestAvailableVersion("camoufox");
return `✓ Camoufox version (${bestVersion?.version}) is available`;
})()}
</div>
@@ -582,10 +555,8 @@ export function CreateProfileDialog({
{isBrowserCurrentlyDownloading("camoufox") && (
<div className="p-3 text-sm rounded-md border text-muted-foreground">
{(() => {
const bestVersion = getBestAvailableVersion(
camoufoxReleaseTypes,
"camoufox",
);
const bestVersion =
getBestAvailableVersion("camoufox");
return `Downloading Camoufox version (${bestVersion?.version})...`;
})()}
</div>
+6 -1
View File
@@ -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({
/>
</SelectTrigger>
<SelectContent>
{supportedBrowsers.map((browser) => {
{importableBrowsers.map((browser) => {
const IconComponent = getBrowserIcon(browser);
return (
<SelectItem key={browser} value={browser}>
-15
View File
@@ -64,7 +64,6 @@ interface ProfilesDataTableProps {
onProxySettings: (profile: BrowserProfile) => void;
onDeleteProfile: (profile: BrowserProfile) => void | Promise<void>;
onRenameProfile: (oldName: string, newName: string) => Promise<void>;
onChangeVersion: (profile: BrowserProfile) => void;
onConfigureCamoufox?: (profile: BrowserProfile) => void;
runningProfiles: Set<string>;
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
</DropdownMenuItem>
)}
{!["chromium", "zen", "camoufox"].includes(
profile.browser,
) && (
<DropdownMenuItem
onClick={() => {
onChangeVersion(profile);
}}
disabled={isDisabled}
>
Switch Release
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
setProfileToRename(profile);
@@ -815,7 +801,6 @@ export function ProfilesDataTable({
onLaunchProfile,
onKillProfile,
onConfigureCamoufox,
onChangeVersion,
onAssignProfilesToGroup,
isUpdating,
launchingProfiles,
-3
View File
@@ -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 }]