mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-22 11:56:22 +02:00
refactor: sync
This commit is contained in:
+3
-3
@@ -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'"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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のプロファイルの同期設定は変更できません"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Невозможно изменить настройки синхронизации профиля другой ОС"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "无法修改跨操作系统配置文件的同步设置"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user