fix: linter and formatting

This commit is contained in:
zhom
2025-05-29 10:39:04 +04:00
parent 08678dcacc
commit 56c1f94616
37 changed files with 4013 additions and 2894 deletions
+294 -153
View File
@@ -1,10 +1,10 @@
use directories::BaseDirs;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use directories::BaseDirs;
use crate::browser::GithubRelease;
@@ -34,7 +34,7 @@ enum PreReleaseKind {
impl VersionComponent {
fn parse(version: &str) -> Self {
let version = version.trim();
// Handle special case for Zen Browser twilight releases
if version.to_lowercase().contains("twilight") {
return VersionComponent {
@@ -47,20 +47,22 @@ impl VersionComponent {
// Split version into numeric and pre-release parts
let (numeric_part, pre_release_part) = Self::split_version(version);
// Parse numeric parts (major.minor.patch)
let parts: Vec<u32> = numeric_part
.split('.')
.filter_map(|part| part.parse().ok())
.collect();
let major = parts.get(0).copied().unwrap_or(0);
let minor = parts.get(1).copied().unwrap_or(0);
let patch = parts.get(2).copied().unwrap_or(0);
// Parse pre-release part
let pre_release = pre_release_part.as_deref().and_then(Self::parse_pre_release);
let pre_release = pre_release_part
.as_deref()
.and_then(Self::parse_pre_release);
VersionComponent {
major,
minor,
@@ -68,39 +70,49 @@ impl VersionComponent {
pre_release,
}
}
fn split_version(version: &str) -> (String, Option<String>) {
let version = version.to_lowercase();
// Look for pre-release indicators
for (i, ch) in version.char_indices() {
if ch.is_alphabetic() && i > 0 {
// Check if this is a pre-release indicator
let remaining = &version[i..];
if remaining.starts_with('a') || remaining.starts_with('b') ||
remaining.starts_with("alpha") || remaining.starts_with("beta") ||
remaining.starts_with("rc") || remaining.starts_with("dev") ||
remaining.starts_with("pre") {
if remaining.starts_with('a')
|| remaining.starts_with('b')
|| remaining.starts_with("alpha")
|| remaining.starts_with("beta")
|| remaining.starts_with("rc")
|| remaining.starts_with("dev")
|| remaining.starts_with("pre")
{
return (version[..i].to_string(), Some(remaining.to_string()));
}
}
}
(version, None)
}
fn parse_pre_release(pre_release: &str) -> Option<PreRelease> {
let pre_release = pre_release.trim().to_lowercase();
if pre_release.is_empty() {
return None;
}
// Extract kind and number
let (kind, number) = if pre_release.starts_with("alpha") {
(PreReleaseKind::Alpha, Self::extract_number(&pre_release[5..]))
(
PreReleaseKind::Alpha,
Self::extract_number(&pre_release[5..]),
)
} else if pre_release.starts_with("beta") {
(PreReleaseKind::Beta, Self::extract_number(&pre_release[4..]))
(
PreReleaseKind::Beta,
Self::extract_number(&pre_release[4..]),
)
} else if pre_release.starts_with("rc") {
(PreReleaseKind::RC, Self::extract_number(&pre_release[2..]))
} else if pre_release.starts_with("dev") {
@@ -108,16 +120,22 @@ impl VersionComponent {
} else if pre_release.starts_with("pre") {
(PreReleaseKind::Pre, Self::extract_number(&pre_release[3..]))
} else if pre_release.starts_with('a') {
(PreReleaseKind::Alpha, Self::extract_number(&pre_release[1..]))
(
PreReleaseKind::Alpha,
Self::extract_number(&pre_release[1..]),
)
} else if pre_release.starts_with('b') {
(PreReleaseKind::Beta, Self::extract_number(&pre_release[1..]))
(
PreReleaseKind::Beta,
Self::extract_number(&pre_release[1..]),
)
} else {
return None;
};
Some(PreRelease { kind, number })
}
fn extract_number(s: &str) -> Option<u32> {
let numeric_part: String = s.chars().filter(|c| c.is_ascii_digit()).collect();
numeric_part.parse().ok()
@@ -133,7 +151,7 @@ impl PartialOrd for VersionComponent {
impl Ord for VersionComponent {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
use std::cmp::Ordering;
// Compare major.minor.patch first
match (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch)) {
Ordering::Equal => {
@@ -182,8 +200,6 @@ pub fn sort_github_releases(releases: &mut [GithubRelease]) {
});
}
pub fn is_alpha_version(version: &str) -> bool {
let version_comp = VersionComponent::parse(version);
version_comp.pre_release.is_some()
@@ -264,14 +280,14 @@ impl ApiClient {
pub fn load_cached_versions(&self, browser: &str) -> Option<Vec<String>> {
let cache_dir = Self::get_cache_dir().ok()?;
let cache_file = cache_dir.join(format!("{}_versions.json", browser));
if !cache_file.exists() {
return None;
}
let content = fs::read_to_string(&cache_file).ok()?;
let cached_data: CachedVersionData = serde_json::from_str(&content).ok()?;
// Always return cached versions regardless of age - they're always valid
println!("Using cached versions for {}", browser);
Some(cached_data.versions)
@@ -283,7 +299,7 @@ impl ApiClient {
Err(_) => return true, // If we can't get cache dir, consider expired
};
let cache_file = cache_dir.join(format!("{}_versions.json", browser));
if !cache_file.exists() {
return true; // No cache file means expired
}
@@ -297,20 +313,24 @@ impl ApiClient {
Ok(data) => data,
Err(_) => return true, // Can't parse cache, consider expired
};
// Check if cache is older than 10 minutes
!Self::is_cache_valid(cached_data.timestamp)
}
pub fn save_cached_versions(&self, browser: &str, versions: &[String]) -> Result<(), Box<dyn std::error::Error>> {
pub fn save_cached_versions(
&self,
browser: &str,
versions: &[String],
) -> Result<(), Box<dyn std::error::Error>> {
let cache_dir = Self::get_cache_dir()?;
let cache_file = cache_dir.join(format!("{}_versions.json", browser));
let cached_data = CachedVersionData {
versions: versions.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);
@@ -320,56 +340,69 @@ impl ApiClient {
fn load_cached_github_releases(&self, browser: &str) -> Option<Vec<GithubRelease>> {
let cache_dir = Self::get_cache_dir().ok()?;
let cache_file = cache_dir.join(format!("{}_github.json", browser));
if !cache_file.exists() {
return None;
}
let content = fs::read_to_string(&cache_file).ok()?;
let cached_data: CachedGithubData = serde_json::from_str(&content).ok()?;
// Always use cached GitHub releases - cache never expires, only gets updated with new versions
println!("Using cached GitHub releases for {}", browser);
Some(cached_data.releases)
}
fn save_cached_github_releases(&self, browser: &str, releases: &[GithubRelease]) -> Result<(), Box<dyn std::error::Error>> {
fn save_cached_github_releases(
&self,
browser: &str,
releases: &[GithubRelease],
) -> Result<(), Box<dyn std::error::Error>> {
let cache_dir = Self::get_cache_dir()?;
let cache_file = cache_dir.join(format!("{}_github.json", browser));
let cached_data = CachedGithubData {
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 {} GitHub releases for {}", releases.len(), browser);
Ok(())
}
pub async fn fetch_firefox_releases_with_caching(&self, no_caching: bool) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_firefox_releases_with_caching(
&self,
no_caching: bool,
) -> 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_alpha_version(&version),
download_url: Some(format!(
"https://download.mozilla.org/?product=firefox-{}&os=osx&lang=en-US",
version
)),
}
}).collect());
return Ok(
cached_versions
.into_iter()
.map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: is_alpha_version(&version),
download_url: Some(format!(
"https://download.mozilla.org/?product=firefox-{}&os=osx&lang=en-US",
version
)),
}
})
.collect(),
);
}
}
println!("Fetching Firefox releases from Mozilla API...");
let url = "https://product-details.mozilla.org/1.0/firefox.json";
let response = self.client
let response = self
.client
.get(url)
.header("User-Agent", "donutbrowser")
.send()
@@ -380,7 +413,7 @@ impl ApiClient {
}
let firefox_response: FirefoxApiResponse = response.json().await?;
// Extract releases and filter for stable versions
let mut releases: Vec<BrowserRelease> = firefox_response
.releases
@@ -413,7 +446,7 @@ impl ApiClient {
// 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) {
@@ -424,35 +457,50 @@ impl ApiClient {
Ok(releases)
}
pub async fn fetch_firefox_developer_releases_with_caching(&self, no_caching: bool) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_firefox_developer_releases_with_caching(
&self,
no_caching: bool,
) -> 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_alpha_version(&version),
download_url: Some(format!(
"https://download.mozilla.org/?product=devedition-{}&os=osx&lang=en-US",
version
)),
}
}).collect());
return Ok(
cached_versions
.into_iter()
.map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(), // Cache doesn't store dates
is_prerelease: is_alpha_version(&version),
download_url: Some(format!(
"https://download.mozilla.org/?product=devedition-{}&os=osx&lang=en-US",
version
)),
}
})
.collect(),
);
}
}
println!("Fetching Firefox Developer Edition releases from Mozilla API...");
let url = "https://product-details.mozilla.org/1.0/devedition.json";
let response = self.client
let response = self
.client
.get(url)
.header("User-Agent", "donutbrowser")
.send()
.await?;
if !response.status().is_success() {
return Err(format!("Failed to fetch Firefox Developer Edition versions: {}", response.status()).into());
return Err(
format!(
"Failed to fetch Firefox Developer Edition versions: {}",
response.status()
)
.into(),
);
}
let firefox_response: FirefoxApiResponse = response.json().await?;
@@ -489,7 +537,7 @@ impl ApiClient {
// 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) {
@@ -500,11 +548,16 @@ impl ApiClient {
Ok(releases)
}
pub async fn fetch_mullvad_releases(&self) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_mullvad_releases(
&self,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self.fetch_mullvad_releases_with_caching(false).await
}
pub async fn fetch_mullvad_releases_with_caching(&self, no_caching: bool) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_mullvad_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_releases) = self.load_cached_github_releases("mullvad") {
@@ -544,11 +597,16 @@ impl ApiClient {
Ok(releases)
}
pub async fn fetch_zen_releases(&self) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_zen_releases(
&self,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self.fetch_zen_releases_with_caching(false).await
}
pub async fn fetch_zen_releases_with_caching(&self, no_caching: bool) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_zen_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_releases) = self.load_cached_github_releases("zen") {
@@ -580,11 +638,16 @@ impl ApiClient {
Ok(releases)
}
pub async fn fetch_brave_releases(&self) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_brave_releases(
&self,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
self.fetch_brave_releases_with_caching(false).await
}
pub async fn fetch_brave_releases_with_caching(&self, no_caching: bool) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_brave_releases_with_caching(
&self,
no_caching: bool,
) -> Result<Vec<GithubRelease>, Box<dyn std::error::Error + Send + Sync>> {
// Check cache first (unless bypassing)
if !no_caching {
if let Some(cached_releases) = self.load_cached_github_releases("brave") {
@@ -608,10 +671,11 @@ impl ApiClient {
.into_iter()
.filter_map(|mut release| {
// Check if this release has a universal DMG asset
let has_universal_dmg = release.assets.iter().any(|asset| {
asset.name.contains(".dmg") && asset.name.contains("universal")
});
let has_universal_dmg = release
.assets
.iter()
.any(|asset| asset.name.contains(".dmg") && asset.name.contains("universal"));
if has_universal_dmg {
// Set is_alpha based on the release name
// Nightly releases contain "Nightly", stable contain "Release"
@@ -636,10 +700,19 @@ impl ApiClient {
Ok(filtered_releases)
}
pub async fn fetch_chromium_latest_version(&self) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_chromium_latest_version(
&self,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// Use architecture-aware URL for Chromium
let arch = if cfg!(target_arch = "aarch64") { "Mac_Arm" } else { "Mac" };
let url = format!("https://commondatastorage.googleapis.com/chromium-browser-snapshots/{}/LAST_CHANGE", arch);
let arch = if cfg!(target_arch = "aarch64") {
"Mac_Arm"
} else {
"Mac"
};
let url = format!(
"https://commondatastorage.googleapis.com/chromium-browser-snapshots/{}/LAST_CHANGE",
arch
);
let version = self
.client
.get(&url)
@@ -654,27 +727,35 @@ impl ApiClient {
Ok(version)
}
pub async fn fetch_chromium_releases_with_caching(&self, no_caching: bool) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_chromium_releases_with_caching(
&self,
no_caching: bool,
) -> 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
download_url: None,
}
}).collect());
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
download_url: None,
}
})
.collect(),
);
}
}
println!("Fetching Chromium releases...");
// Get the latest version first
let latest_version = self.fetch_chromium_latest_version().await?;
let latest_num: u32 = latest_version.parse().unwrap_or(0);
// Generate a list of recent versions (last 20 builds, going back by 1000 each time)
let mut versions = Vec::new();
for i in 0..20 {
@@ -683,7 +764,7 @@ impl ApiClient {
versions.push(version_num.to_string());
}
}
// Cache the results (unless bypassing cache)
if !no_caching {
if let Err(e) = self.save_cached_versions("chromium", &versions) {
@@ -691,17 +772,23 @@ impl ApiClient {
}
}
Ok(versions.into_iter().map(|version| {
BrowserRelease {
version: version.clone(),
date: "".to_string(),
is_prerelease: false,
download_url: None,
}
}).collect())
Ok(
versions
.into_iter()
.map(|version| BrowserRelease {
version: version.clone(),
date: "".to_string(),
is_prerelease: false,
download_url: None,
})
.collect(),
)
}
pub async fn fetch_tor_releases_with_caching(&self, no_caching: bool) -> Result<Vec<BrowserRelease>, Box<dyn std::error::Error + Send + Sync>> {
pub async fn fetch_tor_releases_with_caching(
&self,
no_caching: bool,
) -> 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") {
@@ -732,7 +819,7 @@ impl ApiClient {
// Parse HTML to extract version directories
let mut version_candidates = Vec::new();
// Look for directory links in the HTML
for line in html.lines() {
if line.contains("<a href=\"") && line.contains("/\">") {
@@ -741,9 +828,12 @@ impl ApiClient {
let start = start + 9; // Length of "<a href=\""
if let Some(end) = line[start..].find("/\">") {
let version = &line[start..start + end];
// Skip parent directory and non-version entries
if version != ".." && !version.is_empty() && version.chars().next().unwrap_or('a').is_ascii_digit() {
if version != ".."
&& !version.is_empty()
&& version.chars().next().unwrap_or('a').is_ascii_digit()
{
version_candidates.push(version.to_string());
}
}
@@ -763,7 +853,7 @@ impl ApiClient {
version_strings.push(version);
}
}
// Add a small delay to avoid overwhelming the server
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
@@ -788,8 +878,14 @@ impl ApiClient {
}).collect())
}
async fn check_tor_version_has_macos(&self, version: &str) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://archive.torproject.org/tor-package-archive/torbrowser/{}/", version);
async fn check_tor_version_has_macos(
&self,
version: &str,
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let url = format!(
"https://archive.torproject.org/tor-package-archive/torbrowser/{}/",
version
);
let html = self
.client
.get(&url)
@@ -908,16 +1004,22 @@ mod tests {
async fn test_firefox_api() {
let client = ApiClient::new();
let result = client.fetch_firefox_releases_with_caching(false).await;
match result {
Ok(releases) => {
assert!(!releases.is_empty(), "Should have Firefox releases");
// Check that releases have required fields
let first_release = &releases[0];
assert!(!first_release.version.is_empty(), "Version should not be empty");
assert!(first_release.download_url.is_some(), "Should have download URL");
assert!(
!first_release.version.is_empty(),
"Version should not be empty"
);
assert!(
first_release.download_url.is_some(),
"Should have download URL"
);
println!("Firefox API test passed. Found {} releases", releases.len());
println!("Latest version: {}", first_release.version);
}
@@ -931,19 +1033,33 @@ mod tests {
#[tokio::test]
async fn test_firefox_developer_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_firefox_developer_releases_with_caching(false).await;
let result = client
.fetch_firefox_developer_releases_with_caching(false)
.await;
match result {
Ok(releases) => {
assert!(!releases.is_empty(), "Should have Firefox Developer releases");
assert!(
!releases.is_empty(),
"Should have Firefox Developer releases"
);
let first_release = &releases[0];
assert!(!first_release.version.is_empty(), "Version should not be empty");
assert!(first_release.download_url.is_some(), "Should have download URL");
println!("Firefox Developer API test passed. Found {} releases", releases.len());
assert!(
!first_release.version.is_empty(),
"Version should not be empty"
);
assert!(
first_release.download_url.is_some(),
"Should have download URL"
);
println!(
"Firefox Developer API test passed. Found {} releases",
releases.len()
);
println!("Latest version: {}", first_release.version);
}
Err(e) => {
@@ -956,17 +1072,20 @@ mod tests {
#[tokio::test]
async fn test_mullvad_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_mullvad_releases().await;
match result {
Ok(releases) => {
assert!(!releases.is_empty(), "Should have Mullvad releases");
let first_release = &releases[0];
assert!(!first_release.tag_name.is_empty(), "Tag name should not be empty");
assert!(
!first_release.tag_name.is_empty(),
"Tag name should not be empty"
);
println!("Mullvad API test passed. Found {} releases", releases.len());
println!("Latest version: {}", first_release.tag_name);
}
@@ -980,17 +1099,20 @@ mod tests {
#[tokio::test]
async fn test_zen_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_zen_releases().await;
match result {
Ok(releases) => {
assert!(!releases.is_empty(), "Should have Zen releases");
let first_release = &releases[0];
assert!(!first_release.tag_name.is_empty(), "Tag name should not be empty");
assert!(
!first_release.tag_name.is_empty(),
"Tag name should not be empty"
);
println!("Zen API test passed. Found {} releases", releases.len());
println!("Latest version: {}", first_release.tag_name);
}
@@ -1004,14 +1126,17 @@ mod tests {
#[tokio::test]
async fn test_brave_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_brave_releases().await;
match result {
Ok(releases) => {
// Note: Brave might not always have macOS releases, so we don't assert non-empty
println!("Brave API test passed. Found {} releases with macOS assets", releases.len());
println!(
"Brave API test passed. Found {} releases with macOS assets",
releases.len()
);
if !releases.is_empty() {
println!("Latest version: {}", releases[0].tag_name);
}
@@ -1026,15 +1151,18 @@ mod tests {
#[tokio::test]
async fn test_chromium_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(2500)).await; // Rate limiting
let client = ApiClient::new();
let result = client.fetch_chromium_latest_version().await;
match result {
Ok(version) => {
assert!(!version.is_empty(), "Version should not be empty");
assert!(version.chars().all(|c| c.is_ascii_digit()), "Version should be numeric");
assert!(
version.chars().all(|c| c.is_ascii_digit()),
"Version should be numeric"
);
println!("Chromium API test passed. Latest version: {}", version);
}
Err(e) => {
@@ -1047,21 +1175,31 @@ mod tests {
#[tokio::test]
async fn test_tor_api() {
tokio::time::sleep(tokio::time::Duration::from_millis(3000)).await; // Rate limiting
let client = ApiClient::new();
// Use a timeout for this test since TOR API can be slow
let timeout_duration = tokio::time::Duration::from_secs(30);
let result = tokio::time::timeout(timeout_duration, client.fetch_tor_releases_with_caching(false)).await;
let result = tokio::time::timeout(
timeout_duration,
client.fetch_tor_releases_with_caching(false),
)
.await;
match result {
Ok(Ok(releases)) => {
assert!(!releases.is_empty(), "Should have TOR releases");
let first_release = &releases[0];
assert!(!first_release.version.is_empty(), "Version should not be empty");
assert!(first_release.download_url.is_some(), "Should have download URL");
assert!(
!first_release.version.is_empty(),
"Version should not be empty"
);
assert!(
first_release.download_url.is_some(),
"Should have download URL"
);
println!("TOR API test passed. Found {} releases", releases.len());
println!("Latest version: {}", first_release.version);
}
@@ -1081,14 +1219,17 @@ mod tests {
#[tokio::test]
async fn test_tor_version_check() {
tokio::time::sleep(tokio::time::Duration::from_millis(3500)).await; // Rate limiting
let client = ApiClient::new();
let result = client.check_tor_version_has_macos("14.0.4").await;
match result {
Ok(has_macos) => {
assert!(has_macos, "Version 14.0.4 should have macOS support");
println!("TOR version check test passed. Version 14.0.4 has macOS: {}", has_macos);
println!(
"TOR version check test passed. Version 14.0.4 has macOS: {}",
has_macos
);
}
Err(e) => {
println!("TOR version check test failed: {}", e);
@@ -1096,4 +1237,4 @@ mod tests {
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+95 -51
View File
@@ -61,8 +61,6 @@ pub trait Browser: Send + Sync {
fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool;
}
pub struct FirefoxBrowser {
browser_type: BrowserType,
}
@@ -71,7 +69,6 @@ impl FirefoxBrowser {
pub fn new(browser_type: BrowserType) -> Self {
Self { browser_type }
}
}
impl Browser for FirefoxBrowser {
@@ -79,8 +76,6 @@ impl Browser for FirefoxBrowser {
self.browser_type.clone()
}
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
// Find the .app directory
let app_path = std::fs::read_dir(install_dir)?
@@ -99,7 +94,11 @@ impl Browser for FirefoxBrowser {
.find(|entry| {
let binding = entry.file_name();
let name = binding.to_string_lossy();
name.starts_with("firefox") || name.starts_with("mullvad") || name.starts_with("zen") || name.starts_with("tor") || name.contains("Browser")
name.starts_with("firefox")
|| name.starts_with("mullvad")
|| name.starts_with("zen")
|| name.starts_with("tor")
|| name.contains("Browser")
})
.map(|entry| entry.path())
.ok_or("No executable found in MacOS directory")?;
@@ -113,11 +112,8 @@ impl Browser for FirefoxBrowser {
_proxy_settings: Option<&ProxySettings>,
url: Option<String>,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut args = vec![
"-profile".to_string(),
profile_path.to_string(),
];
let mut args = vec!["-profile".to_string(), profile_path.to_string()];
// Only use -no-remote for browsers that require it for security (Mullvad, Tor)
// Regular Firefox browsers can use remote commands for better URL handling
match self.browser_type {
@@ -129,7 +125,7 @@ impl Browser for FirefoxBrowser {
}
_ => {}
}
// Firefox-based browsers use profile directory and user.js for proxy configuration
if let Some(url) = url {
args.push(url);
@@ -143,7 +139,10 @@ impl Browser for FirefoxBrowser {
.join(self.browser_type().as_str())
.join(version);
println!("Firefox browser checking version {} in directory: {:?}", version, browser_dir);
println!(
"Firefox browser checking version {} in directory: {:?}",
version, browser_dir
);
// Only check if directory exists and contains a .app file
if browser_dir.exists() {
@@ -183,7 +182,6 @@ impl Browser for ChromiumBrowser {
self.browser_type.clone()
}
fn get_executable_path(&self, install_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
// Find the .app directory
let app_path = std::fs::read_dir(install_dir)?
@@ -253,7 +251,10 @@ impl Browser for ChromiumBrowser {
.join(self.browser_type().as_str())
.join(version);
println!("Chromium browser checking version {} in directory: {:?}", version, browser_dir);
println!(
"Chromium browser checking version {} in directory: {:?}",
version, browser_dir
);
// Check if directory exists and contains at least one .app file
if browser_dir.exists() {
@@ -286,9 +287,11 @@ impl Browser for ChromiumBrowser {
// Factory function to create browser instances
pub fn create_browser(browser_type: BrowserType) -> Box<dyn Browser> {
match browser_type {
BrowserType::MullvadBrowser | BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen | BrowserType::TorBrowser => {
Box::new(FirefoxBrowser::new(browser_type))
}
BrowserType::MullvadBrowser
| BrowserType::Firefox
| BrowserType::FirefoxDeveloper
| BrowserType::Zen
| BrowserType::TorBrowser => Box::new(FirefoxBrowser::new(browser_type)),
BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)),
}
}
@@ -332,13 +335,28 @@ mod tests {
assert_eq!(BrowserType::TorBrowser.as_str(), "tor-browser");
// Test from_str
assert_eq!(BrowserType::from_str("mullvad-browser").unwrap(), BrowserType::MullvadBrowser);
assert_eq!(BrowserType::from_str("firefox").unwrap(), BrowserType::Firefox);
assert_eq!(BrowserType::from_str("firefox-developer").unwrap(), BrowserType::FirefoxDeveloper);
assert_eq!(BrowserType::from_str("chromium").unwrap(), BrowserType::Chromium);
assert_eq!(
BrowserType::from_str("mullvad-browser").unwrap(),
BrowserType::MullvadBrowser
);
assert_eq!(
BrowserType::from_str("firefox").unwrap(),
BrowserType::Firefox
);
assert_eq!(
BrowserType::from_str("firefox-developer").unwrap(),
BrowserType::FirefoxDeveloper
);
assert_eq!(
BrowserType::from_str("chromium").unwrap(),
BrowserType::Chromium
);
assert_eq!(BrowserType::from_str("brave").unwrap(), BrowserType::Brave);
assert_eq!(BrowserType::from_str("zen").unwrap(), BrowserType::Zen);
assert_eq!(BrowserType::from_str("tor-browser").unwrap(), BrowserType::TorBrowser);
assert_eq!(
BrowserType::from_str("tor-browser").unwrap(),
BrowserType::TorBrowser
);
// Test invalid browser type
assert!(BrowserType::from_str("invalid").is_err());
@@ -353,10 +371,10 @@ mod tests {
let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser);
assert_eq!(browser.browser_type(), BrowserType::MullvadBrowser);
let browser = FirefoxBrowser::new(BrowserType::TorBrowser);
assert_eq!(browser.browser_type(), BrowserType::TorBrowser);
let browser = FirefoxBrowser::new(BrowserType::Zen);
assert_eq!(browser.browser_type(), BrowserType::Zen);
}
@@ -381,10 +399,10 @@ mod tests {
let browser = create_browser(BrowserType::Zen);
assert_eq!(browser.browser_type(), BrowserType::Zen);
let browser = create_browser(BrowserType::TorBrowser);
assert_eq!(browser.browser_type(), BrowserType::TorBrowser);
let browser = create_browser(BrowserType::FirefoxDeveloper);
assert_eq!(browser.browser_type(), BrowserType::FirefoxDeveloper);
@@ -400,47 +418,71 @@ mod tests {
fn test_firefox_launch_args() {
// Test regular Firefox (should not use -no-remote)
let browser = FirefoxBrowser::new(BrowserType::Firefox);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
assert!(!args.contains(&"-no-remote".to_string()));
let args = browser.create_launch_args("/path/to/profile", None, Some("https://example.com".to_string())).unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile", "https://example.com"]);
let args = browser
.create_launch_args(
"/path/to/profile",
None,
Some("https://example.com".to_string()),
)
.unwrap();
assert_eq!(
args,
vec!["-profile", "/path/to/profile", "https://example.com"]
);
// Test Mullvad Browser (should use -no-remote)
let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
// Test Tor Browser (should use -no-remote)
let browser = FirefoxBrowser::new(BrowserType::TorBrowser);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]);
// Test Zen Browser (should not use -no-remote)
let browser = FirefoxBrowser::new(BrowserType::Zen);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
assert_eq!(args, vec!["-profile", "/path/to/profile"]);
assert!(!args.contains(&"-no-remote".to_string()));
}
#[test]
fn test_chromium_launch_args() {
let browser = ChromiumBrowser::new(BrowserType::Chromium);
let args = browser.create_launch_args("/path/to/profile", None, None).unwrap();
let args = browser
.create_launch_args("/path/to/profile", None, None)
.unwrap();
// Test that basic required arguments are present
assert!(args.contains(&"--user-data-dir=/path/to/profile".to_string()));
assert!(args.contains(&"--no-default-browser-check".to_string()));
// Test that automatic update disabling arguments are present
assert!(args.contains(&"--disable-background-mode".to_string()));
assert!(args.contains(&"--disable-component-update".to_string()));
let args_with_url = browser.create_launch_args("/path/to/profile", None, Some("https://example.com".to_string())).unwrap();
let args_with_url = browser
.create_launch_args(
"/path/to/profile",
None,
Some("https://example.com".to_string()),
)
.unwrap();
assert!(args_with_url.contains(&"https://example.com".to_string()));
// Verify URL is at the end
assert_eq!(args_with_url.last().unwrap(), "https://example.com");
}
@@ -458,7 +500,7 @@ mod tests {
assert_eq!(proxy.proxy_type, "http");
assert_eq!(proxy.host, "127.0.0.1");
assert_eq!(proxy.port, 8080);
// Test different proxy types
let socks_proxy = ProxySettings {
enabled: true,
@@ -466,13 +508,12 @@ mod tests {
host: "proxy.example.com".to_string(),
port: 1080,
};
assert_eq!(socks_proxy.proxy_type, "socks5");
assert_eq!(socks_proxy.host, "proxy.example.com");
assert_eq!(socks_proxy.port, 1080);
}
#[test]
fn test_version_downloaded_check() {
let temp_dir = TempDir::new().unwrap();
@@ -489,17 +530,20 @@ mod tests {
let browser = FirefoxBrowser::new(BrowserType::Firefox);
assert!(browser.is_version_downloaded("139.0", binaries_dir));
assert!(!browser.is_version_downloaded("140.0", binaries_dir));
// Test with Chromium browser
let chromium_dir = binaries_dir.join("chromium").join("1465660");
fs::create_dir_all(&chromium_dir).unwrap();
let chromium_app_dir = chromium_dir.join("Chromium.app");
fs::create_dir_all(&chromium_app_dir.join("Contents").join("MacOS")).unwrap();
// Create a mock executable
let executable_path = chromium_app_dir.join("Contents").join("MacOS").join("Chromium");
let executable_path = chromium_app_dir
.join("Contents")
.join("MacOS")
.join("Chromium");
fs::write(&executable_path, "mock executable").unwrap();
let chromium_browser = ChromiumBrowser::new(BrowserType::Chromium);
assert!(chromium_browser.is_version_downloaded("1465660", binaries_dir));
assert!(!chromium_browser.is_version_downloaded("1465661", binaries_dir));
@@ -513,7 +557,7 @@ mod tests {
// Create browser directory but no .app directory
let browser_dir = binaries_dir.join("firefox").join("139.0");
fs::create_dir_all(&browser_dir).unwrap();
// Create some other files but no .app
fs::write(browser_dir.join("readme.txt"), "Some content").unwrap();
@@ -526,7 +570,7 @@ mod tests {
let browser_type = BrowserType::Firefox;
let cloned = browser_type.clone();
assert_eq!(browser_type, cloned);
// Test Debug trait
let debug_str = format!("{:?}", browser_type);
assert!(debug_str.contains("Firefox"));
@@ -540,13 +584,13 @@ mod tests {
host: "127.0.0.1".to_string(),
port: 8080,
};
// Test that it can be serialized (implements Serialize)
let json = serde_json::to_string(&proxy).unwrap();
assert!(json.contains("127.0.0.1"));
assert!(json.contains("8080"));
assert!(json.contains("http"));
// Test that it can be deserialized (implements Deserialize)
let deserialized: ProxySettings = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.enabled, proxy.enabled);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+34 -18
View File
@@ -4,10 +4,7 @@ use tauri::command;
mod macos {
use core_foundation::base::OSStatus;
use core_foundation::string::CFStringRef;
use core_foundation::{
base::TCFType,
string::CFString,
};
use core_foundation::{base::TCFType, string::CFString};
#[link(name = "CoreServices", kind = "framework")]
extern "C" {
@@ -18,7 +15,7 @@ mod macos {
pub fn is_default_browser() -> Result<bool, String> {
let schemes = ["http", "https"];
let bundle_id = "com.donutbrowser";
for scheme in schemes {
let scheme_str = CFString::new(scheme);
unsafe {
@@ -26,10 +23,10 @@ mod macos {
if current_handler.is_null() {
return Ok(false);
}
let current_handler_cf = CFString::wrap_under_create_rule(current_handler);
let current_handler_str = current_handler_cf.to_string();
if current_handler_str != bundle_id {
return Ok(false);
}
@@ -123,14 +120,21 @@ pub async fn set_as_default_browser() -> Result<(), String> {
}
#[tauri::command]
pub async fn open_url_with_profile(app_handle: tauri::AppHandle, profile_name: String, url: String) -> Result<(), String> {
pub async fn open_url_with_profile(
app_handle: tauri::AppHandle,
profile_name: String,
url: String,
) -> Result<(), String> {
use crate::browser_runner::BrowserRunner;
let runner = BrowserRunner::new();
// Get the profile by name
let profiles = runner.list_profiles().map_err(|e| format!("Failed to list profiles: {}", e))?;
let profile = profiles.into_iter()
let profiles = runner
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {}", e))?;
let profile = profiles
.into_iter()
.find(|p| p.name == profile_name)
.ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
@@ -145,25 +149,37 @@ pub async fn open_url_with_profile(app_handle: tauri::AppHandle, profile_name: S
format!("Failed to open URL with profile: {}", e)
})?;
println!("Successfully opened URL '{}' with profile '{}'", url, profile_name);
println!(
"Successfully opened URL '{}' with profile '{}'",
url, profile_name
);
Ok(())
}
#[tauri::command]
pub async fn smart_open_url(_app_handle: tauri::AppHandle, _url: String, _is_startup: Option<bool>) -> Result<String, String> {
pub async fn smart_open_url(
_app_handle: tauri::AppHandle,
_url: String,
_is_startup: Option<bool>,
) -> Result<String, String> {
use crate::browser_runner::BrowserRunner;
let runner = BrowserRunner::new();
// Get all profiles
let profiles = runner.list_profiles().map_err(|e| format!("Failed to list profiles: {}", e))?;
let profiles = runner
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {}", e))?;
if profiles.is_empty() {
return Err("no_profiles".to_string());
}
println!("URL opening - Total profiles: {}, showing profile selector", profiles.len());
println!(
"URL opening - Total profiles: {}, showing profile selector",
profiles.len()
);
// Always show the profile selector so the user can choose
Err("show_selector".to_string())
}
+86 -76
View File
@@ -45,60 +45,67 @@ impl Downloader {
BrowserType::Brave => {
// For Brave, we need to find the actual macOS asset
let releases = self.api_client.fetch_brave_releases().await?;
// Find the release with the matching version
let release = releases
.iter()
.find(|r| r.tag_name == version || r.tag_name == format!("v{}", version.trim_start_matches('v')))
.find(|r| {
r.tag_name == version || r.tag_name == format!("v{}", version.trim_start_matches('v'))
})
.ok_or(format!("Brave version {} not found", version))?;
// Find the universal macOS DMG asset
let asset = release
.assets
.iter()
.find(|asset| {
asset.name.contains(".dmg") && asset.name.contains("universal")
})
.ok_or(format!("No universal macOS DMG asset found for Brave version {}", version))?;
.find(|asset| asset.name.contains(".dmg") && asset.name.contains("universal"))
.ok_or(format!(
"No universal macOS DMG asset found for Brave version {}",
version
))?;
Ok(asset.browser_download_url.clone())
}
BrowserType::Zen => {
// For Zen, verify the asset exists
let releases = self.api_client.fetch_zen_releases().await?;
let release = releases
.iter()
.find(|r| r.tag_name == version)
.ok_or(format!("Zen version {} not found", version))?;
// Find the macOS universal DMG asset
let asset = release
.assets
.iter()
.find(|asset| asset.name == "zen.macos-universal.dmg")
.ok_or(format!("No macOS universal asset found for Zen version {}", version))?;
.ok_or(format!(
"No macOS universal asset found for Zen version {}",
version
))?;
Ok(asset.browser_download_url.clone())
}
BrowserType::MullvadBrowser => {
// For Mullvad, verify the asset exists
let releases = self.api_client.fetch_mullvad_releases().await?;
let release = releases
.iter()
.find(|r| r.tag_name == version)
.ok_or(format!("Mullvad version {} not found", version))?;
// Find the macOS DMG asset
let asset = release
.assets
.iter()
.find(|asset| {
asset.name.contains(".dmg") && asset.name.contains("mac")
})
.ok_or(format!("No macOS asset found for Mullvad version {}", version))?;
.find(|asset| asset.name.contains(".dmg") && asset.name.contains("mac"))
.ok_or(format!(
"No macOS asset found for Mullvad version {}",
version
))?;
Ok(asset.browser_download_url.clone())
}
_ => {
@@ -117,10 +124,12 @@ impl Downloader {
dest_path: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
let file_path = dest_path.join(&download_info.filename);
// Resolve the actual download URL
let download_url = self.resolve_download_url(browser_type.clone(), version, download_info).await?;
let download_url = self
.resolve_download_url(browser_type.clone(), version, download_info)
.await?;
// Emit initial progress
let progress = DownloadProgress {
browser: browser_type.as_str().to_string(),
@@ -132,7 +141,7 @@ impl Downloader {
eta_seconds: None,
stage: "downloading".to_string(),
};
let _ = app_handle.emit("download-progress", &progress);
// Start download
@@ -161,7 +170,11 @@ impl Downloader {
// Update progress every 100ms to avoid too many events
if now.duration_since(last_update).as_millis() >= 100 {
let elapsed = start_time.elapsed().as_secs_f64();
let speed = if elapsed > 0.0 { downloaded as f64 / elapsed } else { 0.0 };
let speed = if elapsed > 0.0 {
downloaded as f64 / elapsed
} else {
0.0
};
let percentage = if let Some(total) = total_size {
(downloaded as f64 / total as f64) * 100.0
} else {
@@ -201,20 +214,18 @@ mod tests {
#[tokio::test]
async fn test_resolve_brave_download_url() {
let downloader = Downloader::new();
// Test with a known Brave version
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "brave-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::Brave,
"v1.81.9",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::Brave, "v1.81.9", &download_info)
.await;
match result {
Ok(url) => {
assert!(url.contains("github.com/brave/brave-browser"));
@@ -223,7 +234,10 @@ mod tests {
println!("Brave download URL resolved: {}", url);
}
Err(e) => {
println!("Brave URL resolution failed (expected if version doesn't exist): {}", e);
println!(
"Brave URL resolution failed (expected if version doesn't exist): {}",
e
);
// This might fail if the version doesn't exist, which is okay for testing
}
}
@@ -232,19 +246,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_zen_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "zen-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::Zen,
"1.11b",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::Zen, "1.11b", &download_info)
.await;
match result {
Ok(url) => {
assert!(url.contains("github.com/zen-browser/desktop"));
@@ -252,7 +264,10 @@ mod tests {
println!("Zen download URL resolved: {}", url);
}
Err(e) => {
println!("Zen URL resolution failed (expected if version doesn't exist): {}", e);
println!(
"Zen URL resolution failed (expected if version doesn't exist): {}",
e
);
}
}
}
@@ -260,19 +275,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_mullvad_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "placeholder".to_string(),
filename: "mullvad-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::MullvadBrowser,
"14.5a6",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::MullvadBrowser, "14.5a6", &download_info)
.await;
match result {
Ok(url) => {
assert!(url.contains("github.com/mullvad/mullvad-browser"));
@@ -280,7 +293,10 @@ mod tests {
println!("Mullvad download URL resolved: {}", url);
}
Err(e) => {
println!("Mullvad URL resolution failed (expected if version doesn't exist): {}", e);
println!(
"Mullvad URL resolution failed (expected if version doesn't exist): {}",
e
);
}
}
}
@@ -288,19 +304,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_firefox_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "https://download.mozilla.org/?product=firefox-139.0&os=osx&lang=en-US".to_string(),
filename: "firefox-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::Firefox,
"139.0",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::Firefox, "139.0", &download_info)
.await;
match result {
Ok(url) => {
assert_eq!(url, download_info.url);
@@ -315,19 +329,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_chromium_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "https://commondatastorage.googleapis.com/chromium-browser-snapshots/Mac/1465660/chrome-mac.zip".to_string(),
filename: "chromium-test.zip".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::Chromium,
"1465660",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::Chromium, "1465660", &download_info)
.await;
match result {
Ok(url) => {
assert_eq!(url, download_info.url);
@@ -342,19 +354,17 @@ mod tests {
#[tokio::test]
async fn test_resolve_tor_download_url() {
let downloader = Downloader::new();
let download_info = DownloadInfo {
url: "https://archive.torproject.org/tor-package-archive/torbrowser/14.0.4/tor-browser-macos-14.0.4.dmg".to_string(),
filename: "tor-test.dmg".to_string(),
is_archive: true,
};
let result = downloader.resolve_download_url(
BrowserType::TorBrowser,
"14.0.4",
&download_info
).await;
let result = downloader
.resolve_download_url(BrowserType::TorBrowser, "14.0.4", &download_info)
.await;
match result {
Ok(url) => {
assert_eq!(url, download_info.url);
@@ -365,4 +375,4 @@ mod tests {
}
}
}
}
}
+239 -216
View File
@@ -1,258 +1,281 @@
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use directories::BaseDirs;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DownloadedBrowserInfo {
pub browser: String,
pub version: String,
pub download_date: u64,
pub file_path: PathBuf,
pub verified: bool,
pub actual_version: Option<String>, // For browsers like Chromium where we track the actual version
pub browser: String,
pub version: String,
pub download_date: u64,
pub file_path: PathBuf,
pub verified: bool,
pub actual_version: Option<String>, // For browsers like Chromium where we track the actual version
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct DownloadedBrowsersRegistry {
pub browsers: HashMap<String, HashMap<String, DownloadedBrowserInfo>>, // browser -> version -> info
pub browsers: HashMap<String, HashMap<String, DownloadedBrowserInfo>>, // browser -> version -> info
}
impl DownloadedBrowsersRegistry {
pub fn new() -> Self {
Self::default()
pub fn new() -> Self {
Self::default()
}
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let registry_path = Self::get_registry_path()?;
if !registry_path.exists() {
return Ok(Self::new());
}
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let registry_path = Self::get_registry_path()?;
if !registry_path.exists() {
return Ok(Self::new());
}
let content = fs::read_to_string(&registry_path)?;
let registry: DownloadedBrowsersRegistry = serde_json::from_str(&content)?;
Ok(registry)
}
let content = fs::read_to_string(&registry_path)?;
let registry: DownloadedBrowsersRegistry = serde_json::from_str(&content)?;
Ok(registry)
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let registry_path = Self::get_registry_path()?;
// Ensure parent directory exists
if let Some(parent) = registry_path.parent() {
fs::create_dir_all(parent)?;
}
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let registry_path = Self::get_registry_path()?;
// Ensure parent directory exists
if let Some(parent) = registry_path.parent() {
fs::create_dir_all(parent)?;
}
let content = serde_json::to_string_pretty(self)?;
fs::write(&registry_path, content)?;
Ok(())
}
let content = serde_json::to_string_pretty(self)?;
fs::write(&registry_path, content)?;
Ok(())
fn get_registry_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut path = base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("data");
path.push("downloaded_browsers.json");
Ok(path)
}
pub fn add_browser(&mut self, info: DownloadedBrowserInfo) {
self
.browsers
.entry(info.browser.clone())
.or_insert_with(HashMap::new)
.insert(info.version.clone(), info);
}
pub fn remove_browser(&mut self, browser: &str, version: &str) -> Option<DownloadedBrowserInfo> {
self.browsers.get_mut(browser)?.remove(version)
}
pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool {
self
.browsers
.get(browser)
.and_then(|versions| versions.get(version))
.map(|info| info.verified)
.unwrap_or(false)
}
pub fn get_downloaded_versions(&self, browser: &str) -> Vec<String> {
self
.browsers
.get(browser)
.map(|versions| {
versions
.iter()
.filter(|(_, info)| info.verified)
.map(|(version, _)| version.clone())
.collect()
})
.unwrap_or_default()
}
pub fn mark_download_started(&mut self, browser: &str, version: &str, file_path: PathBuf) {
let info = DownloadedBrowserInfo {
browser: browser.to_string(),
version: version.to_string(),
download_date: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
file_path,
verified: false,
actual_version: None,
};
self.add_browser(info);
}
pub fn mark_download_completed_with_actual_version(
&mut self,
browser: &str,
version: &str,
actual_version: Option<String>,
) -> Result<(), String> {
if let Some(info) = self
.browsers
.get_mut(browser)
.and_then(|versions| versions.get_mut(version))
{
info.verified = true;
info.actual_version = actual_version;
Ok(())
} else {
Err(format!(
"Browser {}:{} not found in registry",
browser, version
))
}
}
fn get_registry_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut path = base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" });
path.push("data");
path.push("downloaded_browsers.json");
Ok(path)
}
pub fn add_browser(&mut self, info: DownloadedBrowserInfo) {
self.browsers
.entry(info.browser.clone())
.or_insert_with(HashMap::new)
.insert(info.version.clone(), info);
}
pub fn remove_browser(&mut self, browser: &str, version: &str) -> Option<DownloadedBrowserInfo> {
self.browsers
.get_mut(browser)?
.remove(version)
}
pub fn is_browser_downloaded(&self, browser: &str, version: &str) -> bool {
self.browsers
.get(browser)
.and_then(|versions| versions.get(version))
.map(|info| info.verified)
.unwrap_or(false)
}
pub fn get_downloaded_versions(&self, browser: &str) -> Vec<String> {
self.browsers
.get(browser)
.map(|versions| {
versions
.iter()
.filter(|(_, info)| info.verified)
.map(|(version, _)| version.clone())
.collect()
})
.unwrap_or_default()
}
pub fn mark_download_started(&mut self, browser: &str, version: &str, file_path: PathBuf) {
let info = DownloadedBrowserInfo {
browser: browser.to_string(),
version: version.to_string(),
download_date: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
file_path,
verified: false,
actual_version: None,
};
self.add_browser(info);
}
pub fn mark_download_completed_with_actual_version(&mut self, browser: &str, version: &str, actual_version: Option<String>) -> Result<(), String> {
if let Some(info) = self.browsers
.get_mut(browser)
.and_then(|versions| versions.get_mut(version))
{
info.verified = true;
info.actual_version = actual_version;
Ok(())
pub fn cleanup_failed_download(
&mut self,
browser: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error>> {
if let Some(info) = self.remove_browser(browser, version) {
// Clean up any files that might have been left behind
if info.file_path.exists() {
if info.file_path.is_dir() {
fs::remove_dir_all(&info.file_path)?;
} else {
Err(format!("Browser {}:{} not found in registry", browser, version))
fs::remove_file(&info.file_path)?;
}
}
}
pub fn cleanup_failed_download(&mut self, browser: &str, version: &str) -> Result<(), Box<dyn std::error::Error>> {
if let Some(info) = self.remove_browser(browser, version) {
// Clean up any files that might have been left behind
if info.file_path.exists() {
if info.file_path.is_dir() {
fs::remove_dir_all(&info.file_path)?;
} else {
fs::remove_file(&info.file_path)?;
}
}
// Also clean up the browser directory if it exists
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut browser_dir = base_dirs.data_local_dir().to_path_buf();
browser_dir.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" });
browser_dir.push("binaries");
browser_dir.push(browser);
browser_dir.push(version);
if browser_dir.exists() {
fs::remove_dir_all(&browser_dir)?;
}
}
Ok(())
// Also clean up the browser directory if it exists
let base_dirs = BaseDirs::new().ok_or("Failed to get base directories")?;
let mut browser_dir = base_dirs.data_local_dir().to_path_buf();
browser_dir.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
browser_dir.push("binaries");
browser_dir.push(browser);
browser_dir.push(version);
if browser_dir.exists() {
fs::remove_dir_all(&browser_dir)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::*;
#[test]
fn test_registry_creation() {
let registry = DownloadedBrowsersRegistry::new();
assert!(registry.browsers.is_empty());
}
#[test]
fn test_registry_creation() {
let registry = DownloadedBrowsersRegistry::new();
assert!(registry.browsers.is_empty());
}
#[test]
fn test_add_and_get_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
};
#[test]
fn test_add_and_get_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
};
registry.add_browser(info.clone());
registry.add_browser(info.clone());
assert!(registry.is_browser_downloaded("firefox", "139.0"));
assert!(!registry.is_browser_downloaded("firefox", "140.0"));
assert!(!registry.is_browser_downloaded("chrome", "139.0"));
}
assert!(registry.is_browser_downloaded("firefox", "139.0"));
assert!(!registry.is_browser_downloaded("firefox", "140.0"));
assert!(!registry.is_browser_downloaded("chrome", "139.0"));
}
#[test]
fn test_get_downloaded_versions() {
let mut registry = DownloadedBrowsersRegistry::new();
let info1 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path1"),
verified: true,
actual_version: None,
};
let info2 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "140.0".to_string(),
download_date: 1234567891,
file_path: PathBuf::from("/test/path2"),
verified: false, // Not verified, should not be included
actual_version: None,
};
#[test]
fn test_get_downloaded_versions() {
let mut registry = DownloadedBrowsersRegistry::new();
let info3 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "141.0".to_string(),
download_date: 1234567892,
file_path: PathBuf::from("/test/path3"),
verified: true,
actual_version: None,
};
let info1 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path1"),
verified: true,
actual_version: None,
};
registry.add_browser(info1);
registry.add_browser(info2);
registry.add_browser(info3);
let info2 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "140.0".to_string(),
download_date: 1234567891,
file_path: PathBuf::from("/test/path2"),
verified: false, // Not verified, should not be included
actual_version: None,
};
let versions = registry.get_downloaded_versions("firefox");
assert_eq!(versions.len(), 2);
assert!(versions.contains(&"139.0".to_string()));
assert!(versions.contains(&"141.0".to_string()));
assert!(!versions.contains(&"140.0".to_string()));
}
let info3 = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "141.0".to_string(),
download_date: 1234567892,
file_path: PathBuf::from("/test/path3"),
verified: true,
actual_version: None,
};
#[test]
fn test_mark_download_lifecycle() {
let mut registry = DownloadedBrowsersRegistry::new();
// Mark download started
registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path"));
// Should not be considered downloaded yet
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
// Mark as completed
registry.mark_download_completed_with_actual_version("firefox", "139.0", Some("139.0".to_string())).unwrap();
// Now should be considered downloaded
assert!(registry.is_browser_downloaded("firefox", "139.0"));
}
registry.add_browser(info1);
registry.add_browser(info2);
registry.add_browser(info3);
#[test]
fn test_remove_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
};
let versions = registry.get_downloaded_versions("firefox");
assert_eq!(versions.len(), 2);
assert!(versions.contains(&"139.0".to_string()));
assert!(versions.contains(&"141.0".to_string()));
assert!(!versions.contains(&"140.0".to_string()));
}
registry.add_browser(info);
assert!(registry.is_browser_downloaded("firefox", "139.0"));
#[test]
fn test_mark_download_lifecycle() {
let mut registry = DownloadedBrowsersRegistry::new();
let removed = registry.remove_browser("firefox", "139.0");
assert!(removed.is_some());
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
}
}
// Mark download started
registry.mark_download_started("firefox", "139.0", PathBuf::from("/test/path"));
// Should not be considered downloaded yet
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
// Mark as completed
registry
.mark_download_completed_with_actual_version("firefox", "139.0", Some("139.0".to_string()))
.unwrap();
// Now should be considered downloaded
assert!(registry.is_browser_downloaded("firefox", "139.0"));
}
#[test]
fn test_remove_browser() {
let mut registry = DownloadedBrowsersRegistry::new();
let info = DownloadedBrowserInfo {
browser: "firefox".to_string(),
version: "139.0".to_string(),
download_date: 1234567890,
file_path: PathBuf::from("/test/path"),
verified: true,
actual_version: None,
};
registry.add_browser(info);
assert!(registry.is_browser_downloaded("firefox", "139.0"));
let removed = registry.remove_browser("firefox", "139.0");
assert!(removed.is_some());
assert!(!registry.is_browser_downloaded("firefox", "139.0"));
}
}
+22 -22
View File
@@ -3,8 +3,8 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use tauri::Emitter;
use crate::download::DownloadProgress;
use crate::browser::BrowserType;
use crate::download::DownloadProgress;
pub struct Extractor;
@@ -176,7 +176,7 @@ impl Extractor {
// Find the extracted .app directory or Chromium.app specifically
let mut app_path: Option<PathBuf> = None;
// First, try to find any .app file in the destination directory
if let Ok(entries) = fs::read_dir(dest_dir) {
for entry in entries {
@@ -197,7 +197,7 @@ impl Extractor {
let target_path = dest_dir.join(sub_path.file_name().unwrap());
fs::rename(&sub_path, &target_path)?;
app_path = Some(target_path);
// Clean up the now-empty subdirectory
let _ = fs::remove_dir_all(&path);
break;
@@ -247,16 +247,16 @@ mod tests {
let temp_dir = TempDir::new().unwrap();
let fake_archive = temp_dir.path().join("test.rar");
File::create(&fake_archive).unwrap();
// Create a mock app handle (this won't work in real tests without Tauri runtime)
// For now, we'll just test the logic without the actual extraction
// Test that unsupported formats return an error
let extension = fake_archive
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
assert_eq!(extension, "rar");
// We know this would fail with "Unsupported archive format: rar"
}
@@ -265,13 +265,13 @@ mod tests {
fn test_dmg_path_validation() {
let temp_dir = TempDir::new().unwrap();
let dmg_path = temp_dir.path().join("test.dmg");
// Test that we can identify DMG files correctly
let extension = dmg_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
assert_eq!(extension, "dmg");
}
@@ -279,13 +279,13 @@ mod tests {
fn test_zip_path_validation() {
let temp_dir = TempDir::new().unwrap();
let zip_path = temp_dir.path().join("test.zip");
// Test that we can identify ZIP files correctly
let extension = zip_path
.extension()
.and_then(|ext| ext.to_str())
.unwrap_or("");
assert_eq!(extension, "zip");
}
@@ -299,9 +299,9 @@ mod tests {
.unwrap()
.as_secs()
));
std::thread::sleep(std::time::Duration::from_millis(10));
let mount_point2 = std::env::temp_dir().join(format!(
"donut_mount_{}",
std::time::SystemTime::now()
@@ -309,7 +309,7 @@ mod tests {
.unwrap()
.as_secs()
));
// They should be different (or at least have the potential to be)
assert!(mount_point1.to_string_lossy().contains("donut_mount_"));
assert!(mount_point2.to_string_lossy().contains("donut_mount_"));
@@ -318,18 +318,18 @@ mod tests {
#[test]
fn test_app_path_detection() {
let temp_dir = TempDir::new().unwrap();
// Create a fake .app directory
let app_dir = temp_dir.path().join("TestApp.app");
std::fs::create_dir_all(&app_dir).unwrap();
// Test finding .app directories
let entries: Vec<_> = fs::read_dir(temp_dir.path())
.unwrap()
.filter_map(Result::ok)
.filter(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
.collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].file_name(), "TestApp.app");
}
@@ -337,17 +337,17 @@ mod tests {
#[test]
fn test_nested_app_detection() {
let temp_dir = TempDir::new().unwrap();
// Create a nested structure like Chromium
let chrome_dir = temp_dir.path().join("chrome-mac");
std::fs::create_dir_all(&chrome_dir).unwrap();
let app_dir = chrome_dir.join("Chromium.app");
std::fs::create_dir_all(&app_dir).unwrap();
// Test finding nested .app directories
let mut found_app = false;
if let Ok(entries) = fs::read_dir(temp_dir.path()) {
for entry in entries {
if let Ok(entry) = entry {
@@ -368,7 +368,7 @@ mod tests {
}
}
}
assert!(found_app);
}
}
}
+40 -31
View File
@@ -1,7 +1,7 @@
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{Manager, Emitter};
use tauri::{Emitter, Manager};
use tauri_plugin_deep_link::DeepLinkExt;
// Store pending URLs that need to be handled when the window is ready
@@ -23,28 +23,33 @@ mod version_updater;
extern crate lazy_static;
use browser_runner::{
check_browser_status, create_browser_profile, create_browser_profile_new, delete_profile,
download_browser, fetch_browser_versions, fetch_browser_versions_cached_first,
fetch_browser_versions_detailed, fetch_browser_versions_with_count, fetch_browser_versions_with_count_cached_first,
get_cached_browser_versions_detailed, get_downloaded_browser_versions, get_saved_mullvad_releases, get_supported_browsers, is_browser_downloaded, check_browser_exists,
kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile, should_update_browser_cache, update_profile_proxy,
update_profile_version,
check_browser_exists, check_browser_status, create_browser_profile, create_browser_profile_new,
delete_profile, download_browser, fetch_browser_versions, fetch_browser_versions_cached_first,
fetch_browser_versions_detailed, fetch_browser_versions_with_count,
fetch_browser_versions_with_count_cached_first, get_cached_browser_versions_detailed,
get_downloaded_browser_versions, get_saved_mullvad_releases, get_supported_browsers,
is_browser_downloaded, kill_browser_profile, launch_browser_profile, list_browser_profiles,
rename_profile, should_update_browser_cache, update_profile_proxy, update_profile_version,
};
use settings_manager::{
disable_default_browser_prompt, get_app_settings, save_app_settings,
should_show_settings_on_startup, get_table_sorting_settings, save_table_sorting_settings,
disable_default_browser_prompt, get_app_settings, get_table_sorting_settings, save_app_settings,
save_table_sorting_settings, should_show_settings_on_startup,
};
use default_browser::{is_default_browser, open_url_with_profile, set_as_default_browser, smart_open_url};
use default_browser::{
is_default_browser, open_url_with_profile, set_as_default_browser, smart_open_url,
};
use version_updater::{trigger_manual_version_update, get_version_update_status, get_version_updater, check_version_update_needed, force_version_update_check};
use version_updater::{
check_version_update_needed, force_version_update_check, get_version_update_status,
get_version_updater, trigger_manual_version_update,
};
use auto_updater::{
check_for_browser_updates, start_browser_update, complete_browser_update,
is_browser_disabled_for_update, dismiss_update_notification,
complete_browser_update_with_auto_update,
mark_auto_update_download, remove_auto_update_download, is_auto_update_download,
check_for_browser_updates, complete_browser_update, complete_browser_update_with_auto_update,
dismiss_update_notification, is_auto_update_download, is_browser_disabled_for_update,
mark_auto_update_download, remove_auto_update_download, start_browser_update,
};
#[tauri::command]
@@ -57,13 +62,14 @@ fn greet() -> String {
#[tauri::command]
async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), String> {
println!("handle_url_open called with URL: {}", url);
// Check if the main window exists and is ready
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
// Window is visible, emit event directly
println!("Main window is visible, emitting show-profile-selector event");
app.emit("show-profile-selector", url.clone())
app
.emit("show-profile-selector", url.clone())
.map_err(|e| format!("Failed to emit URL open event: {}", e))?;
let _ = window.show();
let _ = window.set_focus();
@@ -79,7 +85,7 @@ async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), Strin
let mut pending = PENDING_URLS.lock().unwrap();
pending.push(url);
}
Ok(())
}
@@ -91,10 +97,13 @@ async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result<bo
pending.clear(); // Clear after getting them
urls
};
if !pending_urls.is_empty() {
println!("Handling {} pending URLs from frontend request", pending_urls.len());
println!(
"Handling {} pending URLs from frontend request",
pending_urls.len()
);
for url in pending_urls {
println!("Emitting show-profile-selector event for URL: {}", url);
if let Err(e) = app_handle.emit("show-profile-selector", url.clone()) {
@@ -102,10 +111,10 @@ async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result<bo
return Err(format!("Failed to emit URL event: {}", e));
}
}
return Ok(true);
}
Ok(false)
}
@@ -119,13 +128,13 @@ pub fn run() {
.setup(|app| {
// Set up deep link handler
let handle = app.handle().clone();
#[cfg(any(windows, target_os = "linux"))]
{
// For Windows and Linux, register all deep links at runtime for development
app.deep_link().register_all()?;
}
// Handle deep links - this works for both scenarios:
// 1. App is running and URL is opened
// 2. App is not running and URL causes app to launch
@@ -136,10 +145,10 @@ pub fn run() {
for url in urls {
let url_string = url.to_string();
println!("Deep link received: {}", url_string);
// Clone the handle for each async task
let handle_clone = handle.clone();
// Handle the URL asynchronously
tauri::async_runtime::spawn(async move {
if let Err(e) = handle_url_open(handle_clone, url_string.clone()).await {
@@ -155,14 +164,14 @@ pub fn run() {
tauri::async_runtime::spawn(async move {
let version_updater = get_version_updater();
let mut updater_guard = version_updater.lock().await;
// Set the app handle
updater_guard.set_app_handle(app_handle).await;
// Start the background updates
updater_guard.start_background_updates().await;
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -173,7 +182,7 @@ pub fn run() {
is_browser_downloaded,
check_browser_exists,
create_browser_profile_new,
create_browser_profile, // Keep for backward compatibility
create_browser_profile, // Keep for backward compatibility
list_browser_profiles,
launch_browser_profile,
fetch_browser_versions,
-2
View File
@@ -174,8 +174,6 @@ impl ProxyManager {
})
}
// Get stored proxy info for a profile
pub fn get_profile_proxy_info(&self, profile_name: &str) -> Option<(String, u16)> {
let profile_proxies = self.profile_proxies.lock().unwrap();
+20 -7
View File
@@ -66,7 +66,11 @@ impl SettingsManager {
pub fn get_settings_dir(&self) -> PathBuf {
let mut path = self.base_dirs.data_local_dir().to_path_buf();
path.push(if cfg!(debug_assertions) { "DonutBrowserDev" } else { "DonutBrowser" });
path.push(if cfg!(debug_assertions) {
"DonutBrowserDev"
} else {
"DonutBrowser"
});
path.push("settings");
path
}
@@ -88,25 +92,31 @@ impl SettingsManager {
}
let content = fs::read_to_string(&settings_file)?;
// Parse the settings file - serde will use default values for missing fields
match serde_json::from_str::<AppSettings>(&content) {
Ok(settings) => {
// Save the settings back to ensure any missing fields are written with defaults
if let Err(e) = self.save_settings(&settings) {
eprintln!("Warning: Failed to update settings file with defaults: {}", e);
eprintln!(
"Warning: Failed to update settings file with defaults: {}",
e
);
}
Ok(settings)
}
Err(e) => {
eprintln!("Warning: Failed to parse settings file, using defaults: {}", e);
eprintln!(
"Warning: Failed to parse settings file, using defaults: {}",
e
);
let default_settings = AppSettings::default();
// Try to save default settings to fix the corrupted file
if let Err(save_error) = self.save_settings(&default_settings) {
eprintln!("Warning: Failed to save default settings: {}", save_error);
}
Ok(default_settings)
}
}
@@ -136,7 +146,10 @@ impl SettingsManager {
Ok(sorting)
}
pub fn save_table_sorting(&self, sorting: &TableSortingSettings) -> Result<(), Box<dyn std::error::Error>> {
pub fn save_table_sorting(
&self,
sorting: &TableSortingSettings,
) -> Result<(), Box<dyn std::error::Error>> {
let settings_dir = self.get_settings_dir();
create_dir_all(&settings_dir)?;
File diff suppressed because it is too large Load Diff