mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-30 23:58:11 +02:00
refactor: cleanup
This commit is contained in:
@@ -744,13 +744,36 @@ impl AppAutoUpdater {
|
||||
log::info!("Extracting update...");
|
||||
let extracted_app_path = self.extract_update(&download_path, &temp_dir).await?;
|
||||
|
||||
log::info!("Installing update (overwriting binary)...");
|
||||
self.install_update(&extracted_app_path).await?;
|
||||
// On Windows, MSI/EXE installers close the running app, so running them now
|
||||
// would kill the process before the "Update ready" toast can appear. Instead,
|
||||
// defer execution to restart_application() when the user clicks "Restart Now".
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let ext = extracted_app_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
if ext == "msi" || ext == "exe" {
|
||||
log::info!("Deferring Windows installer execution until user-initiated restart");
|
||||
*PENDING_INSTALLER_PATH.lock().unwrap() = Some(extracted_app_path);
|
||||
} else {
|
||||
log::info!("Installing update (overwriting binary)...");
|
||||
self.install_update(&extracted_app_path).await?;
|
||||
log::info!("Cleaning up temporary files...");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Cleaning up temporary files...");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
log::info!("Installing update (overwriting binary)...");
|
||||
self.install_update(&extracted_app_path).await?;
|
||||
log::info!("Cleaning up temporary files...");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
|
||||
log::info!("Update installed successfully, emitting app-update-ready event");
|
||||
log::info!("Update ready, emitting app-update-ready event");
|
||||
|
||||
let _ = events::emit("app-update-ready", update_info.new_version.clone());
|
||||
|
||||
@@ -1421,14 +1444,63 @@ rm "{}"
|
||||
{
|
||||
let app_path = self.get_current_app_path()?;
|
||||
let current_pid = std::process::id();
|
||||
let pending = PENDING_INSTALLER_PATH.lock().unwrap().take();
|
||||
|
||||
// Create a temporary restart batch script
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_path = temp_dir.join("donut_restart.bat");
|
||||
let update_temp_dir = temp_dir.join("donut_app_update");
|
||||
|
||||
// Create the restart script content
|
||||
let script_content = format!(
|
||||
r#"@echo off
|
||||
let script_content = if let Some(installer_path) = pending {
|
||||
let ext = installer_path
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let install_cmd = match ext.as_str() {
|
||||
"msi" => format!(
|
||||
"msiexec /i \"{}\" /quiet /norestart REBOOT=ReallySuppress",
|
||||
installer_path.to_str().unwrap()
|
||||
),
|
||||
"exe" => format!("\"{}\" /S", installer_path.to_str().unwrap()),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
format!(
|
||||
r#"@echo off
|
||||
rem Wait for the current process to exit
|
||||
:wait_loop
|
||||
tasklist /fi "PID eq {pid}" >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
timeout /t 1 /nobreak >nul
|
||||
goto wait_loop
|
||||
)
|
||||
|
||||
rem Wait a bit more to ensure clean exit
|
||||
timeout /t 2 /nobreak >nul
|
||||
|
||||
rem Run the installer
|
||||
{install_cmd}
|
||||
|
||||
rem Wait for installation to complete
|
||||
timeout /t 3 /nobreak >nul
|
||||
|
||||
rem Start the new application
|
||||
start "" "{app_path}"
|
||||
|
||||
rem Clean up installer temp files
|
||||
rmdir /s /q "{update_temp}"
|
||||
|
||||
rem Clean up this script
|
||||
del "%~f0"
|
||||
"#,
|
||||
pid = current_pid,
|
||||
install_cmd = install_cmd,
|
||||
app_path = app_path.to_str().unwrap(),
|
||||
update_temp = update_temp_dir.to_str().unwrap(),
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
r#"@echo off
|
||||
rem Wait for the current process to exit
|
||||
:wait_loop
|
||||
tasklist /fi "PID eq {}" >nul 2>&1
|
||||
@@ -1446,24 +1518,20 @@ start "" "{}"
|
||||
rem Clean up this script
|
||||
del "%~f0"
|
||||
"#,
|
||||
current_pid,
|
||||
app_path.to_str().unwrap()
|
||||
);
|
||||
current_pid,
|
||||
app_path.to_str().unwrap()
|
||||
)
|
||||
};
|
||||
|
||||
// Write the script to file
|
||||
fs::write(&script_path, script_content)?;
|
||||
|
||||
// Execute the restart script in the background
|
||||
let mut cmd = Command::new("cmd");
|
||||
cmd.args(["/C", script_path.to_str().unwrap()]);
|
||||
|
||||
// Start the process detached
|
||||
let _child = cmd.spawn()?;
|
||||
|
||||
// Give the script a moment to start
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Exit the current process
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
@@ -1926,4 +1994,5 @@ mod tests {
|
||||
// Global singleton instance
|
||||
lazy_static::lazy_static! {
|
||||
static ref APP_AUTO_UPDATER: AppAutoUpdater = AppAutoUpdater::new();
|
||||
static ref PENDING_INSTALLER_PATH: std::sync::Mutex<Option<PathBuf>> = std::sync::Mutex::new(None);
|
||||
}
|
||||
|
||||
@@ -316,6 +316,20 @@ fn run_daemon() {
|
||||
}
|
||||
Event::Reopen { .. } => {
|
||||
tray::open_gui();
|
||||
|
||||
// Re-hide daemon from Dock. macOS activates the daemon (making it
|
||||
// visible) when the user clicks the Dock icon, overriding the
|
||||
// Accessory policy set at init.
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use objc2::MainThreadMarker;
|
||||
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy};
|
||||
|
||||
if let Some(mtm) = MainThreadMarker::new() {
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
app.setActivationPolicy(NSApplicationActivationPolicy::Accessory);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -628,6 +628,41 @@ impl CamoufoxManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Write DuckDuckGo search engine prefs to user.js for all profiles
|
||||
{
|
||||
let user_js_path = profile_path.join("user.js");
|
||||
let ddg_prefs = concat!(
|
||||
"user_pref(\"browser.search.defaultenginename\", \"DuckDuckGo\");\n",
|
||||
"user_pref(\"browser.search.order.1\", \"DuckDuckGo\");\n",
|
||||
"user_pref(\"browser.urlbar.placeholderName\", \"DuckDuckGo\");\n",
|
||||
"user_pref(\"browser.urlbar.placeholderName.private\", \"DuckDuckGo\");\n",
|
||||
);
|
||||
|
||||
let needs_write = if user_js_path.exists() {
|
||||
std::fs::read_to_string(&user_js_path)
|
||||
.map(|existing| !existing.contains("browser.search.defaultenginename"))
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if needs_write {
|
||||
use std::fs::OpenOptions;
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&user_js_path)
|
||||
{
|
||||
Ok(mut f) => {
|
||||
if let Err(e) = std::io::Write::write_all(&mut f, ddg_prefs.as_bytes()) {
|
||||
log::warn!("Failed to write DuckDuckGo prefs to user.js: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => log::warn!("Failed to open user.js for DuckDuckGo prefs: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
.launch_camoufox(
|
||||
&app_handle,
|
||||
|
||||
@@ -74,18 +74,20 @@ fn get_app_bundle_path() -> Option<std::path::PathBuf> {
|
||||
pub fn open_gui() {
|
||||
log::info!("Opening GUI...");
|
||||
|
||||
// On macOS, use `open` WITHOUT `-n`. The daemon runs with Accessory
|
||||
// activation policy so macOS won't confuse it with the GUI process.
|
||||
// `open` will either activate the existing GUI or launch a new one.
|
||||
// Using `-n` would bypass the single-instance plugin entirely.
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Use `open -n` to force launching a new process. Without `-n`, macOS
|
||||
// re-activates the daemon (the existing process from the bundle) instead
|
||||
// of launching the GUI binary. The single-instance Tauri plugin in the
|
||||
// GUI handles deduplication if a GUI instance is already running.
|
||||
// Launch the GUI binary directly. The daemon lives inside the same .app
|
||||
// bundle, so `open` (even with `-n`) can re-activate the daemon instead
|
||||
// of launching the GUI. Directly running the binary avoids macOS's app
|
||||
// activation machinery. The single-instance Tauri plugin in the GUI
|
||||
// handles deduplication if a GUI instance is already running.
|
||||
if let Some(app_bundle) = get_app_bundle_path() {
|
||||
let _ = Command::new("open").args(["-n"]).arg(&app_bundle).spawn();
|
||||
let gui_binary = app_bundle.join("Contents").join("MacOS").join("Donut");
|
||||
if gui_binary.exists() {
|
||||
let _ = Command::new(&gui_binary).spawn();
|
||||
} else {
|
||||
let _ = Command::new("open").args(["-n"]).arg(&app_bundle).spawn();
|
||||
}
|
||||
} else {
|
||||
let _ = Command::new("open").args(["-n", "-a", "Donut"]).spawn();
|
||||
}
|
||||
|
||||
+180
-38
@@ -158,7 +158,11 @@ impl Downloader {
|
||||
let release = releases
|
||||
.iter()
|
||||
.find(|r| r.tag_name == version)
|
||||
.ok_or(format!("Camoufox version {version} not found"))?;
|
||||
.or_else(|| {
|
||||
log::info!("Camoufox: requested version {version} not found, using latest available");
|
||||
releases.first()
|
||||
})
|
||||
.ok_or("No Camoufox releases found".to_string())?;
|
||||
|
||||
// Get platform and architecture info
|
||||
let (os, arch) = Self::get_platform_info();
|
||||
@@ -179,14 +183,10 @@ impl Downloader {
|
||||
.fetch_wayfern_version_with_caching(true)
|
||||
.await?;
|
||||
|
||||
// Verify requested version matches available version
|
||||
if version_info.version != version {
|
||||
return Err(
|
||||
format!(
|
||||
"Wayfern version {version} not found. Available version: {}",
|
||||
version_info.version
|
||||
)
|
||||
.into(),
|
||||
log::info!(
|
||||
"Wayfern: requested version {version}, using available version {}",
|
||||
version_info.version
|
||||
);
|
||||
}
|
||||
|
||||
@@ -659,6 +659,41 @@ impl Downloader {
|
||||
return Err("Please accept Wayfern Terms and Conditions before downloading browsers".into());
|
||||
}
|
||||
|
||||
// For Wayfern/Camoufox, resolve the actual available version from the API
|
||||
let version = if browser_str == "wayfern" {
|
||||
match self
|
||||
.api_client
|
||||
.fetch_wayfern_version_with_caching(true)
|
||||
.await
|
||||
{
|
||||
Ok(info) if info.version != version => {
|
||||
log::info!(
|
||||
"Wayfern: requested {version}, using available {}",
|
||||
info.version
|
||||
);
|
||||
info.version
|
||||
}
|
||||
_ => version,
|
||||
}
|
||||
} else if browser_str == "camoufox" {
|
||||
match self
|
||||
.api_client
|
||||
.fetch_camoufox_releases_with_caching(true)
|
||||
.await
|
||||
{
|
||||
Ok(releases) if !releases.is_empty() && releases[0].tag_name != version => {
|
||||
log::info!(
|
||||
"Camoufox: requested {version}, using available {}",
|
||||
releases[0].tag_name
|
||||
);
|
||||
releases[0].tag_name.clone()
|
||||
}
|
||||
_ => version,
|
||||
}
|
||||
} else {
|
||||
version
|
||||
};
|
||||
|
||||
// Check if this browser-version pair is already being downloaded
|
||||
let download_key = format!("{browser_str}-{version}");
|
||||
let cancel_token = {
|
||||
@@ -1033,57 +1068,164 @@ pub async fn cancel_download(browser_str: String, version: String) -> Result<(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set DuckDuckGo as the default search engine in Camoufox policies.json.
|
||||
/// Removes the fake "None" search engine and explicitly sets DuckDuckGo as default.
|
||||
/// Find all candidate `distribution/` directories inside the Camoufox browser dir.
|
||||
/// On macOS: `<browser_dir>/<app>.app/Contents/Resources/distribution/`
|
||||
/// On Linux: `<browser_dir>/camoufox/distribution/`
|
||||
/// On Windows: `<browser_dir>/distribution/`
|
||||
/// Also includes `<browser_dir>/distribution/` as a fallback for all platforms.
|
||||
fn find_camoufox_distribution_dirs(browser_dir: &Path) -> Vec<std::path::PathBuf> {
|
||||
let mut dirs = Vec::new();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Ok(entries) = std::fs::read_dir(browser_dir) {
|
||||
for entry in entries.flatten() {
|
||||
if entry.path().extension().is_some_and(|ext| ext == "app") {
|
||||
dirs.push(
|
||||
entry
|
||||
.path()
|
||||
.join("Contents")
|
||||
.join("Resources")
|
||||
.join("distribution"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let camoufox_subdir = browser_dir.join("camoufox").join("distribution");
|
||||
dirs.push(camoufox_subdir);
|
||||
}
|
||||
|
||||
// Fallback for all platforms
|
||||
dirs.push(browser_dir.join("distribution"));
|
||||
|
||||
dirs
|
||||
}
|
||||
|
||||
/// Set DuckDuckGo as the default search engine in Camoufox.
|
||||
/// Creates or updates distribution/policies.json with a proper DuckDuckGo engine definition.
|
||||
/// Called both at download time and at launch time to cover existing installations.
|
||||
pub fn configure_camoufox_search_engine(
|
||||
browser_dir: &Path,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let policies_path = browser_dir.join("distribution").join("policies.json");
|
||||
let distribution_dirs = find_camoufox_distribution_dirs(browser_dir);
|
||||
|
||||
if !policies_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
// Find an existing policies.json, or pick the first candidate dir to create one
|
||||
let (policies_path, mut policies) = {
|
||||
let mut found = None;
|
||||
for dir in &distribution_dirs {
|
||||
let path = dir.join("policies.json");
|
||||
if path.exists() {
|
||||
if let Ok(content) = std::fs::read_to_string(&path) {
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
|
||||
found = Some((path, val));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match found {
|
||||
Some(f) => f,
|
||||
None => {
|
||||
// Pick the first candidate directory that exists (or can be created)
|
||||
let target_dir = distribution_dirs
|
||||
.iter()
|
||||
.find(|d| d.parent().is_some_and(|p| p.exists()))
|
||||
.or(distribution_dirs.first())
|
||||
.ok_or("No suitable distribution directory found")?;
|
||||
std::fs::create_dir_all(target_dir)?;
|
||||
(
|
||||
target_dir.join("policies.json"),
|
||||
serde_json::json!({"policies": {}}),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let content = std::fs::read_to_string(&policies_path)?;
|
||||
let mut policies: serde_json::Value = serde_json::from_str(&content)?;
|
||||
|
||||
let current_default = policies
|
||||
// Check if already configured
|
||||
let has_ddg_default = policies
|
||||
.get("policies")
|
||||
.and_then(|p| p.get("SearchEngines"))
|
||||
.and_then(|se| se.get("Default"))
|
||||
.and_then(|d| d.as_str())
|
||||
.unwrap_or("");
|
||||
== Some("DuckDuckGo");
|
||||
|
||||
if current_default == "DuckDuckGo" {
|
||||
let has_ddg_engine = policies
|
||||
.get("policies")
|
||||
.and_then(|p| p.get("SearchEngines"))
|
||||
.and_then(|se| se.get("Add"))
|
||||
.and_then(|a| a.as_array())
|
||||
.is_some_and(|arr| {
|
||||
arr
|
||||
.iter()
|
||||
.any(|e| e.get("Name").and_then(|n| n.as_str()) == Some("DuckDuckGo"))
|
||||
});
|
||||
|
||||
if has_ddg_default && has_ddg_engine {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(policies_obj) = policies.get_mut("policies") {
|
||||
if let Some(se) = policies_obj.get_mut("SearchEngines") {
|
||||
// Set DuckDuckGo as the explicit default
|
||||
if let Some(obj) = se.as_object_mut() {
|
||||
obj.insert(
|
||||
"Default".to_string(),
|
||||
serde_json::Value::String("DuckDuckGo".to_string()),
|
||||
);
|
||||
}
|
||||
let ddg_engine = serde_json::json!({
|
||||
"Name": "DuckDuckGo",
|
||||
"URLTemplate": "https://duckduckgo.com/?q={searchTerms}",
|
||||
"SuggestURLTemplate": "https://duckduckgo.com/ac/?q={searchTerms}&type=list",
|
||||
"Method": "GET",
|
||||
"IconURL": "https://duckduckgo.com/favicon.ico",
|
||||
"Alias": "ddg"
|
||||
});
|
||||
|
||||
// Remove the fake "None" search engine entry from Add
|
||||
if let Some(add_arr) = se.get_mut("Add").and_then(|a| a.as_array_mut()) {
|
||||
add_arr.retain(|entry| entry.get("Name").and_then(|n| n.as_str()) != Some("None"));
|
||||
}
|
||||
// Ensure policies.SearchEngines exists
|
||||
let policies_obj = policies
|
||||
.as_object_mut()
|
||||
.ok_or("Invalid policies.json")?
|
||||
.entry("policies")
|
||||
.or_insert(serde_json::json!({}));
|
||||
let se = policies_obj
|
||||
.as_object_mut()
|
||||
.ok_or("Invalid policies object")?
|
||||
.entry("SearchEngines")
|
||||
.or_insert(serde_json::json!({}));
|
||||
|
||||
// Ensure DuckDuckGo is not in the Remove list
|
||||
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"));
|
||||
}
|
||||
if let Some(se_obj) = se.as_object_mut() {
|
||||
// Set DuckDuckGo as default
|
||||
se_obj.insert(
|
||||
"Default".to_string(),
|
||||
serde_json::Value::String("DuckDuckGo".to_string()),
|
||||
);
|
||||
|
||||
// Add DuckDuckGo engine definition if not present
|
||||
let add_arr = se_obj
|
||||
.entry("Add")
|
||||
.or_insert(serde_json::json!([]))
|
||||
.as_array_mut()
|
||||
.ok_or("SearchEngines.Add is not an array")?;
|
||||
|
||||
// Remove fake "None" engine
|
||||
add_arr.retain(|entry| entry.get("Name").and_then(|n| n.as_str()) != Some("None"));
|
||||
|
||||
// Add DuckDuckGo if not already present
|
||||
if !add_arr
|
||||
.iter()
|
||||
.any(|e| e.get("Name").and_then(|n| n.as_str()) == Some("DuckDuckGo"))
|
||||
{
|
||||
add_arr.push(ddg_engine);
|
||||
}
|
||||
|
||||
// Ensure DuckDuckGo is not in the Remove list
|
||||
if let Some(remove_arr) = se_obj.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!("Set DuckDuckGo as default search engine in Camoufox policies.json");
|
||||
log::info!(
|
||||
"Configured DuckDuckGo search engine in {}",
|
||||
policies_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+22
-20
@@ -1006,29 +1006,31 @@ pub fn run() {
|
||||
}
|
||||
});
|
||||
|
||||
let _app_handle_update = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log::info!("Starting app update check at startup...");
|
||||
let updater = app_auto_updater::AppAutoUpdater::instance();
|
||||
match updater.check_for_updates().await {
|
||||
Ok(Some(update_info)) => {
|
||||
log::info!(
|
||||
"App update available: {} -> {}",
|
||||
update_info.current_version,
|
||||
update_info.new_version
|
||||
);
|
||||
// Emit update available event to the frontend
|
||||
if let Err(e) = events::emit("app-update-available", &update_info) {
|
||||
log::error!("Failed to emit app update event: {e}");
|
||||
} else {
|
||||
log::debug!("App update event emitted successfully");
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(3 * 60 * 60));
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
log::info!("Checking for app updates...");
|
||||
match updater.check_for_updates().await {
|
||||
Ok(Some(update_info)) => {
|
||||
log::info!(
|
||||
"App update available: {} -> {}",
|
||||
update_info.current_version,
|
||||
update_info.new_version
|
||||
);
|
||||
if let Err(e) = events::emit("app-update-available", &update_info) {
|
||||
log::error!("Failed to emit app update event: {e}");
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
log::debug!("No app updates available");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check for app updates: {e}");
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
log::debug!("No app updates available");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to check for app updates: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,23 +128,10 @@ impl SettingsManager {
|
||||
|
||||
// Parse the settings file - serde will use default values for missing fields
|
||||
match serde_json::from_str::<AppSettings>(&content) {
|
||||
Ok(settings) => {
|
||||
// Save the settings back to ensure any missing fields are written with defaults
|
||||
if let Err(e) = self.save_settings(&settings) {
|
||||
log::warn!("Warning: Failed to update settings file with defaults: {e}");
|
||||
}
|
||||
Ok(settings)
|
||||
}
|
||||
Ok(settings) => Ok(settings),
|
||||
Err(e) => {
|
||||
log::warn!("Warning: Failed to parse settings file, using defaults: {e}");
|
||||
let default_settings = AppSettings::default();
|
||||
|
||||
// Try to save default settings to fix the corrupted file
|
||||
if let Err(save_error) = self.save_settings(&default_settings) {
|
||||
log::warn!("Warning: Failed to save default settings: {save_error}");
|
||||
}
|
||||
|
||||
Ok(default_settings)
|
||||
Ok(AppSettings::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -168,6 +168,8 @@ export default function Home() {
|
||||
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
|
||||
const [launchOnLoginDialogOpen, setLaunchOnLoginDialogOpen] = useState(false);
|
||||
const [windowResizeWarningOpen, setWindowResizeWarningOpen] = useState(false);
|
||||
const [windowResizeWarningBrowserType, setWindowResizeWarningBrowserType] =
|
||||
useState<string | undefined>(undefined);
|
||||
const windowResizeWarningResolver = useRef<
|
||||
((proceed: boolean) => void) | null
|
||||
>(null);
|
||||
@@ -523,7 +525,6 @@ export default function Home() {
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[selectedGroupId],
|
||||
@@ -541,6 +542,7 @@ export default function Home() {
|
||||
if (!dismissed) {
|
||||
const proceed = await new Promise<boolean>((resolve) => {
|
||||
windowResizeWarningResolver.current = resolve;
|
||||
setWindowResizeWarningBrowserType(profile.browser);
|
||||
setWindowResizeWarningOpen(true);
|
||||
});
|
||||
if (!proceed) {
|
||||
@@ -1248,6 +1250,7 @@ export default function Home() {
|
||||
|
||||
<WindowResizeWarningDialog
|
||||
isOpen={windowResizeWarningOpen}
|
||||
browserType={windowResizeWarningBrowserType}
|
||||
onResult={(proceed) => {
|
||||
setWindowResizeWarningOpen(false);
|
||||
windowResizeWarningResolver.current?.(proceed);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { useCloudAuth } from "@/hooks/use-cloud-auth";
|
||||
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||
import type { BrowserProfile, SyncMode, SyncSettings } from "@/types";
|
||||
import { isSyncEnabled } from "@/types";
|
||||
@@ -34,24 +35,34 @@ export function ProfileSyncDialog({
|
||||
onSyncConfigOpen,
|
||||
}: ProfileSyncDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const { user: cloudUser } = useCloudAuth();
|
||||
const isCloudSyncEligible =
|
||||
cloudUser != null &&
|
||||
cloudUser.plan !== "free" &&
|
||||
(cloudUser.subscriptionStatus === "active" ||
|
||||
cloudUser.planPeriod === "lifetime");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const [syncMode, setSyncMode] = useState<SyncMode>(
|
||||
profile?.sync_mode ?? "Disabled",
|
||||
);
|
||||
const [hasConfig, setHasConfig] = useState(false);
|
||||
const [hasSelfHostedConfig, setHasSelfHostedConfig] = useState(false);
|
||||
const [hasE2ePassword, setHasE2ePassword] = useState(false);
|
||||
const [isCheckingConfig, setIsCheckingConfig] = useState(false);
|
||||
|
||||
const hasConfig = isCloudSyncEligible || hasSelfHostedConfig;
|
||||
|
||||
const checkSyncConfig = useCallback(async () => {
|
||||
setIsCheckingConfig(true);
|
||||
try {
|
||||
const settings = await invoke<SyncSettings>("get_sync_settings");
|
||||
setHasConfig(Boolean(settings.sync_server_url && settings.sync_token));
|
||||
setHasSelfHostedConfig(
|
||||
Boolean(settings.sync_server_url && settings.sync_token),
|
||||
);
|
||||
const hasPassword = await invoke<boolean>("check_has_e2e_password");
|
||||
setHasE2ePassword(hasPassword);
|
||||
} catch {
|
||||
setHasConfig(false);
|
||||
setHasSelfHostedConfig(false);
|
||||
} finally {
|
||||
setIsCheckingConfig(false);
|
||||
}
|
||||
|
||||
@@ -1046,18 +1046,19 @@ export function SharedCamoufoxConfigForm({
|
||||
</fieldset>
|
||||
{limitedMode && (
|
||||
<>
|
||||
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-[2]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30 z-[1]" />
|
||||
<div className="absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-[3]">
|
||||
<div className="flex items-center gap-2 rounded-md bg-background/80 px-3 py-1.5">
|
||||
<ProBadge />
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t("fingerprint.proFeature")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-background to-transparent z-[1]" />
|
||||
<div className="absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-background to-transparent z-[1]" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent z-[1]" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1253,18 +1254,19 @@ export function SharedCamoufoxConfigForm({
|
||||
</fieldset>
|
||||
{limitedMode && (
|
||||
<>
|
||||
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-[2]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30 z-[1]" />
|
||||
<div className="absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-[3]">
|
||||
<div className="flex items-center gap-2 rounded-md bg-background/80 px-3 py-1.5">
|
||||
<ProBadge />
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t("fingerprint.proFeature")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-background to-transparent z-[1]" />
|
||||
<div className="absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-background to-transparent z-[1]" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent z-[1]" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -998,18 +998,19 @@ export function WayfernConfigForm({
|
||||
</fieldset>
|
||||
{limitedMode && (
|
||||
<>
|
||||
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-[2]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30 z-[1]" />
|
||||
<div className="absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-[3]">
|
||||
<div className="flex items-center gap-2 rounded-md bg-background/80 px-3 py-1.5">
|
||||
<ProBadge />
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t("fingerprint.proFeature")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-background to-transparent z-[1]" />
|
||||
<div className="absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-background to-transparent z-[1]" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent z-[1]" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1212,18 +1213,19 @@ export function WayfernConfigForm({
|
||||
</fieldset>
|
||||
{limitedMode && (
|
||||
<>
|
||||
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-[2]">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30 z-[1]" />
|
||||
<div className="absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent z-[2]" />
|
||||
<div className="absolute inset-0 flex items-center justify-center z-[3]">
|
||||
<div className="flex items-center gap-2 rounded-md bg-background/80 px-3 py-1.5">
|
||||
<ProBadge />
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t("fingerprint.proFeature")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 left-0 w-6 bg-gradient-to-r from-background to-transparent z-[1]" />
|
||||
<div className="absolute inset-y-0 right-0 w-6 bg-gradient-to-l from-background to-transparent z-[1]" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-background to-transparent z-[1]" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -17,15 +17,23 @@ import { Label } from "@/components/ui/label";
|
||||
interface WindowResizeWarningDialogProps {
|
||||
isOpen: boolean;
|
||||
onResult: (proceed: boolean) => void;
|
||||
browserType?: string;
|
||||
}
|
||||
|
||||
export function WindowResizeWarningDialog({
|
||||
isOpen,
|
||||
onResult,
|
||||
browserType,
|
||||
}: WindowResizeWarningDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setDontShowAgain(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleContinue = async () => {
|
||||
if (dontShowAgain) {
|
||||
try {
|
||||
@@ -41,6 +49,16 @@ export function WindowResizeWarningDialog({
|
||||
onResult(false);
|
||||
};
|
||||
|
||||
const isCamoufox = browserType === "camoufox";
|
||||
|
||||
const title = isCamoufox
|
||||
? t("warnings.windowResizeCamoufoxTitle")
|
||||
: t("warnings.windowResizeTitle");
|
||||
|
||||
const description = isCamoufox
|
||||
? t("warnings.windowResizeCamoufoxDescription")
|
||||
: t("warnings.windowResizeDescription");
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen}>
|
||||
<DialogContent
|
||||
@@ -50,12 +68,10 @@ export function WindowResizeWarningDialog({
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("warnings.windowResizeTitle")}</DialogTitle>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("warnings.windowResizeDescription")}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Custom Window Dimensions",
|
||||
"windowResizeDescription": "Changing browser window dimensions may increase the chance of website detection that browser information is spoofed.",
|
||||
"windowResizeCamoufoxTitle": "Viewport Locked by Camoufox",
|
||||
"windowResizeCamoufoxDescription": "Camoufox locks the viewport to the spoofed screen dimensions for anti-fingerprinting. Resizing the window may cause cropped or grey areas. This is expected behavior.",
|
||||
"dontShowAgain": "Don't show this again",
|
||||
"continue": "Continue",
|
||||
"cancel": "Cancel"
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Dimensiones de ventana personalizadas",
|
||||
"windowResizeDescription": "Cambiar las dimensiones de la ventana del navegador puede aumentar la posibilidad de que los sitios web detecten que la información del navegador está falsificada.",
|
||||
"windowResizeCamoufoxTitle": "Viewport bloqueado por Camoufox",
|
||||
"windowResizeCamoufoxDescription": "Camoufox bloquea el viewport a las dimensiones de pantalla falsificadas para anti-fingerprinting. Redimensionar la ventana puede causar áreas recortadas o grises. Este es el comportamiento esperado.",
|
||||
"dontShowAgain": "No mostrar esto de nuevo",
|
||||
"continue": "Continuar",
|
||||
"cancel": "Cancelar"
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Dimensions de fenêtre personnalisées",
|
||||
"windowResizeDescription": "Modifier les dimensions de la fenêtre du navigateur peut augmenter les chances de détection par les sites web que les informations du navigateur sont falsifiées.",
|
||||
"windowResizeCamoufoxTitle": "Viewport verrouillé par Camoufox",
|
||||
"windowResizeCamoufoxDescription": "Camoufox verrouille le viewport aux dimensions d'écran falsifiées pour l'anti-fingerprinting. Redimensionner la fenêtre peut causer des zones recadrées ou grises. C'est le comportement attendu.",
|
||||
"dontShowAgain": "Ne plus afficher",
|
||||
"continue": "Continuer",
|
||||
"cancel": "Annuler"
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "カスタムウィンドウサイズ",
|
||||
"windowResizeDescription": "ブラウザウィンドウのサイズを変更すると、ブラウザ情報が偽装されていることをウェブサイトに検出される可能性が高くなります。",
|
||||
"windowResizeCamoufoxTitle": "Camoufoxによりビューポートがロックされています",
|
||||
"windowResizeCamoufoxDescription": "Camoufoxはアンチフィンガープリントのためにビューポートを偽装された画面サイズにロックします。ウィンドウのサイズを変更すると、切り取られた領域やグレーの領域が表示される場合があります。これは想定された動作です。",
|
||||
"dontShowAgain": "今後表示しない",
|
||||
"continue": "続行",
|
||||
"cancel": "キャンセル"
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Dimensões de janela personalizadas",
|
||||
"windowResizeDescription": "Alterar as dimensões da janela do navegador pode aumentar a chance de detecção pelos sites de que as informações do navegador estão falsificadas.",
|
||||
"windowResizeCamoufoxTitle": "Viewport bloqueado pelo Camoufox",
|
||||
"windowResizeCamoufoxDescription": "O Camoufox bloqueia o viewport nas dimensões de tela falsificadas para anti-fingerprinting. Redimensionar a janela pode causar áreas cortadas ou cinzas. Este é o comportamento esperado.",
|
||||
"dontShowAgain": "Não mostrar novamente",
|
||||
"continue": "Continuar",
|
||||
"cancel": "Cancelar"
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "Пользовательские размеры окна",
|
||||
"windowResizeDescription": "Изменение размеров окна браузера может повысить вероятность обнаружения сайтами того, что информация браузера подменена.",
|
||||
"windowResizeCamoufoxTitle": "Viewport заблокирован Camoufox",
|
||||
"windowResizeCamoufoxDescription": "Camoufox блокирует viewport на подменённых размерах экрана для защиты от фингерпринтинга. Изменение размера окна может вызвать обрезанные или серые области. Это ожидаемое поведение.",
|
||||
"dontShowAgain": "Больше не показывать",
|
||||
"continue": "Продолжить",
|
||||
"cancel": "Отмена"
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"warnings": {
|
||||
"windowResizeTitle": "自定义窗口尺寸",
|
||||
"windowResizeDescription": "更改浏览器窗口尺寸可能会增加网站检测到浏览器信息被伪装的概率。",
|
||||
"windowResizeCamoufoxTitle": "视口已被 Camoufox 锁定",
|
||||
"windowResizeCamoufoxDescription": "Camoufox 将视口锁定为伪装的屏幕尺寸以防止指纹识别。调整窗口大小可能会导致内容被裁剪或出现灰色区域。这是预期行为。",
|
||||
"dontShowAgain": "不再显示",
|
||||
"continue": "继续",
|
||||
"cancel": "取消"
|
||||
|
||||
Reference in New Issue
Block a user