diff --git a/package.json b/package.json index 9f78f0f..e7e2f22 100644 --- a/package.json +++ b/package.json @@ -93,9 +93,9 @@ "biome check --fix" ], "src-tauri/**/*.rs": [ - "cd src-tauri && cargo fmt --all", - "cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings -D clippy::all", - "cd src-tauri && cargo test" + "bash -c 'cd src-tauri && cargo fmt --all'", + "bash -c 'cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings -D clippy::all'", + "bash -c 'cd src-tauri && cargo test --lib'" ] } } diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index 852aa0d..ca32b14 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -1162,6 +1162,7 @@ async fn delete_proxy( request_body = RunProfileRequest, responses( (status = 200, description = "Profile launched successfully", body = RunProfileResponse), + (status = 400, description = "Cannot launch cross-OS profile"), (status = 401, description = "Unauthorized"), (status = 404, description = "Profile not found"), (status = 500, description = "Internal server error") @@ -1189,6 +1190,10 @@ async fn run_profile( .find(|p| p.id.to_string() == id) .ok_or(StatusCode::NOT_FOUND)?; + if profile.is_cross_os() { + return Err(StatusCode::BAD_REQUEST); + } + // Generate a random port for remote debugging let remote_debugging_port = rand::random::().saturating_add(9000).max(9000); diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index d53c730..daf1bce 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -2401,6 +2401,14 @@ impl BrowserRunner { .find(|p| p.id.to_string() == profile_id) .ok_or_else(|| format!("Profile '{profile_id}' not found"))?; + if profile.is_cross_os() { + return Err(format!( + "Cannot open URL with profile '{}': it was created on {} and is not supported on this system", + profile.name, + profile.host_os.as_deref().unwrap_or("unknown") + )); + } + log::info!("Opening URL '{url}' with profile '{profile_id}'"); // Use launch_or_open_url which handles both launching new instances and opening in existing ones @@ -2429,6 +2437,14 @@ pub async fn launch_browser_profile( profile.id ); + if profile.is_cross_os() { + return Err(format!( + "Cannot launch profile '{}': it was created on {} and is not supported on this system", + profile.name, + profile.host_os.as_deref().unwrap_or("unknown") + )); + } + let browser_runner = BrowserRunner::instance(); // Store the internal proxy settings for passing to launch_browser @@ -2664,6 +2680,14 @@ pub async fn launch_browser_profile_with_debugging( remote_debugging_port: Option, headless: bool, ) -> Result { + if profile.is_cross_os() { + return Err(format!( + "Cannot launch profile '{}': it was created on {} and is not supported on this system", + profile.name, + profile.host_os.as_deref().unwrap_or("unknown") + )); + } + let browser_runner = BrowserRunner::instance(); browser_runner .launch_browser_with_debugging(app_handle, &profile, url, remote_debugging_port, headless) diff --git a/src-tauri/src/cloud_auth.rs b/src-tauri/src/cloud_auth.rs index cb6c380..6af5c03 100644 --- a/src-tauri/src/cloud_auth.rs +++ b/src-tauri/src/cloud_auth.rs @@ -611,6 +611,21 @@ impl CloudAuthManager { } } + /// Non-async version that uses try_lock, defaults to false if lock can't be acquired. + pub fn has_active_paid_subscription_sync(&self) -> bool { + match self.state.try_lock() { + Ok(state) => match &*state { + Some(auth) => { + auth.user.plan != "free" + && (auth.user.subscription_status == "active" + || auth.user.plan_period.as_deref() == Some("lifetime")) + } + None => false, + }, + Err(_) => false, + } + } + pub async fn is_fingerprint_os_allowed(&self, fingerprint_os: Option<&str>) -> bool { let host_os = crate::profile::types::get_host_os(); match fingerprint_os { diff --git a/src-tauri/src/downloaded_browsers_registry.rs b/src-tauri/src/downloaded_browsers_registry.rs index 2a46018..75d03dd 100644 --- a/src-tauri/src/downloaded_browsers_registry.rs +++ b/src-tauri/src/downloaded_browsers_registry.rs @@ -312,6 +312,30 @@ impl DownloadedBrowsersRegistry { } } + // Filter out versions that would leave a browser with zero versions in the registry + { + let data = self.data.lock().unwrap(); + let mut removal_counts: std::collections::HashMap = + std::collections::HashMap::new(); + for (browser, _) in &to_remove { + *removal_counts.entry(browser.clone()).or_insert(0) += 1; + } + to_remove.retain(|(browser, version)| { + let total = data + .browsers + .get(browser.as_str()) + .map(|v| v.len()) + .unwrap_or(0); + let removing = *removal_counts.get(browser.as_str()).unwrap_or(&0); + if removing >= total { + log::info!("Keeping last available version: {browser} {version}"); + *removal_counts.get_mut(browser.as_str()).unwrap() -= 1; + return false; + } + true + }); + } + // Remove unused binaries and their version folders for (browser, version) in to_remove { if let Err(e) = self.cleanup_failed_download(&browser, &version) { @@ -1164,6 +1188,58 @@ mod tests { ); } + #[test] + fn test_last_version_kept_during_cleanup() { + let registry = DownloadedBrowsersRegistry::new(); + + // Add a single version for "firefox" + registry.add_browser(DownloadedBrowserInfo { + browser: "firefox".to_string(), + version: "139.0".to_string(), + file_path: PathBuf::from("/test/firefox/139.0"), + }); + + // Add two versions for "chromium" + registry.add_browser(DownloadedBrowserInfo { + browser: "chromium".to_string(), + version: "120.0".to_string(), + file_path: PathBuf::from("/test/chromium/120.0"), + }); + registry.add_browser(DownloadedBrowserInfo { + browser: "chromium".to_string(), + version: "121.0".to_string(), + file_path: PathBuf::from("/test/chromium/121.0"), + }); + + // No active or running profiles + let result = registry + .cleanup_unused_binaries_internal(&[], &[]) + .expect("cleanup should succeed"); + + // firefox 139.0 should be kept (last version), chromium should lose one but keep one + // The exact one kept depends on iteration order, but at least one must remain + assert!( + !result.contains(&"firefox 139.0".to_string()), + "Last version of firefox should not be cleaned up" + ); + // At most one chromium version should have been cleaned up + let chromium_cleaned: Vec<_> = result + .iter() + .filter(|r| r.starts_with("chromium")) + .collect(); + assert!( + chromium_cleaned.len() <= 1, + "At most one chromium version should be cleaned up, got: {:?}", + chromium_cleaned + ); + + // Verify firefox is still registered + assert!( + registry.is_browser_registered("firefox", "139.0"), + "Last firefox version should still be registered" + ); + } + #[test] fn test_is_browser_registered_vs_downloaded() { let registry = DownloadedBrowsersRegistry::new(); diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index 8a17e96..feb53d8 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -434,6 +434,63 @@ impl Downloader { Ok(()) } + fn configure_camoufox_search_engine( + &self, + browser_dir: &Path, + ) -> Result<(), Box> { + let policies_path = browser_dir.join("distribution").join("policies.json"); + + if !policies_path.exists() { + if let Some(parent) = policies_path.parent() { + std::fs::create_dir_all(parent)?; + } + let policies = serde_json::json!({ + "policies": { + "SearchEngines": { + "Default": "DuckDuckGo" + } + } + }); + std::fs::write(&policies_path, serde_json::to_string_pretty(&policies)?)?; + log::info!("Created policies.json with DuckDuckGo as default search engine"); + return Ok(()); + } + + let content = std::fs::read_to_string(&policies_path)?; + let mut policies: serde_json::Value = serde_json::from_str(&content)?; + + let current_default = policies + .get("policies") + .and_then(|p| p.get("SearchEngines")) + .and_then(|se| se.get("Default")) + .and_then(|d| d.as_str()) + .unwrap_or(""); + + if current_default != "None" { + log::info!( + "Camoufox search engine already configured to '{}', not overwriting", + current_default + ); + return Ok(()); + } + + if let Some(policies_obj) = policies.get_mut("policies") { + if let Some(se) = policies_obj.get_mut("SearchEngines") { + se["Default"] = serde_json::json!("DuckDuckGo"); + + if let Some(remove_arr) = se.get_mut("Remove").and_then(|r| r.as_array_mut()) { + remove_arr.retain(|v| v.as_str() != Some("DuckDuckGo")); + } + } + } + + let updated = serde_json::to_string_pretty(&policies)?; + std::fs::write(&policies_path, updated)?; + + log::info!("Updated Camoufox search engine from 'None' to DuckDuckGo"); + Ok(()) + } + pub async fn download_browser( &self, _app_handle: &tauri::AppHandle, @@ -975,7 +1032,10 @@ impl Downloader { .await { log::warn!("Failed to create version.json for Camoufox: {e}"); - // Don't fail the download if version.json creation fails + } + + if let Err(e) = self.configure_camoufox_search_engine(&browser_dir) { + log::warn!("Failed to configure Camoufox search engine: {e}"); } } diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index b15bebc..f268c83 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -593,7 +593,11 @@ impl Extractor { } } - log::info!("ZIP extraction completed. Searching for executable..."); + log::info!("ZIP extraction completed."); + + self.flatten_single_directory_archive(dest_dir)?; + + log::info!("Searching for executable..."); self .find_extracted_executable(dest_dir) .await @@ -617,7 +621,9 @@ impl Extractor { // Set executable permissions for extracted files self.set_executable_permissions_recursive(dest_dir).await?; - log::info!("tar.gz extraction completed. Searching for executable..."); + log::info!("tar.gz extraction completed."); + self.flatten_single_directory_archive(dest_dir)?; + log::info!("Searching for executable..."); self.find_extracted_executable(dest_dir).await } @@ -638,7 +644,9 @@ impl Extractor { // Set executable permissions for extracted files self.set_executable_permissions_recursive(dest_dir).await?; - log::info!("tar.bz2 extraction completed. Searching for executable..."); + log::info!("tar.bz2 extraction completed."); + self.flatten_single_directory_archive(dest_dir)?; + log::info!("Searching for executable..."); self.find_extracted_executable(dest_dir).await } @@ -673,7 +681,9 @@ impl Extractor { // Set executable permissions for extracted files self.set_executable_permissions_recursive(dest_dir).await?; - log::info!("tar.xz extraction completed. Searching for executable..."); + log::info!("tar.xz extraction completed."); + self.flatten_single_directory_archive(dest_dir)?; + log::info!("Searching for executable..."); self.find_extracted_executable(dest_dir).await } @@ -691,7 +701,9 @@ impl Extractor { extractor.to(dest_dir); } - log::info!("MSI extraction completed. Searching for executable..."); + log::info!("MSI extraction completed."); + self.flatten_single_directory_archive(dest_dir)?; + log::info!("Searching for executable..."); self.find_extracted_executable(dest_dir).await } @@ -778,6 +790,71 @@ impl Extractor { self.find_extracted_executable(dest_dir).await } + fn flatten_single_directory_archive( + &self, + dest_dir: &Path, + ) -> Result<(), Box> { + let entries: Vec<_> = fs::read_dir(dest_dir)?.filter_map(|e| e.ok()).collect(); + + let archive_extensions = ["zip", "tar", "xz", "gz", "bz2", "dmg", "msi", "exe"]; + + let mut dirs = Vec::new(); + let mut has_non_archive_files = false; + + for entry in &entries { + let path = entry.path(); + if path.is_dir() { + dirs.push(path); + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if !archive_extensions.contains(&ext.to_lowercase().as_str()) { + has_non_archive_files = true; + } + } else { + has_non_archive_files = true; + } + } + + if dirs.len() == 1 && !has_non_archive_files { + let single_dir = &dirs[0]; + log::info!( + "Flattening single-directory archive: moving contents of {} to {}", + single_dir.display(), + dest_dir.display() + ); + + let inner_entries: Vec<_> = fs::read_dir(single_dir)?.filter_map(|e| e.ok()).collect(); + + for entry in inner_entries { + let source = entry.path(); + let file_name = match source.file_name() { + Some(name) => name.to_owned(), + None => continue, + }; + let target = dest_dir.join(&file_name); + fs::rename(&source, &target).map_err(|e| { + format!( + "Failed to move {} to {}: {}", + source.display(), + target.display(), + e + ) + })?; + } + + fs::remove_dir(single_dir).map_err(|e| { + format!( + "Failed to remove empty directory {}: {}", + single_dir.display(), + e + ) + })?; + + log::info!("Successfully flattened archive directory structure"); + } + + Ok(()) + } + async fn find_extracted_executable( &self, dest_dir: &Path, diff --git a/src-tauri/src/group_manager.rs b/src-tauri/src/group_manager.rs index b475a75..8fef804 100644 --- a/src-tauri/src/group_manager.rs +++ b/src-tauri/src/group_manager.rs @@ -119,10 +119,11 @@ impl GroupManager { return Err(format!("Group with name '{name}' already exists").into()); } + let sync_enabled = crate::cloud_auth::CLOUD_AUTH.has_active_paid_subscription_sync(); let group = ProfileGroup { id: uuid::Uuid::new_v4().to_string(), name, - sync_enabled: false, + sync_enabled, last_sync: None, }; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b53a89f..7143f4e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -82,9 +82,9 @@ use settings_manager::{ }; use sync::{ - is_group_in_use_by_synced_profile, is_proxy_in_use_by_synced_profile, - is_vpn_in_use_by_synced_profile, request_profile_sync, set_group_sync_enabled, - set_profile_sync_enabled, set_proxy_sync_enabled, set_vpn_sync_enabled, + enable_sync_for_all_entities, get_unsynced_entity_counts, is_group_in_use_by_synced_profile, + is_proxy_in_use_by_synced_profile, is_vpn_in_use_by_synced_profile, request_profile_sync, + set_group_sync_enabled, set_profile_sync_enabled, set_proxy_sync_enabled, set_vpn_sync_enabled, }; use tag_manager::get_all_tags; @@ -1309,6 +1309,8 @@ pub fn run() { is_group_in_use_by_synced_profile, set_vpn_sync_enabled, is_vpn_in_use_by_synced_profile, + get_unsynced_entity_counts, + enable_sync_for_all_entities, read_profile_cookies, copy_profile_cookies, check_wayfern_terms_accepted, diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index 0f9db28..87f5b99 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -117,11 +117,12 @@ pub struct StoredProxy { impl StoredProxy { pub fn new(name: String, proxy_settings: ProxySettings) -> Self { + let sync_enabled = crate::cloud_auth::CLOUD_AUTH.has_active_paid_subscription_sync(); Self { id: uuid::Uuid::new_v4().to_string(), name, proxy_settings, - sync_enabled: false, + sync_enabled, last_sync: None, is_cloud_managed: false, is_cloud_derived: false, diff --git a/src-tauri/src/sync/engine.rs b/src-tauri/src/sync/engine.rs index c216b61..3ab1af8 100644 --- a/src-tauri/src/sync/engine.rs +++ b/src-tauri/src/sync/engine.rs @@ -1823,3 +1823,87 @@ pub async fn set_vpn_sync_enabled( pub fn is_vpn_in_use_by_synced_profile(vpn_id: String) -> bool { is_vpn_used_by_synced_profile(&vpn_id) } + +#[derive(Debug, Clone, serde::Serialize)] +pub struct UnsyncedEntityCounts { + pub proxies: usize, + pub groups: usize, + pub vpns: usize, +} + +#[tauri::command] +pub fn get_unsynced_entity_counts() -> Result { + let proxy_count = { + let proxies = crate::proxy_manager::PROXY_MANAGER.get_stored_proxies(); + proxies + .iter() + .filter(|p| !p.sync_enabled && !p.is_cloud_managed) + .count() + }; + + let group_count = { + let gm = crate::group_manager::GROUP_MANAGER.lock().unwrap(); + let groups = gm + .get_all_groups() + .map_err(|e| format!("Failed to get groups: {e}"))?; + groups.iter().filter(|g| !g.sync_enabled).count() + }; + + let vpn_count = { + let storage = crate::vpn::VPN_STORAGE.lock().unwrap(); + let configs = storage + .list_configs() + .map_err(|e| format!("Failed to list VPN configs: {e}"))?; + configs.iter().filter(|c| !c.sync_enabled).count() + }; + + Ok(UnsyncedEntityCounts { + proxies: proxy_count, + groups: group_count, + vpns: vpn_count, + }) +} + +#[tauri::command] +pub async fn enable_sync_for_all_entities(app_handle: tauri::AppHandle) -> Result<(), String> { + // Enable sync for all unsynced proxies + { + let proxies = crate::proxy_manager::PROXY_MANAGER.get_stored_proxies(); + for proxy in &proxies { + if !proxy.sync_enabled && !proxy.is_cloud_managed { + set_proxy_sync_enabled(app_handle.clone(), proxy.id.clone(), true).await?; + } + } + } + + // Enable sync for all unsynced groups + { + let groups = { + let gm = crate::group_manager::GROUP_MANAGER.lock().unwrap(); + gm.get_all_groups() + .map_err(|e| format!("Failed to get groups: {e}"))? + }; + for group in &groups { + if !group.sync_enabled { + set_group_sync_enabled(app_handle.clone(), group.id.clone(), true).await?; + } + } + } + + // Enable sync for all unsynced VPNs + { + let configs = { + let storage = crate::vpn::VPN_STORAGE.lock().unwrap(); + storage + .list_configs() + .map_err(|e| format!("Failed to list VPN configs: {e}"))? + }; + for config in &configs { + if !config.sync_enabled { + set_vpn_sync_enabled(app_handle.clone(), config.id.clone(), true).await?; + } + } + } + + Ok(()) +} diff --git a/src-tauri/src/sync/mod.rs b/src-tauri/src/sync/mod.rs index 658c596..45e9b4e 100644 --- a/src-tauri/src/sync/mod.rs +++ b/src-tauri/src/sync/mod.rs @@ -7,12 +7,12 @@ pub mod types; pub use client::SyncClient; pub use engine::{ - enable_group_sync_if_needed, enable_proxy_sync_if_needed, enable_vpn_sync_if_needed, - is_group_in_use_by_synced_profile, is_group_used_by_synced_profile, - is_proxy_in_use_by_synced_profile, is_proxy_used_by_synced_profile, - is_vpn_in_use_by_synced_profile, is_vpn_used_by_synced_profile, request_profile_sync, - set_group_sync_enabled, set_profile_sync_enabled, set_proxy_sync_enabled, set_vpn_sync_enabled, - sync_profile, trigger_sync_for_profile, SyncEngine, + enable_group_sync_if_needed, enable_proxy_sync_if_needed, enable_sync_for_all_entities, + enable_vpn_sync_if_needed, get_unsynced_entity_counts, is_group_in_use_by_synced_profile, + is_group_used_by_synced_profile, is_proxy_in_use_by_synced_profile, + is_proxy_used_by_synced_profile, is_vpn_in_use_by_synced_profile, is_vpn_used_by_synced_profile, + request_profile_sync, set_group_sync_enabled, set_profile_sync_enabled, set_proxy_sync_enabled, + set_vpn_sync_enabled, sync_profile, trigger_sync_for_profile, SyncEngine, }; pub use manifest::{compute_diff, generate_manifest, HashCache, ManifestDiff, SyncManifest}; pub use scheduler::{get_global_scheduler, set_global_scheduler, SyncScheduler}; diff --git a/src-tauri/src/vpn/storage.rs b/src-tauri/src/vpn/storage.rs index a3d5ed2..edc35f9 100644 --- a/src-tauri/src/vpn/storage.rs +++ b/src-tauri/src/vpn/storage.rs @@ -328,6 +328,7 @@ impl VpnStorage { } let id = Uuid::new_v4().to_string(); + let sync_enabled = crate::cloud_auth::CLOUD_AUTH.has_active_paid_subscription_sync(); let config = VpnConfig { id, @@ -336,7 +337,7 @@ impl VpnStorage { config_data: config_data.to_string(), created_at: Utc::now().timestamp(), last_used: None, - sync_enabled: false, + sync_enabled, last_sync: None, }; @@ -396,6 +397,7 @@ impl VpnStorage { let base = filename.trim_end_matches(".conf").trim_end_matches(".ovpn"); format!("{} ({})", base, vpn_type) }); + let sync_enabled = crate::cloud_auth::CLOUD_AUTH.has_active_paid_subscription_sync(); let config = VpnConfig { id, @@ -404,7 +406,7 @@ impl VpnStorage { config_data: content.to_string(), created_at: Utc::now().timestamp(), last_used: None, - sync_enabled: false, + sync_enabled, last_sync: None, }; diff --git a/src/app/page.tsx b/src/app/page.tsx index e76fe5e..ccda8d0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -23,6 +23,7 @@ import { ProfileSyncDialog } from "@/components/profile-sync-dialog"; import { ProxyAssignmentDialog } from "@/components/proxy-assignment-dialog"; import { ProxyManagementDialog } from "@/components/proxy-management-dialog"; import { SettingsDialog } from "@/components/settings-dialog"; +import { SyncAllDialog } from "@/components/sync-all-dialog"; import { SyncConfigDialog } from "@/components/sync-config-dialog"; import { WayfernTermsDialog } from "@/components/wayfern-terms-dialog"; import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications"; @@ -143,6 +144,7 @@ export default function Home() { useState(false); const [isBulkDeleting, setIsBulkDeleting] = useState(false); const [syncConfigDialogOpen, setSyncConfigDialogOpen] = useState(false); + const [syncAllDialogOpen, setSyncAllDialogOpen] = useState(false); const [profileSyncDialogOpen, setProfileSyncDialogOpen] = useState(false); const [currentProfileForSync, setCurrentProfileForSync] = useState(null); @@ -1118,7 +1120,17 @@ export default function Home() { setSyncConfigDialogOpen(false)} + onClose={(loginOccurred) => { + setSyncConfigDialogOpen(false); + if (loginOccurred) { + setSyncAllDialogOpen(true); + } + }} + /> + + setSyncAllDialogOpen(false)} />
-
- Chromium (Wayfern) -
+
Wayfern
Anti-Detect Browser
@@ -580,9 +578,7 @@ export function CreateProfileDialog({ })()}
-
- Firefox (Camoufox) -
+
Camoufox
Anti-Detect Browser
diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index a107105..bfb0e1e 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -1595,7 +1595,10 @@ export function ProfilesDataTable({ -

Created on {osName} - view only

+

+ This profile was created on {osName} and is not supported on + this system +

); @@ -1608,7 +1611,12 @@ export function ProfilesDataTable({ : "another OS"; return ( Created on {osName} - view only

} + content={ +

+ This profile was created on {osName} and is not supported on + this system +

+ } sideOffset={4} horizontalOffset={8} > diff --git a/src/components/sync-all-dialog.tsx b/src/components/sync-all-dialog.tsx new file mode 100644 index 0000000..5c21aa3 --- /dev/null +++ b/src/components/sync-all-dialog.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { LoadingButton } from "@/components/loading-button"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; + +interface UnsyncedEntityCounts { + proxies: number; + groups: number; + vpns: number; +} + +interface SyncAllDialogProps { + isOpen: boolean; + onClose: () => void; +} + +export function SyncAllDialog({ isOpen, onClose }: SyncAllDialogProps) { + const { t } = useTranslation(); + const [counts, setCounts] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isEnabling, setIsEnabling] = useState(false); + + const loadCounts = useCallback(async () => { + setIsLoading(true); + try { + const result = await invoke( + "get_unsynced_entity_counts", + ); + setCounts(result); + } catch (error) { + console.error("Failed to get unsynced entity counts:", error); + setCounts(null); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + if (isOpen) { + void loadCounts(); + } + }, [isOpen, loadCounts]); + + const handleEnableAll = useCallback(async () => { + setIsEnabling(true); + try { + await invoke("enable_sync_for_all_entities"); + showSuccessToast(t("syncAll.success")); + onClose(); + } catch (error) { + console.error("Failed to enable sync for all entities:", error); + showErrorToast(String(error)); + } finally { + setIsEnabling(false); + } + }, [onClose, t]); + + const totalCount = + (counts?.proxies ?? 0) + (counts?.groups ?? 0) + (counts?.vpns ?? 0); + + // Don't show if there's nothing to sync + if (!isLoading && totalCount === 0) { + return null; + } + + const parts: string[] = []; + if (counts?.proxies && counts.proxies > 0) { + parts.push(t("syncAll.proxies", { count: counts.proxies })); + } + if (counts?.groups && counts.groups > 0) { + parts.push(t("syncAll.groups", { count: counts.groups })); + } + if (counts?.vpns && counts.vpns > 0) { + parts.push(t("syncAll.vpns", { count: counts.vpns })); + } + + return ( + 0} onOpenChange={onClose}> + + + {t("syncAll.title")} + {t("syncAll.description")} + + + {isLoading ? ( +
+
+
+ ) : ( +
+

+ {t("syncAll.itemsList", { items: parts.join(", ") })} +

+
+ )} + + + + + {t("syncAll.enableAll")} + + + +
+ ); +} diff --git a/src/components/sync-config-dialog.tsx b/src/components/sync-config-dialog.tsx index 61eed1d..0171bbd 100644 --- a/src/components/sync-config-dialog.tsx +++ b/src/components/sync-config-dialog.tsx @@ -28,7 +28,7 @@ import type { SyncSettings } from "@/types"; interface SyncConfigDialogProps { isOpen: boolean; - onClose: () => void; + onClose: (loginOccurred?: boolean) => void; } export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) { @@ -179,8 +179,8 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) { } catch (e) { console.error("Failed to restart sync service:", e); } - // Auto-close dialog after successful login - onClose(); + // Auto-close dialog after successful login, signal that login occurred + onClose(true); } catch (error) { console.error("OTP verification failed:", error); showErrorToast(String(error)); diff --git a/src/hooks/use-browser-download.ts b/src/hooks/use-browser-download.ts index be656b1..77bffc0 100644 --- a/src/hooks/use-browser-download.ts +++ b/src/hooks/use-browser-download.ts @@ -273,6 +273,17 @@ export function useBrowserDownload() { const progress = event.payload; setDownloadProgress(progress); + if ( + progress.stage === "downloading" || + progress.stage === "extracting" || + progress.stage === "verifying" + ) { + setDownloadingBrowsers((prev) => { + if (prev.has(progress.browser)) return prev; + return new Set(prev).add(progress.browser); + }); + } + const browserName = getBrowserDisplayName(progress.browser); if (progress.stage === "downloading") { @@ -311,11 +322,21 @@ export function useBrowserDownload() { } else if (progress.stage === "verifying") { showDownloadToast(browserName, progress.version, "verifying"); } else if (progress.stage === "cancelled") { + setDownloadingBrowsers((prev) => { + const next = new Set(prev); + next.delete(progress.browser); + return next; + }); dismissToast( `download-${browserName.toLowerCase()}-${progress.version}`, ); setDownloadProgress(null); } else if (progress.stage === "completed") { + setDownloadingBrowsers((prev) => { + const next = new Set(prev); + next.delete(progress.browser); + return next; + }); // On completion, refresh the downloaded versions for this browser and also refresh camoufox, // since the Create dialog implicitly uses camoufox on the anti-detect tab try { diff --git a/src/hooks/use-browser-state.ts b/src/hooks/use-browser-state.ts index b42f983..688dda0 100644 --- a/src/hooks/use-browser-state.ts +++ b/src/hooks/use-browser-state.ts @@ -174,7 +174,7 @@ export function useBrowserState( if (isCrossOsProfile(profile) && profile.host_os) { const osName = getOSDisplayName(profile.host_os); - return `Created on ${osName}. Can only be launched on ${osName}.`; + return `This profile was created on ${osName} and is not supported on this system`; } const isRunning = runningProfiles.has(profile.id); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f8a37ba..9159bd3 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -166,8 +166,8 @@ "antiDetect": { "title": "Anti-Detect Browser", "description": "Choose a browser with anti-detection capabilities", - "chromium": "Chromium (Wayfern)", - "firefox": "Firefox (Camoufox)", + "chromium": "Wayfern", + "firefox": "Camoufox", "badge": "Anti-Detect Browser" }, "regular": { @@ -483,11 +483,25 @@ "wayfern": "Wayfern" }, "fingerprint": { - "crossOsWarning": "Spoofing a different operating system is harder — system-level APIs are more difficult to mask, making it easier for websites to detect inconsistencies. No anti-detect browser can perfectly spoof every detail across operating systems." + "crossOsWarning": "Spoofing fingerprint for a different operating system is less reliable because it is impossible to perfectly mimic all underlying components. Use with caution." + }, + "syncAll": { + "title": "Enable Sync for Existing Items", + "description": "You have items that are not being synced. Would you like to enable sync for all of them?", + "itemsList": "Items not synced: {{items}}", + "proxies": "{{count}} proxy", + "proxies_plural": "{{count}} proxies", + "groups": "{{count}} group", + "groups_plural": "{{count}} groups", + "vpns": "{{count}} VPN", + "vpns_plural": "{{count}} VPNs", + "enableAll": "Enable All", + "skip": "Skip", + "success": "Sync enabled for all items" }, "crossOs": { - "viewOnly": "Created on {{os}} - view only", - "cannotLaunch": "Created on {{os}}. Can only be launched on {{os}}.", + "viewOnly": "This profile was created on {{os}} and is not supported on this system", + "cannotLaunch": "This profile was created on {{os}} and is not supported on this system", "cannotModify": "Cannot modify sync settings for a cross-OS profile" } } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 683aebb..aa7da48 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -166,8 +166,8 @@ "antiDetect": { "title": "Navegador Anti-Detección", "description": "Elige un navegador con capacidades anti-detección", - "chromium": "Chromium (Wayfern)", - "firefox": "Firefox (Camoufox)", + "chromium": "Wayfern", + "firefox": "Camoufox", "badge": "Navegador Anti-Detección" }, "regular": { @@ -483,11 +483,25 @@ "wayfern": "Wayfern" }, "fingerprint": { - "crossOsWarning": "Suplantar un sistema operativo diferente es más difícil: las API a nivel de sistema son más difíciles de enmascarar, lo que facilita que los sitios web detecten inconsistencias. Ningún navegador antidetección puede suplantar perfectamente cada detalle entre sistemas operativos." + "crossOsWarning": "La suplantación de huella digital para un sistema operativo diferente es menos fiable porque es imposible imitar perfectamente todos los componentes subyacentes. Usar con precaución." + }, + "syncAll": { + "title": "Activar sincronización para elementos existentes", + "description": "Tienes elementos que no se están sincronizando. ¿Te gustaría activar la sincronización para todos?", + "itemsList": "Elementos no sincronizados: {{items}}", + "proxies": "{{count}} proxy", + "proxies_plural": "{{count}} proxies", + "groups": "{{count}} grupo", + "groups_plural": "{{count}} grupos", + "vpns": "{{count}} VPN", + "vpns_plural": "{{count}} VPNs", + "enableAll": "Activar todos", + "skip": "Omitir", + "success": "Sincronización activada para todos los elementos" }, "crossOs": { - "viewOnly": "Creado en {{os}} - solo lectura", - "cannotLaunch": "Creado en {{os}}. Solo se puede iniciar en {{os}}.", + "viewOnly": "Este perfil fue creado en {{os}} y no es compatible con este sistema", + "cannotLaunch": "Este perfil fue creado en {{os}} y no es compatible con este sistema", "cannotModify": "No se pueden modificar los ajustes de sincronización de un perfil de otro sistema operativo" } } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 03ed63d..798edea 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -166,8 +166,8 @@ "antiDetect": { "title": "Navigateur anti-détection", "description": "Choisissez un navigateur avec des capacités anti-détection", - "chromium": "Chromium (Wayfern)", - "firefox": "Firefox (Camoufox)", + "chromium": "Wayfern", + "firefox": "Camoufox", "badge": "Navigateur anti-détection" }, "regular": { @@ -483,11 +483,25 @@ "wayfern": "Wayfern" }, "fingerprint": { - "crossOsWarning": "Usurper un système d'exploitation différent est plus difficile : les API au niveau du système sont plus difficiles à masquer, ce qui permet aux sites web de détecter plus facilement les incohérences. Aucun navigateur anti-détection ne peut parfaitement usurper chaque détail d'un système d'exploitation à l'autre." + "crossOsWarning": "L'usurpation d'empreinte pour un système d'exploitation différent est moins fiable car il est impossible d'imiter parfaitement tous les composants sous-jacents. À utiliser avec précaution." + }, + "syncAll": { + "title": "Activer la synchronisation pour les éléments existants", + "description": "Vous avez des éléments qui ne sont pas synchronisés. Voulez-vous activer la synchronisation pour tous ?", + "itemsList": "Éléments non synchronisés : {{items}}", + "proxies": "{{count}} proxy", + "proxies_plural": "{{count}} proxies", + "groups": "{{count}} groupe", + "groups_plural": "{{count}} groupes", + "vpns": "{{count}} VPN", + "vpns_plural": "{{count}} VPNs", + "enableAll": "Tout activer", + "skip": "Ignorer", + "success": "Synchronisation activée pour tous les éléments" }, "crossOs": { - "viewOnly": "Créé sur {{os}} - lecture seule", - "cannotLaunch": "Créé sur {{os}}. Ne peut être lancé que sur {{os}}.", + "viewOnly": "Ce profil a été créé sur {{os}} et n'est pas pris en charge sur ce système", + "cannotLaunch": "Ce profil a été créé sur {{os}} et n'est pas pris en charge sur ce système", "cannotModify": "Impossible de modifier les paramètres de synchronisation d'un profil d'un autre système d'exploitation" } } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index a2c6789..7e2fe65 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -166,8 +166,8 @@ "antiDetect": { "title": "アンチ検出ブラウザ", "description": "アンチ検出機能を持つブラウザを選択", - "chromium": "Chromium (Wayfern)", - "firefox": "Firefox (Camoufox)", + "chromium": "Wayfern", + "firefox": "Camoufox", "badge": "アンチ検出ブラウザ" }, "regular": { @@ -483,11 +483,25 @@ "wayfern": "Wayfern" }, "fingerprint": { - "crossOsWarning": "異なるオペレーティングシステムの偽装はより困難です。システムレベルのAPIはマスクしにくく、ウェブサイトが矛盾を検出しやすくなります。どのアンチディテクトブラウザも、異なるOS間のすべての詳細を完璧に偽装することはできません。" + "crossOsWarning": "異なるオペレーティングシステムのフィンガープリント偽装は、すべての基盤コンポーネントを完璧に模倣することが不可能なため、信頼性が低くなります。注意してご使用ください。" + }, + "syncAll": { + "title": "既存アイテムの同期を有効にする", + "description": "同期されていないアイテムがあります。すべての同期を有効にしますか?", + "itemsList": "未同期アイテム: {{items}}", + "proxies": "{{count}}個のプロキシ", + "proxies_plural": "{{count}}個のプロキシ", + "groups": "{{count}}個のグループ", + "groups_plural": "{{count}}個のグループ", + "vpns": "{{count}}個のVPN", + "vpns_plural": "{{count}}個のVPN", + "enableAll": "すべて有効にする", + "skip": "スキップ", + "success": "すべてのアイテムの同期が有効になりました" }, "crossOs": { - "viewOnly": "{{os}}で作成 - 閲覧のみ", - "cannotLaunch": "{{os}}で作成されました。{{os}}でのみ起動できます。", + "viewOnly": "このプロファイルは{{os}}で作成されたもので、このシステムではサポートされていません", + "cannotLaunch": "このプロファイルは{{os}}で作成されたもので、このシステムではサポートされていません", "cannotModify": "他のOSのプロファイルの同期設定は変更できません" } } diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 9b92cd2..41d4d85 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -166,8 +166,8 @@ "antiDetect": { "title": "Navegador Anti-Detecção", "description": "Escolha um navegador com capacidades anti-detecção", - "chromium": "Chromium (Wayfern)", - "firefox": "Firefox (Camoufox)", + "chromium": "Wayfern", + "firefox": "Camoufox", "badge": "Navegador Anti-Detecção" }, "regular": { @@ -483,11 +483,25 @@ "wayfern": "Wayfern" }, "fingerprint": { - "crossOsWarning": "Falsificar um sistema operacional diferente é mais difícil: as APIs de nível de sistema são mais difíceis de mascarar, facilitando a detecção de inconsistências pelos sites. Nenhum navegador antidetecção consegue falsificar perfeitamente todos os detalhes entre sistemas operacionais." + "crossOsWarning": "A falsificação de impressão digital para um sistema operacional diferente é menos confiável porque é impossível imitar perfeitamente todos os componentes subjacentes. Use com cautela." + }, + "syncAll": { + "title": "Ativar sincronização para itens existentes", + "description": "Você tem itens que não estão sendo sincronizados. Gostaria de ativar a sincronização para todos?", + "itemsList": "Itens não sincronizados: {{items}}", + "proxies": "{{count}} proxy", + "proxies_plural": "{{count}} proxies", + "groups": "{{count}} grupo", + "groups_plural": "{{count}} grupos", + "vpns": "{{count}} VPN", + "vpns_plural": "{{count}} VPNs", + "enableAll": "Ativar todos", + "skip": "Pular", + "success": "Sincronização ativada para todos os itens" }, "crossOs": { - "viewOnly": "Criado em {{os}} - somente leitura", - "cannotLaunch": "Criado em {{os}}. Só pode ser iniciado em {{os}}.", + "viewOnly": "Este perfil foi criado em {{os}} e não é compatível com este sistema", + "cannotLaunch": "Este perfil foi criado em {{os}} e não é compatível com este sistema", "cannotModify": "Não é possível modificar as configurações de sincronização de um perfil de outro sistema operacional" } } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index be52e19..3ca6e0f 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -166,8 +166,8 @@ "antiDetect": { "title": "Антидетект браузер", "description": "Выберите браузер с возможностями защиты от обнаружения", - "chromium": "Chromium (Wayfern)", - "firefox": "Firefox (Camoufox)", + "chromium": "Wayfern", + "firefox": "Camoufox", "badge": "Антидетект браузер" }, "regular": { @@ -483,11 +483,25 @@ "wayfern": "Wayfern" }, "fingerprint": { - "crossOsWarning": "Подмена другой операционной системы сложнее — системные API труднее замаскировать, что упрощает обнаружение несоответствий веб-сайтами. Ни один антидетект-браузер не может идеально подменить все детали при смене операционной системы." + "crossOsWarning": "Подмена отпечатка для другой операционной системы менее надёжна, так как невозможно идеально имитировать все базовые компоненты. Используйте с осторожностью." + }, + "syncAll": { + "title": "Включить синхронизацию для существующих элементов", + "description": "У вас есть элементы, которые не синхронизируются. Хотите включить синхронизацию для всех?", + "itemsList": "Несинхронизированные элементы: {{items}}", + "proxies": "{{count}} прокси", + "proxies_plural": "{{count}} прокси", + "groups": "{{count}} группа", + "groups_plural": "{{count}} групп", + "vpns": "{{count}} VPN", + "vpns_plural": "{{count}} VPN", + "enableAll": "Включить все", + "skip": "Пропустить", + "success": "Синхронизация включена для всех элементов" }, "crossOs": { - "viewOnly": "Создан на {{os}} - только просмотр", - "cannotLaunch": "Создан на {{os}}. Может быть запущен только на {{os}}.", + "viewOnly": "Этот профиль был создан на {{os}} и не поддерживается в этой системе", + "cannotLaunch": "Этот профиль был создан на {{os}} и не поддерживается в этой системе", "cannotModify": "Невозможно изменить настройки синхронизации профиля другой ОС" } } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d80b993..2a6af7d 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -166,8 +166,8 @@ "antiDetect": { "title": "防检测浏览器", "description": "选择具有防检测功能的浏览器", - "chromium": "Chromium (Wayfern)", - "firefox": "Firefox (Camoufox)", + "chromium": "Wayfern", + "firefox": "Camoufox", "badge": "防检测浏览器" }, "regular": { @@ -483,11 +483,25 @@ "wayfern": "Wayfern" }, "fingerprint": { - "crossOsWarning": "伪装不同的操作系统更加困难——系统级API更难以掩盖,使网站更容易检测到不一致之处。没有任何反检测浏览器能够完美伪装跨操作系统的所有细节。" + "crossOsWarning": "伪装不同操作系统的指纹不太可靠,因为不可能完美模拟所有底层组件。请谨慎使用。" + }, + "syncAll": { + "title": "为现有项目启用同步", + "description": "您有未同步的项目。是否要为所有项目启用同步?", + "itemsList": "未同步项目: {{items}}", + "proxies": "{{count}} 个代理", + "proxies_plural": "{{count}} 个代理", + "groups": "{{count}} 个分组", + "groups_plural": "{{count}} 个分组", + "vpns": "{{count}} 个 VPN", + "vpns_plural": "{{count}} 个 VPN", + "enableAll": "全部启用", + "skip": "跳过", + "success": "已为所有项目启用同步" }, "crossOs": { - "viewOnly": "在 {{os}} 上创建 - 仅查看", - "cannotLaunch": "在 {{os}} 上创建。只能在 {{os}} 上启动。", + "viewOnly": "此配置文件在 {{os}} 上创建,不受此系统支持", + "cannotLaunch": "此配置文件在 {{os}} 上创建,不受此系统支持", "cannotModify": "无法修改跨操作系统配置文件的同步设置" } } diff --git a/src/lib/browser-utils.ts b/src/lib/browser-utils.ts index 091e7d7..b8b0c09 100644 --- a/src/lib/browser-utils.ts +++ b/src/lib/browser-utils.ts @@ -15,8 +15,8 @@ export function getBrowserDisplayName(browserType: string): string { zen: "Zen Browser", brave: "Brave", chromium: "Chromium", - camoufox: "Firefox (Camoufox)", - wayfern: "Chromium (Wayfern)", + camoufox: "Camoufox", + wayfern: "Wayfern", }; return browserNames[browserType] || browserType;