mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-22 20:06:18 +02:00
refactor: cleanup and better brave release fetching
This commit is contained in:
Vendored
+1
@@ -108,6 +108,7 @@
|
||||
"pathex",
|
||||
"pathlib",
|
||||
"peerconnection",
|
||||
"pids",
|
||||
"pixbuf",
|
||||
"plasmohq",
|
||||
"platformdirs",
|
||||
|
||||
+249
-257
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }]
|
||||
|
||||
Reference in New Issue
Block a user