refactor: sync

This commit is contained in:
zhom
2026-02-20 07:22:42 +04:00
parent 8bc1ea500b
commit 4872dcc8ad
28 changed files with 678 additions and 72 deletions
+3 -3
View File
@@ -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'"
]
}
}
+5
View File
@@ -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::<u16>().saturating_add(9000).max(9000);
+24
View File
@@ -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<u16>,
headless: bool,
) -> Result<BrowserProfile, String> {
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)
+15
View File
@@ -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 {
@@ -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<String, usize> =
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();
+61 -1
View File
@@ -434,6 +434,63 @@ impl Downloader {
Ok(())
}
fn configure_camoufox_search_engine(
&self,
browser_dir: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<R: tauri::Runtime>(
&self,
_app_handle: &tauri::AppHandle<R>,
@@ -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}");
}
}
+82 -5
View File
@@ -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<dyn std::error::Error + Send + Sync>> {
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,
+2 -1
View File
@@ -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,
};
+5 -3
View File
@@ -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,
+2 -1
View File
@@ -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,
+84
View File
@@ -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<UnsyncedEntityCounts, String> {
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(())
}
+6 -6
View File
@@ -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};
+4 -2
View File
@@ -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,
};
+13 -1
View File
@@ -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<BrowserProfile | null>(null);
@@ -1118,7 +1120,17 @@ export default function Home() {
<SyncConfigDialog
isOpen={syncConfigDialogOpen}
onClose={() => setSyncConfigDialogOpen(false)}
onClose={(loginOccurred) => {
setSyncConfigDialogOpen(false);
if (loginOccurred) {
setSyncAllDialogOpen(true);
}
}}
/>
<SyncAllDialog
isOpen={syncAllDialogOpen}
onClose={() => setSyncAllDialogOpen(false)}
/>
<ProfileSyncDialog
+2 -6
View File
@@ -555,9 +555,7 @@ export function CreateProfileDialog({
})()}
</div>
<div className="text-left">
<div className="font-medium">
Chromium (Wayfern)
</div>
<div className="font-medium">Wayfern</div>
<div className="text-sm text-muted-foreground">
Anti-Detect Browser
</div>
@@ -580,9 +578,7 @@ export function CreateProfileDialog({
})()}
</div>
<div className="text-left">
<div className="font-medium">
Firefox (Camoufox)
</div>
<div className="font-medium">Camoufox</div>
<div className="text-sm text-muted-foreground">
Anti-Detect Browser
</div>
+10 -2
View File
@@ -1595,7 +1595,10 @@ export function ProfilesDataTable({
</span>
</TooltipTrigger>
<TooltipContent>
<p>Created on {osName} - view only</p>
<p>
This profile was created on {osName} and is not supported on
this system
</p>
</TooltipContent>
</Tooltip>
);
@@ -1608,7 +1611,12 @@ export function ProfilesDataTable({
: "another OS";
return (
<NonHoverableTooltip
content={<p>Created on {osName} - view only</p>}
content={
<p>
This profile was created on {osName} and is not supported on
this system
</p>
}
sideOffset={4}
horizontalOffset={8}
>
+124
View File
@@ -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<UnsyncedEntityCounts | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isEnabling, setIsEnabling] = useState(false);
const loadCounts = useCallback(async () => {
setIsLoading(true);
try {
const result = await invoke<UnsyncedEntityCounts>(
"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 (
<Dialog open={isOpen && totalCount > 0} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t("syncAll.title")}</DialogTitle>
<DialogDescription>{t("syncAll.description")}</DialogDescription>
</DialogHeader>
{isLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
) : (
<div className="py-4">
<p className="text-sm text-muted-foreground">
{t("syncAll.itemsList", { items: parts.join(", ") })}
</p>
</div>
)}
<DialogFooter className="flex gap-2">
<Button variant="outline" onClick={onClose} disabled={isEnabling}>
{t("syncAll.skip")}
</Button>
<LoadingButton
onClick={handleEnableAll}
isLoading={isEnabling}
disabled={isLoading}
>
{t("syncAll.enableAll")}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+3 -3
View File
@@ -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));
+21
View File
@@ -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 {
+1 -1
View File
@@ -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);
+19 -5
View File
@@ -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"
}
}
+19 -5
View File
@@ -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"
}
}
+19 -5
View File
@@ -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"
}
}
+19 -5
View File
@@ -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のプロファイルの同期設定は変更できません"
}
}
+19 -5
View File
@@ -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"
}
}
+19 -5
View File
@@ -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": "Невозможно изменить настройки синхронизации профиля другой ОС"
}
}
+19 -5
View File
@@ -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": "无法修改跨操作系统配置文件的同步设置"
}
}
+2 -2
View File
@@ -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;