diff --git a/.github/workflows/lint-rs.yml b/.github/workflows/lint-rs.yml index e9a2859..16b535e 100644 --- a/.github/workflows/lint-rs.yml +++ b/.github/workflows/lint-rs.yml @@ -88,7 +88,6 @@ jobs: working-directory: ./src-tauri run: | cargo build --bin donut-proxy --release - cargo build --bin donut-daemon --release - name: Copy sidecar binaries to Tauri binaries shell: bash @@ -97,12 +96,9 @@ jobs: HOST_TARGET="${{ steps.host_target.outputs.target }}" if [[ "$HOST_TARGET" == *"windows"* ]]; then cp src-tauri/target/release/donut-proxy.exe src-tauri/binaries/donut-proxy-${HOST_TARGET}.exe - cp src-tauri/target/release/donut-daemon.exe src-tauri/binaries/donut-daemon-${HOST_TARGET}.exe else cp src-tauri/target/release/donut-proxy src-tauri/binaries/donut-proxy-${HOST_TARGET} - cp src-tauri/target/release/donut-daemon src-tauri/binaries/donut-daemon-${HOST_TARGET} chmod +x src-tauri/binaries/donut-proxy-${HOST_TARGET} - chmod +x src-tauri/binaries/donut-daemon-${HOST_TARGET} fi - name: Run rustfmt check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0490748..73258c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -162,7 +162,6 @@ jobs: working-directory: ./src-tauri run: | cargo build --bin donut-proxy --target ${{ matrix.target }} --release - cargo build --bin donut-daemon --target ${{ matrix.target }} --release - name: Copy sidecar binaries to Tauri binaries shell: bash @@ -170,12 +169,9 @@ jobs: mkdir -p src-tauri/binaries if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then cp src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe src-tauri/binaries/donut-proxy-${{ matrix.target }}.exe - cp src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe src-tauri/binaries/donut-daemon-${{ matrix.target }}.exe else cp src-tauri/target/${{ matrix.target }}/release/donut-proxy src-tauri/binaries/donut-proxy-${{ matrix.target }} - cp src-tauri/target/${{ matrix.target }}/release/donut-daemon src-tauri/binaries/donut-daemon-${{ matrix.target }} chmod +x src-tauri/binaries/donut-proxy-${{ matrix.target }} - chmod +x src-tauri/binaries/donut-daemon-${{ matrix.target }} fi - name: Import Apple certificate diff --git a/.github/workflows/rolling-release.yml b/.github/workflows/rolling-release.yml index 45bb3f7..15fa530 100644 --- a/.github/workflows/rolling-release.yml +++ b/.github/workflows/rolling-release.yml @@ -161,7 +161,6 @@ jobs: working-directory: ./src-tauri run: | cargo build --bin donut-proxy --target ${{ matrix.target }} --release - cargo build --bin donut-daemon --target ${{ matrix.target }} --release - name: Copy sidecar binaries to Tauri binaries shell: bash @@ -169,12 +168,9 @@ jobs: mkdir -p src-tauri/binaries if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then cp src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe src-tauri/binaries/donut-proxy-${{ matrix.target }}.exe - cp src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe src-tauri/binaries/donut-daemon-${{ matrix.target }}.exe else cp src-tauri/target/${{ matrix.target }}/release/donut-proxy src-tauri/binaries/donut-proxy-${{ matrix.target }} - cp src-tauri/target/${{ matrix.target }}/release/donut-daemon src-tauri/binaries/donut-daemon-${{ matrix.target }} chmod +x src-tauri/binaries/donut-proxy-${{ matrix.target }} - chmod +x src-tauri/binaries/donut-daemon-${{ matrix.target }} fi - name: Import Apple certificate diff --git a/_typos.toml b/_typos.toml index 4e1a00f..ec93ca5 100644 --- a/_typos.toml +++ b/_typos.toml @@ -3,7 +3,6 @@ extend-exclude = [ "src-tauri/src/camoufox/data/*.json", "src-tauri/src/camoufox/data/*.xml", "src/i18n/locales/*.json", - "src-tauri/build.rs", # Auto-generated from commit subjects by release.yml; typos here originate # in commit messages, which are immutable, so don't spell-check it. "CHANGELOG.md", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7ae1565..591b2f7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1840,7 +1840,6 @@ dependencies = [ "smoltcp", "sys-locale", "sysinfo", - "tao", "tar", "tauri", "tauri-build", @@ -1861,7 +1860,6 @@ dependencies = [ "toml 1.1.2+spec-1.1.0", "tower", "tower-http", - "tray-icon 0.24.0", "url", "urlencoding", "utoipa", @@ -3667,25 +3665,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libxdo" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" -dependencies = [ - "libxdo-sys", -] - -[[package]] -name = "libxdo-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" -dependencies = [ - "libc", - "x11", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -3923,7 +3902,6 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "libxdo", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -6633,7 +6611,7 @@ dependencies = [ "tauri-utils", "thiserror 2.0.18", "tokio", - "tray-icon 0.23.1", + "tray-icon", "url", "webkit2gtk", "webview2-com", @@ -7507,27 +7485,6 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "tray-icon" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47e6d063cfe4ad2e416fcbb310be3a37c5fd85c745b62cb562bfa4a003df674" -dependencies = [ - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png 0.18.1", - "thiserror 2.0.18", - "windows-sys 0.60.2", -] - [[package]] name = "tree_magic_mini" version = "3.2.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 14cc39f..ef1c041 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,10 +24,6 @@ path = "src/main.rs" name = "donut-proxy" path = "src/bin/proxy_server.rs" -[[bin]] -name = "donut-daemon" -path = "src/bin/donut_daemon.rs" - [build-dependencies] tauri-build = { version = "2", features = [] } resvg = "0.47" @@ -111,9 +107,7 @@ quick-xml = { version = "0.40", features = ["serialize"] } boringtun = "0.7" smoltcp = { version = "0.13", default-features = false, features = ["std", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-tcp", "socket-udp"] } -# Daemon dependencies (tray icon) -tray-icon = "0.24" -tao = "0.35" +# Tray icon decoding (main-process system tray) image = "0.25" dirs = "6" crossbeam-channel = "0.5" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index a53ddb6..69d1d0f 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -5,7 +5,7 @@ fn main() { // This allows running cargo test without building the frontend first ensure_dist_folder_exists(); - // Generate tray icon PNGs from SVG (macOS template icon format) + // Generate tray icon PNG files from SVG (macOS template icon format) generate_tray_icons(); #[cfg(target_os = "macos")] @@ -93,19 +93,13 @@ fn external_binaries_exist() -> bool { let binaries_dir = PathBuf::from(&manifest_dir).join("binaries"); // Check for all required external binaries (must match tauri.conf.json externalBin) - let (donut_proxy_name, donut_daemon_name) = if target.contains("windows") { - ( - format!("donut-proxy-{}.exe", target), - format!("donut-daemon-{}.exe", target), - ) + let donut_proxy_name = if target.contains("windows") { + format!("donut-proxy-{}.exe", target) } else { - ( - format!("donut-proxy-{}", target), - format!("donut-daemon-{}", target), - ) + format!("donut-proxy-{}", target) }; - binaries_dir.join(&donut_proxy_name).exists() && binaries_dir.join(&donut_daemon_name).exists() + binaries_dir.join(&donut_proxy_name).exists() } fn ensure_dist_folder_exists() { diff --git a/src-tauri/copy-proxy-binary.mjs b/src-tauri/copy-proxy-binary.mjs index 83fdc7f..77f8049 100644 --- a/src-tauri/copy-proxy-binary.mjs +++ b/src-tauri/copy-proxy-binary.mjs @@ -77,4 +77,3 @@ function copyBinary(baseName) { } copyBinary("donut-proxy"); -copyBinary("donut-daemon"); diff --git a/src-tauri/copy-proxy-binary.sh b/src-tauri/copy-proxy-binary.sh index d0d3f25..95711be 100755 --- a/src-tauri/copy-proxy-binary.sh +++ b/src-tauri/copy-proxy-binary.sh @@ -102,6 +102,3 @@ copy_binary() { # Copy donut-proxy binary copy_binary "donut-proxy" -# Copy donut-daemon binary -copy_binary "donut-daemon" - diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index 11990ea..69393d1 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -1,6 +1,5 @@ use crate::browser::ProxySettings; use crate::camoufox_manager::CamoufoxConfig; -use crate::daemon_ws::{ws_handler, WsState}; use crate::events; use crate::group_manager::GROUP_MANAGER; use crate::profile::manager::ProfileManager; @@ -412,16 +411,9 @@ impl ApiServer { )) .layer(middleware::from_fn(terms_check_middleware)); - // Create WebSocket route with its own state (no auth required for daemon IPC) - let ws_state = WsState::new(); - let ws_routes = Router::new() - .route("/events", get(ws_handler)) - .with_state(ws_state); - let api_for_v1 = api.clone(); let app = Router::new() .merge(v1_routes) - .nest("/ws", ws_routes) .route("/openapi.json", get(move || async move { Json(api) })) .route( "/v1/openapi.json", diff --git a/src-tauri/src/bin/donut_daemon.rs b/src-tauri/src/bin/donut_daemon.rs deleted file mode 100644 index 7ad6a7a..0000000 --- a/src-tauri/src/bin/donut_daemon.rs +++ /dev/null @@ -1,498 +0,0 @@ -// Donut Browser Daemon - Background process for tray icon and services -// This runs independently of the main Tauri GUI - -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -use std::env; -use std::fs; -use std::path::PathBuf; -use std::process; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc; -use std::time::{Duration, Instant}; - -use serde::{Deserialize, Serialize}; -use tao::event::{Event, StartCause}; -use tao::event_loop::{ControlFlow, EventLoopBuilder}; -use tokio::runtime::Runtime; -use tray_icon::menu::MenuEvent; -use tray_icon::TrayIcon; -#[cfg(not(target_os = "macos"))] -use tray_icon::{MouseButton, TrayIconEvent}; - -use donutbrowser_lib::daemon::{autostart, services, tray}; - -static SHOULD_QUIT: AtomicBool = AtomicBool::new(false); - -#[cfg(windows)] -fn win_process_exists(pid: u32) -> bool { - const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000; - - extern "system" { - fn OpenProcess(dwDesiredAccess: u32, bInheritHandles: i32, dwProcessId: u32) -> *mut (); - fn CloseHandle(hObject: *mut ()) -> i32; - } - - let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) }; - if handle.is_null() { - false - } else { - unsafe { CloseHandle(handle) }; - true - } -} - -enum ServiceStatus { - Ready { - api_port: Option, - mcp_running: bool, - }, - Failed(String), -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -struct DaemonState { - daemon_pid: Option, - api_port: Option, - mcp_running: bool, - version: String, -} - -fn get_state_path() -> PathBuf { - autostart::get_data_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("daemon-state.json") -} - -fn ensure_data_dir() -> std::io::Result<()> { - if let Some(data_dir) = autostart::get_data_dir() { - fs::create_dir_all(&data_dir)?; - } - Ok(()) -} - -fn read_state() -> DaemonState { - let path = get_state_path(); - if path.exists() { - if let Ok(content) = fs::read_to_string(&path) { - if let Ok(state) = serde_json::from_str(&content) { - return state; - } - } - } - DaemonState::default() -} - -fn write_state(state: &DaemonState) -> std::io::Result<()> { - let path = get_state_path(); - let content = serde_json::to_string_pretty(state)?; - fs::write(path, content) -} - -fn set_high_priority() { - #[cfg(unix)] - { - // Set high priority so the daemon is killed last under resource pressure - // Negative nice value = higher priority. Try -10, fall back to -5 if it fails. - unsafe { - if libc::setpriority(libc::PRIO_PROCESS, 0, -10) != 0 { - let _ = libc::setpriority(libc::PRIO_PROCESS, 0, -5); - } - } - } - - #[cfg(windows)] - { - use windows::Win32::Foundation::CloseHandle; - use windows::Win32::System::Threading::{ - GetCurrentProcess, SetPriorityClass, ABOVE_NORMAL_PRIORITY_CLASS, - }; - - // Set high priority so the daemon is killed last under resource pressure - unsafe { - let handle = GetCurrentProcess(); - let _ = SetPriorityClass(handle, ABOVE_NORMAL_PRIORITY_CLASS); - // GetCurrentProcess returns a pseudo-handle that doesn't need to be closed, - // but we do it anyway for consistency - let _ = CloseHandle(handle); - } - } -} - -fn run_daemon() { - // Set high priority so the daemon is less likely to be killed under resource pressure - set_high_priority(); - - // Initialize logging to file for debugging (since stdout/stderr may be redirected) - let log_path = autostart::get_data_dir() - .unwrap_or_else(|| std::path::PathBuf::from(".")) - .join("daemon.log"); - - let log_file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&log_path); - - env_logger::Builder::from_default_env() - .filter_level(log::LevelFilter::Info) - .format_timestamp_millis() - .target(if let Ok(file) = log_file { - env_logger::Target::Pipe(Box::new(file)) - } else { - env_logger::Target::Stderr - }) - .init(); - - if let Err(e) = ensure_data_dir() { - eprintln!("Failed to create data directory: {}", e); - process::exit(1); - } - - log::info!("[daemon] Starting with PID {}", process::id()); - - // Create tokio runtime for async operations - let rt = Runtime::new().expect("Failed to create tokio runtime"); - - // Create channel for service status updates - let (tx, rx) = mpsc::channel::(); - - // Spawn services in a background thread so we don't block the event loop - let rt_handle = rt.handle().clone(); - std::thread::spawn(move || { - let result = rt_handle.block_on(async { services::DaemonServices::start().await }); - let status = match result { - Ok(s) => ServiceStatus::Ready { - api_port: s.api_port, - mcp_running: s.mcp_running, - }, - Err(e) => ServiceStatus::Failed(e), - }; - let _ = tx.send(status); - }); - - // Write initial state (services still starting) - let state = DaemonState { - daemon_pid: Some(process::id()), - api_port: None, - mcp_running: false, - version: env!("CARGO_PKG_VERSION").to_string(), - }; - if let Err(e) = write_state(&state) { - log::error!("Failed to write state: {}", e); - } - - // Prepare tray menu and icon (but don't create the tray icon yet) - let tray_menu = tray::TrayMenu::new(); - - let icon = tray::load_icon(); - let menu_channel = MenuEvent::receiver(); - - // Create the event loop IMMEDIATELY (critical for macOS tray icon) - let event_loop = EventLoopBuilder::new().build(); - - // Store tray icon in Option - created after event loop starts - let mut tray_icon: Option = None; - - // Install signal handlers so SIGTERM/SIGINT trigger graceful shutdown - #[cfg(unix)] - unsafe { - extern "C" fn signal_handler(_sig: libc::c_int) { - SHOULD_QUIT.store(true, std::sync::atomic::Ordering::SeqCst); - } - libc::signal( - libc::SIGTERM, - signal_handler as *const () as libc::sighandler_t, - ); - libc::signal( - libc::SIGINT, - signal_handler as *const () as libc::sighandler_t, - ); - } - - #[cfg(windows)] - { - extern "system" { - fn SetConsoleCtrlHandler( - handler: Option i32>, - add: i32, - ) -> i32; - } - - unsafe extern "system" fn ctrl_handler(_ctrl_type: u32) -> i32 { - SHOULD_QUIT.store(true, std::sync::atomic::Ordering::SeqCst); - 1 // TRUE - } - - unsafe { - SetConsoleCtrlHandler(Some(ctrl_handler), 1); - } - } - - // Run the event loop - event_loop.run(move |event, _, control_flow| { - // Use WaitUntil to check for menu events periodically while staying low on CPU - *control_flow = ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(100)); - - match event { - Event::NewEvents(StartCause::Init) => { - // Hide from dock on macOS (must be done after event loop starts) - #[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); - } - } - - // Create tray icon after event loop has started (required for macOS) - tray_icon = Some(tray::create_tray_icon(icon.clone(), &tray_menu.menu)); - log::info!("[daemon] Tray icon created"); - } - Event::MainEventsCleared => { - // Check for service status updates from background thread - if let Ok(status) = rx.try_recv() { - match status { - ServiceStatus::Ready { - api_port, - mcp_running, - } => { - log::info!("[daemon] Services started successfully"); - - // Update state file - let mut state = read_state(); - state.api_port = api_port; - state.mcp_running = mcp_running; - if let Err(e) = write_state(&state) { - log::error!("Failed to write state: {}", e); - } - } - ServiceStatus::Failed(e) => { - log::error!("Failed to start services: {}", e); - } - } - } - - // Process menu events - while let Ok(event) = menu_channel.try_recv() { - if event.id == tray_menu.quit_item.id() { - log::info!("[daemon] Quit requested"); - SHOULD_QUIT.store(true, Ordering::SeqCst); - } - } - - // Handle tray icon click (left-click opens the app) - // On macOS, left-click already shows the menu, so don't also launch the GUI. - #[cfg(not(target_os = "macos"))] - while let Ok(event) = TrayIconEvent::receiver().try_recv() { - if let TrayIconEvent::Click { - button: MouseButton::Left, - .. - } = event - { - tray::open_gui(); - } - } - - // Use swap to only run cleanup once - if SHOULD_QUIT.swap(false, Ordering::SeqCst) { - // Remove tray icon from status bar immediately so the UI feels responsive - tray_icon = None; - - tray::quit_gui(); - - let mut state = read_state(); - state.daemon_pid = None; - let _ = write_state(&state); - log::info!("[daemon] Exiting"); - - // Use process::exit for immediate termination instead of ControlFlow::Exit. - // ControlFlow::Exit can delay because tao's macOS event loop defers exit, - // and dropping the tokio runtime blocks until all spawned tasks finish. - process::exit(0); - } - } - 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); - } - } - } - _ => {} - } - - // Keep tray_icon alive - let _ = &tray_icon; - - // Keep runtime alive - let _ = &rt; - }); -} - -fn stop_daemon() { - let state = read_state(); - - if let Some(pid) = state.daemon_pid { - // On Windows, taskkill /F kills instantly with no handler, so kill GUI first - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - use std::process::Command; - const CREATE_NO_WINDOW: u32 = 0x08000000; - - let state_path = get_state_path(); - if let Ok(content) = fs::read_to_string(&state_path) { - if let Ok(val) = serde_json::from_str::(&content) { - if let Some(gui_pid) = val.get("gui_pid").and_then(|v| v.as_u64()) { - let _ = Command::new("taskkill") - .args(["/PID", &gui_pid.to_string(), "/F"]) - .creation_flags(CREATE_NO_WINDOW) - .output(); - } - } - } - - let _ = Command::new("taskkill") - .args(["/PID", &pid.to_string(), "/F"]) - .creation_flags(CREATE_NO_WINDOW) - .output(); - eprintln!("Sent stop signal to daemon (PID {})", pid); - } - - #[cfg(unix)] - { - unsafe { - libc::kill(pid as i32, libc::SIGTERM); - } - eprintln!("Sent stop signal to daemon (PID {})", pid); - } - } else { - eprintln!("Daemon is not running"); - } -} - -fn show_status() { - let state = read_state(); - - if let Some(pid) = state.daemon_pid { - #[cfg(unix)] - let is_running = unsafe { libc::kill(pid as i32, 0) == 0 }; - - #[cfg(windows)] - let is_running = win_process_exists(pid); - - #[cfg(not(any(unix, windows)))] - let is_running = false; - - if is_running { - eprintln!("Daemon is running (PID {})", pid); - if let Some(port) = state.api_port { - eprintln!(" API: Running on port {}", port); - } else { - eprintln!(" API: Stopped"); - } - eprintln!( - " MCP: {}", - if state.mcp_running { - "Running" - } else { - "Stopped" - } - ); - } else { - eprintln!("Daemon is not running (stale PID in state file)"); - } - } else { - eprintln!("Daemon is not running"); - } -} - -fn print_usage() { - eprintln!("Donut Browser Daemon"); - eprintln!(); - eprintln!("Usage: donut-daemon "); - eprintln!(); - eprintln!("Commands:"); - eprintln!(" start Start the daemon (detaches from terminal)"); - eprintln!(" stop Stop the running daemon"); - eprintln!(" status Show daemon status"); - eprintln!(" run Run in foreground (for debugging)"); - eprintln!(" autostart Manage autostart settings"); - eprintln!(" enable Enable autostart on login"); - eprintln!(" disable Disable autostart on login"); - eprintln!(" status Show autostart status"); -} - -fn main() { - let args: Vec = env::args().collect(); - - if args.len() < 2 { - print_usage(); - process::exit(1); - } - - match args[1].as_str() { - "start" => { - run_daemon(); - } - "stop" => { - stop_daemon(); - } - "status" => { - show_status(); - } - "run" => { - run_daemon(); - } - "autostart" => { - if args.len() < 3 { - eprintln!("Usage: donut-daemon autostart "); - process::exit(1); - } - match args[2].as_str() { - "enable" => { - if let Err(e) = autostart::enable_autostart() { - eprintln!("Failed to enable autostart: {}", e); - process::exit(1); - } - eprintln!("Autostart enabled"); - } - "disable" => { - if let Err(e) = autostart::disable_autostart() { - eprintln!("Failed to disable autostart: {}", e); - process::exit(1); - } - eprintln!("Autostart disabled"); - } - "status" => { - if autostart::is_autostart_enabled() { - eprintln!("Autostart is enabled"); - } else { - eprintln!("Autostart is disabled"); - } - } - _ => { - eprintln!("Unknown autostart command: {}", args[2]); - process::exit(1); - } - } - } - _ => { - print_usage(); - process::exit(1); - } - } -} diff --git a/src-tauri/src/daemon/autostart.rs b/src-tauri/src/daemon/autostart.rs deleted file mode 100644 index 9727f12..0000000 --- a/src-tauri/src/daemon/autostart.rs +++ /dev/null @@ -1,351 +0,0 @@ -use directories::ProjectDirs; -#[cfg(any(target_os = "macos", target_os = "linux"))] -use std::fs; -use std::io; -use std::path::PathBuf; - -fn get_daemon_path() -> Option { - // First try to find the daemon binary in the same directory as the current executable - if let Ok(current_exe) = std::env::current_exe() { - let daemon_path = current_exe.parent()?.join(daemon_binary_name()); - if daemon_path.exists() { - return Some(daemon_path); - } - } - - // Try common installation paths - #[cfg(target_os = "macos")] - { - let paths = [ - PathBuf::from("/Applications/Donut Browser.app/Contents/MacOS/donut-daemon"), - dirs::home_dir()?.join("Applications/Donut Browser.app/Contents/MacOS/donut-daemon"), - ]; - for path in paths { - if path.exists() { - return Some(path); - } - } - } - - #[cfg(target_os = "windows")] - { - let paths = [ - dirs::data_local_dir()?.join("Donut Browser/donut-daemon.exe"), - PathBuf::from("C:\\Program Files\\Donut Browser\\donut-daemon.exe"), - ]; - for path in paths { - if path.exists() { - return Some(path); - } - } - } - - #[cfg(target_os = "linux")] - { - let paths = [ - PathBuf::from("/usr/bin/donut-daemon"), - PathBuf::from("/usr/local/bin/donut-daemon"), - dirs::home_dir()?.join(".local/bin/donut-daemon"), - ]; - for path in paths { - if path.exists() { - return Some(path); - } - } - } - - None -} - -fn daemon_binary_name() -> &'static str { - #[cfg(windows)] - { - "donut-daemon.exe" - } - #[cfg(not(windows))] - { - "donut-daemon" - } -} - -#[cfg(target_os = "macos")] -pub fn enable_autostart() -> io::Result<()> { - let daemon_path = get_daemon_path() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Daemon binary not found"))?; - - let plist_dir = dirs::home_dir() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found"))? - .join("Library/LaunchAgents"); - - fs::create_dir_all(&plist_dir)?; - - let plist_path = plist_dir.join("com.donutbrowser.daemon.plist"); - - // Get log directory (use data directory instead of /tmp) - let log_dir = get_data_dir() - .unwrap_or_else(|| PathBuf::from("/tmp")) - .join("logs"); - fs::create_dir_all(&log_dir)?; - - let plist_content = format!( - r#" - - - - Label - com.donutbrowser.daemon - ProgramArguments - - {daemon_path} - run - - RunAtLoad - - LimitLoadToSessionType - Aqua - ProcessType - Interactive - StandardOutPath - {log_dir}/daemon.out.log - StandardErrorPath - {log_dir}/daemon.err.log - - -"#, - daemon_path = daemon_path.display(), - log_dir = log_dir.display() - ); - - fs::write(&plist_path, plist_content)?; - - log::info!("Created launch agent at {:?}", plist_path); - Ok(()) -} - -#[cfg(target_os = "macos")] -pub fn get_plist_path() -> Option { - dirs::home_dir().map(|h| h.join("Library/LaunchAgents/com.donutbrowser.daemon.plist")) -} - -#[cfg(target_os = "macos")] -pub fn disable_autostart() -> io::Result<()> { - let plist_path = get_plist_path() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Home directory not found"))?; - - if plist_path.exists() { - // First unload the launch agent if it's loaded - let _ = unload_launch_agent(); - fs::remove_file(&plist_path)?; - log::info!("Removed launch agent at {:?}", plist_path); - } - - Ok(()) -} - -#[cfg(target_os = "macos")] -pub fn is_autostart_enabled() -> bool { - get_plist_path().is_some_and(|p| p.exists()) -} - -#[cfg(target_os = "macos")] -pub fn load_launch_agent() -> io::Result<()> { - use std::process::Command; - - let plist_path = get_plist_path() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Could not determine plist path"))?; - - if !plist_path.exists() { - return Err(io::Error::new( - io::ErrorKind::NotFound, - "Launch agent plist does not exist", - )); - } - - // Use launchctl load to start the daemon via launchd - // The -w flag writes the "disabled" key to the override plist - let output = Command::new("launchctl") - .args(["load", "-w"]) - .arg(&plist_path) - .output()?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - // "already loaded" is not an error condition for us - if !stderr.contains("already loaded") { - return Err(io::Error::other(format!( - "launchctl load failed: {}", - stderr - ))); - } - } - - log::info!("Loaded launch agent via launchctl"); - Ok(()) -} - -#[cfg(target_os = "macos")] -pub fn start_launch_agent() -> io::Result<()> { - use std::process::Command; - - let output = Command::new("launchctl") - .args(["start", "com.donutbrowser.daemon"]) - .output()?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(io::Error::other(format!( - "launchctl start failed: {}", - stderr - ))); - } - - log::info!("Started launch agent via launchctl"); - Ok(()) -} - -#[cfg(target_os = "macos")] -pub fn unload_launch_agent() -> io::Result<()> { - use std::process::Command; - - let plist_path = get_plist_path() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Could not determine plist path"))?; - - if !plist_path.exists() { - return Ok(()); - } - - let output = Command::new("launchctl") - .args(["unload"]) - .arg(&plist_path) - .output()?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - // Not being loaded is not an error - if !stderr.contains("Could not find specified service") { - log::warn!("launchctl unload warning: {}", stderr); - } - } - - log::info!("Unloaded launch agent via launchctl"); - Ok(()) -} - -#[cfg(target_os = "linux")] -pub fn enable_autostart() -> io::Result<()> { - let daemon_path = get_daemon_path() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Daemon binary not found"))?; - - let autostart_dir = dirs::config_dir() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Config directory not found"))? - .join("autostart"); - - fs::create_dir_all(&autostart_dir)?; - - let desktop_path = autostart_dir.join("donut-daemon.desktop"); - - let escaped_daemon_path = daemon_path - .display() - .to_string() - .replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('`', "\\`") - .replace('$', "\\$"); - let desktop_content = format!( - r#"[Desktop Entry] -Type=Application -Name=Donut Browser Daemon -Exec="{escaped_daemon_path}" run -Hidden=false -NoDisplay=true -X-GNOME-Autostart-enabled=true -"#, - ); - - fs::write(&desktop_path, desktop_content)?; - - log::info!("Created autostart entry at {:?}", desktop_path); - Ok(()) -} - -#[cfg(target_os = "linux")] -pub fn disable_autostart() -> io::Result<()> { - let desktop_path = dirs::config_dir() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Config directory not found"))? - .join("autostart/donut-daemon.desktop"); - - if desktop_path.exists() { - fs::remove_file(&desktop_path)?; - log::info!("Removed autostart entry at {:?}", desktop_path); - } - - Ok(()) -} - -#[cfg(target_os = "linux")] -pub fn is_autostart_enabled() -> bool { - dirs::config_dir() - .map(|c| c.join("autostart/donut-daemon.desktop").exists()) - .unwrap_or(false) -} - -#[cfg(target_os = "windows")] -pub fn enable_autostart() -> io::Result<()> { - use winreg::enums::HKEY_CURRENT_USER; - use winreg::RegKey; - - let daemon_path = get_daemon_path() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Daemon binary not found"))?; - - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - let (key, _) = hkcu.create_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Run")?; - - key.set_value( - "DonutBrowserDaemon", - &format!("\"{}\" run", daemon_path.display()), - )?; - - log::info!("Added registry autostart entry"); - Ok(()) -} - -#[cfg(target_os = "windows")] -pub fn disable_autostart() -> io::Result<()> { - use winreg::enums::HKEY_CURRENT_USER; - use winreg::RegKey; - - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - if let Ok(key) = hkcu.open_subkey_with_flags( - "Software\\Microsoft\\Windows\\CurrentVersion\\Run", - winreg::enums::KEY_WRITE, - ) { - let _ = key.delete_value("DonutBrowserDaemon"); - log::info!("Removed registry autostart entry"); - } - - Ok(()) -} - -#[cfg(target_os = "windows")] -pub fn is_autostart_enabled() -> bool { - use winreg::enums::HKEY_CURRENT_USER; - use winreg::RegKey; - - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - if let Ok(key) = hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Run") { - key.get_value::("DonutBrowserDaemon").is_ok() - } else { - false - } -} - -pub fn get_data_dir() -> Option { - if crate::app_dirs::is_portable() { - return Some(crate::app_dirs::data_dir()); - } - if let Some(proj_dirs) = ProjectDirs::from("com", "donutbrowser", "Donut Browser") { - Some(proj_dirs.data_dir().to_path_buf()) - } else { - dirs::home_dir().map(|h| h.join(".donutbrowser")) - } -} diff --git a/src-tauri/src/daemon/mod.rs b/src-tauri/src/daemon/mod.rs deleted file mode 100644 index f5280f9..0000000 --- a/src-tauri/src/daemon/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod autostart; -pub mod services; -pub mod tray; diff --git a/src-tauri/src/daemon/services.rs b/src-tauri/src/daemon/services.rs deleted file mode 100644 index 870f823..0000000 --- a/src-tauri/src/daemon/services.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::events::{self, DaemonEmitter, DaemonEvent}; -use std::sync::Arc; -use tokio::sync::broadcast; - -pub struct DaemonServices { - pub api_port: Option, - pub mcp_running: bool, - event_emitter: Arc, -} - -impl DaemonServices { - pub async fn start() -> Result { - log::info!("Starting daemon services..."); - - // Create the daemon event emitter - let (emitter, _rx) = DaemonEmitter::with_capacity(256); - let emitter_arc = Arc::new(emitter); - - // Set the global event emitter - if let Err(e) = events::set_global_emitter(emitter_arc.clone()) { - log::warn!("Failed to set global event emitter: {}", e); - } - - // NOTE: The API server currently requires an AppHandle which is only available - // in the Tauri GUI context. For now, the daemon starts with minimal services. - // The GUI will start the API server when it connects to the daemon. - // - // TODO: Refactor API server to work without AppHandle for daemon mode - let api_port = None; - let mcp_running = false; - - log::info!("Daemon services started (minimal mode - waiting for GUI connection)"); - - Ok(Self { - api_port, - mcp_running, - event_emitter: emitter_arc, - }) - } - - pub fn subscribe_events(&self) -> broadcast::Receiver { - self.event_emitter.subscribe() - } - - pub async fn stop(&mut self) { - log::info!("Stopping daemon services..."); - - self.api_port = None; - self.mcp_running = false; - } -} diff --git a/src-tauri/src/daemon/tray.rs b/src-tauri/src/daemon/tray.rs deleted file mode 100644 index 4787fbd..0000000 --- a/src-tauri/src/daemon/tray.rs +++ /dev/null @@ -1,204 +0,0 @@ -use std::process::Command; -use tray_icon::menu::{Menu, MenuItem}; -use tray_icon::{Icon, TrayIcon, TrayIconBuilder}; - -pub fn load_icon() -> Icon { - // On Windows, use the full-color icon so it renders well on dark taskbars. - // On macOS/Linux, use the template icon (black with alpha) for system light/dark handling. - #[cfg(target_os = "windows")] - let icon_bytes = include_bytes!("../../icons/tray-icon-win-44.png"); - #[cfg(not(target_os = "windows"))] - let icon_bytes = include_bytes!("../../icons/tray-icon-44.png"); - - let image = image::load_from_memory(icon_bytes) - .expect("Failed to load icon") - .into_rgba8(); - - let (width, height) = image.dimensions(); - let rgba = image.into_raw(); - - Icon::from_rgba(rgba, width, height).expect("Failed to create icon") -} - -pub struct TrayMenu { - pub menu: Menu, - pub quit_item: MenuItem, -} - -impl Default for TrayMenu { - fn default() -> Self { - Self::new() - } -} - -impl TrayMenu { - pub fn new() -> Self { - let menu = Menu::new(); - - let quit_item = MenuItem::new("Quit Donut Browser", true, None); - - menu.append(&quit_item).unwrap(); - - Self { menu, quit_item } - } -} - -pub fn create_tray_icon(icon: Icon, menu: &Menu) -> TrayIcon { - let builder = TrayIconBuilder::new() - .with_icon(icon) - .with_tooltip("Donut Browser") - .with_menu(Box::new(menu.clone())); - - // On macOS, template icons are automatically colored by the system for light/dark mode - #[cfg(target_os = "macos")] - let builder = builder.with_icon_as_template(true); - - builder.build().expect("Failed to create tray icon") -} - -/// Resolve the .app bundle path from the current daemon executable. -/// In production the daemon is at `Donut.app/Contents/MacOS/donut-daemon`. -#[cfg(target_os = "macos")] -fn get_app_bundle_path() -> Option { - let exe = std::env::current_exe().ok()?; - let macos_dir = exe.parent()?; - let contents_dir = macos_dir.parent()?; - let app_dir = contents_dir.parent()?; - if app_dir.extension().and_then(|e| e.to_str()) == Some("app") { - Some(app_dir.to_path_buf()) - } else { - None - } -} - -pub fn open_gui() { - log::info!("Opening GUI..."); - - #[cfg(target_os = "macos")] - { - // 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 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(); - } - } - - #[cfg(target_os = "windows")] - { - use std::path::PathBuf; - - if let Ok(current_exe) = std::env::current_exe() { - if let Some(exe_dir) = current_exe.parent() { - let app_path = exe_dir.join("donutbrowser.exe"); - if app_path.exists() { - let _ = Command::new(app_path).spawn(); - return; - } - } - } - - let paths = [ - dirs::data_local_dir().map(|p| p.join("Donut Browser").join("Donut Browser.exe")), - Some(PathBuf::from( - "C:\\Program Files\\Donut Browser\\Donut Browser.exe", - )), - ]; - - for path in paths.iter().flatten() { - if path.exists() { - let _ = Command::new(path).spawn(); - return; - } - } - } - - #[cfg(target_os = "linux")] - { - let _ = Command::new("donutbrowser").spawn(); - } -} - -fn read_gui_pid() -> Option { - let path = super::autostart::get_data_dir()?.join("daemon-state.json"); - let content = std::fs::read_to_string(path).ok()?; - let val: serde_json::Value = serde_json::from_str(&content).ok()?; - val.get("gui_pid")?.as_u64().map(|p| p as u32) -} - -fn kill_gui_by_pid() -> bool { - let Some(pid) = read_gui_pid() else { - return false; - }; - - #[cfg(unix)] - { - let ret = unsafe { libc::kill(pid as i32, libc::SIGTERM) }; - ret == 0 - } - - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - Command::new("taskkill") - .args(["/PID", &pid.to_string(), "/F"]) - .creation_flags(CREATE_NO_WINDOW) - .output() - .map(|o| o.status.success()) - .unwrap_or(false) - } - - #[cfg(not(any(unix, windows)))] - { - false - } -} - -pub fn quit_gui() { - log::info!("[daemon] Quitting GUI..."); - - if kill_gui_by_pid() { - log::info!("[daemon] GUI killed by PID"); - return; - } - - log::info!("[daemon] PID-based kill failed, falling back to name-based kill"); - - #[cfg(target_os = "macos")] - { - // Use spawn() instead of output() to avoid blocking the event loop. - // AppleScript has a ~2 minute default timeout that would freeze the tray icon. - let _ = Command::new("osascript") - .args(["-e", "tell application \"Donut\" to quit"]) - .spawn(); - } - - #[cfg(target_os = "windows")] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - let _ = Command::new("taskkill") - .args(["/IM", "Donut.exe", "/F"]) - .creation_flags(CREATE_NO_WINDOW) - .spawn(); - let _ = Command::new("taskkill") - .args(["/IM", "donutbrowser.exe", "/F"]) - .creation_flags(CREATE_NO_WINDOW) - .spawn(); - } - - #[cfg(target_os = "linux")] - { - let _ = Command::new("pkill").args(["-x", "donutbrowser"]).spawn(); - } -} diff --git a/src-tauri/src/daemon_client.rs b/src-tauri/src/daemon_client.rs deleted file mode 100644 index de786da..0000000 --- a/src-tauri/src/daemon_client.rs +++ /dev/null @@ -1,152 +0,0 @@ -use futures_util::{SinkExt, StreamExt}; -use serde::{Deserialize, Serialize}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use tauri::Emitter; -use tokio::sync::Mutex; -use tokio_tungstenite::{connect_async, tungstenite::Message}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WsMessage { - #[serde(rename = "type")] - pub msg_type: String, - pub event: Option, - pub payload: Option, -} - -pub struct DaemonClient { - app_handle: tauri::AppHandle, - connected: Arc, - shutdown: Arc, - daemon_port: Arc>>, -} - -impl DaemonClient { - pub fn new(app_handle: tauri::AppHandle) -> Self { - Self { - app_handle, - connected: Arc::new(AtomicBool::new(false)), - shutdown: Arc::new(AtomicBool::new(false)), - daemon_port: Arc::new(Mutex::new(None)), - } - } - - pub fn is_connected(&self) -> bool { - self.connected.load(Ordering::SeqCst) - } - - pub async fn connect(&self, port: u16) -> Result<(), String> { - *self.daemon_port.lock().await = Some(port); - - let url = format!("ws://127.0.0.1:{}/ws/events", port); - - log::info!("[daemon-client] Connecting to daemon at {}", url); - - let (ws_stream, _) = connect_async(&url) - .await - .map_err(|e| format!("Failed to connect to daemon: {}", e))?; - - self.connected.store(true, Ordering::SeqCst); - log::info!("[daemon-client] Connected to daemon"); - - let (mut write, mut read) = ws_stream.split(); - - let app_handle = self.app_handle.clone(); - let connected = self.connected.clone(); - let shutdown = self.shutdown.clone(); - - // Spawn task to handle incoming messages - tokio::spawn(async move { - while !shutdown.load(Ordering::SeqCst) { - match read.next().await { - Some(Ok(Message::Text(text))) => { - if let Ok(ws_msg) = serde_json::from_str::(&text) { - match ws_msg.msg_type.as_str() { - "event" => { - if let (Some(event), Some(payload)) = (ws_msg.event, ws_msg.payload) { - // Forward event to Tauri frontend - if let Err(e) = app_handle.emit(&event, payload) { - log::error!("[daemon-client] Failed to emit event: {}", e); - } - } - } - "connected" => { - log::info!("[daemon-client] Received connection confirmation"); - } - "pong" => { - log::debug!("[daemon-client] Received pong"); - } - _ => { - log::debug!("[daemon-client] Unknown message type: {}", ws_msg.msg_type); - } - } - } - } - Some(Ok(Message::Ping(data))) => { - log::debug!("[daemon-client] Received ping"); - if let Err(e) = write.send(Message::Pong(data)).await { - log::error!("[daemon-client] Failed to send pong: {}", e); - break; - } - } - Some(Ok(Message::Close(_))) => { - log::info!("[daemon-client] Daemon closed connection"); - break; - } - Some(Err(e)) => { - log::error!("[daemon-client] WebSocket error: {}", e); - break; - } - None => { - log::info!("[daemon-client] WebSocket stream ended"); - break; - } - _ => {} - } - } - - connected.store(false, Ordering::SeqCst); - log::info!("[daemon-client] Disconnected from daemon"); - }); - - Ok(()) - } - - pub fn disconnect(&self) { - self.shutdown.store(true, Ordering::SeqCst); - self.connected.store(false, Ordering::SeqCst); - } -} - -pub async fn start_daemon_connection(app_handle: tauri::AppHandle, port: u16) -> DaemonClient { - let client = DaemonClient::new(app_handle); - - if let Err(e) = client.connect(port).await { - log::error!("[daemon-client] Failed to connect: {}", e); - } - - client -} - -pub async fn find_and_connect_to_daemon(app_handle: tauri::AppHandle) -> Option { - // Try default port first - let default_port = 10108; - - log::info!( - "[daemon-client] Looking for daemon on port {}", - default_port - ); - - let client = DaemonClient::new(app_handle); - - match client.connect(default_port).await { - Ok(()) => Some(client), - Err(e) => { - log::warn!( - "[daemon-client] Could not connect to daemon on default port: {}", - e - ); - None - } - } -} diff --git a/src-tauri/src/daemon_spawn.rs b/src-tauri/src/daemon_spawn.rs deleted file mode 100644 index 4e4d797..0000000 --- a/src-tauri/src/daemon_spawn.rs +++ /dev/null @@ -1,360 +0,0 @@ -// Daemon Spawn - Start the daemon from the GUI -// Currently disabled; will be re-enabled in the future - -use serde::Deserialize; -use std::fs; -use std::path::PathBuf; -use std::process::{Command, Stdio}; -use std::thread; -use std::time::Duration; - -use crate::daemon::autostart; - -/// Check if a process with the given PID exists using the Windows API. -/// This avoids spawning tasklist.exe which causes a visible conhost window flash. -#[cfg(windows)] -fn win_process_exists(pid: u32) -> bool { - const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000; - - extern "system" { - fn OpenProcess(dwDesiredAccess: u32, bInheritHandles: i32, dwProcessId: u32) -> *mut (); - fn CloseHandle(hObject: *mut ()) -> i32; - } - - let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) }; - if handle.is_null() { - false - } else { - unsafe { CloseHandle(handle) }; - true - } -} - -#[derive(Debug, Deserialize, Default)] -struct DaemonState { - daemon_pid: Option, -} - -fn get_state_path() -> PathBuf { - autostart::get_data_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("daemon-state.json") -} - -fn read_state() -> DaemonState { - let path = get_state_path(); - if path.exists() { - if let Ok(content) = fs::read_to_string(&path) { - if let Ok(state) = serde_json::from_str(&content) { - return state; - } - } - } - DaemonState::default() -} - -pub fn is_daemon_running() -> bool { - let state = read_state(); - - if let Some(pid) = state.daemon_pid { - #[cfg(unix)] - { - unsafe { libc::kill(pid as i32, 0) == 0 } - } - - #[cfg(windows)] - { - win_process_exists(pid) - } - - #[cfg(not(any(unix, windows)))] - { - false - } - } else { - false - } -} - -#[cfg(target_os = "macos")] -fn is_dev_mode() -> bool { - if let Ok(current_exe) = std::env::current_exe() { - let path_str = current_exe.to_string_lossy(); - path_str.contains("target/debug") || path_str.contains("target/release") - } else { - false - } -} - -#[cfg(target_os = "macos")] -fn get_daemon_path() -> Option { - // First try to find the daemon binary next to the current executable - if let Ok(current_exe) = std::env::current_exe() { - if let Some(exe_dir) = current_exe.parent() { - let daemon_path = exe_dir.join("donut-daemon"); - if daemon_path.exists() { - return Some(daemon_path); - } - } - } - - // Try common installation paths - let paths = [ - PathBuf::from("/Applications/Donut Browser.app/Contents/MacOS/donut-daemon"), - dirs::home_dir() - .map(|h| h.join("Applications/Donut Browser.app/Contents/MacOS/donut-daemon")) - .unwrap_or_default(), - ]; - paths.into_iter().find(|path| path.exists()) -} - -#[cfg(any(target_os = "linux", windows))] -fn get_daemon_path() -> Option { - // First, try to find it next to the current executable - if let Ok(current_exe) = std::env::current_exe() { - let exe_dir = current_exe.parent()?; - - // Check for daemon binary in same directory - #[cfg(target_os = "windows")] - let daemon_name = "donut-daemon.exe"; - #[cfg(target_os = "linux")] - let daemon_name = "donut-daemon"; - - let daemon_path = exe_dir.join(daemon_name); - if daemon_path.exists() { - return Some(daemon_path); - } - } - - // Try to find it in PATH - #[cfg(target_os = "windows")] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - if let Ok(output) = Command::new("where") - .arg("donut-daemon") - .creation_flags(CREATE_NO_WINDOW) - .output() - { - if output.status.success() { - let path = String::from_utf8_lossy(&output.stdout); - let path = path.lines().next()?.trim(); - return Some(PathBuf::from(path)); - } - } - } - - #[cfg(target_os = "linux")] - { - if let Ok(output) = Command::new("which").arg("donut-daemon").output() { - if output.status.success() { - let path = String::from_utf8_lossy(&output.stdout); - let path = path.trim(); - if !path.is_empty() { - return Some(PathBuf::from(path)); - } - } - } - } - - None -} - -pub fn spawn_daemon() -> Result<(), String> { - // Log the daemon state for debugging - let state = read_state(); - log::info!("Daemon state before spawn: pid={:?}", state.daemon_pid); - - // Check if already running - if is_daemon_running() { - log::info!("Daemon is already running (verified by PID check)"); - return Ok(()); - } - - log::info!("Daemon is not running, attempting to start..."); - - // Log current exe location for debugging - let current_exe = std::env::current_exe().ok(); - log::info!("Current exe: {:?}", current_exe); - - // On macOS, use launchctl to start the daemon via launchd - // This ensures the daemon runs in the user's Aqua session with WindowServer access - // and survives app termination since it's managed by launchd, not as a child process - #[cfg(target_os = "macos")] - { - spawn_daemon_macos()?; - } - - // On Linux, use direct spawn - #[cfg(target_os = "linux")] - { - spawn_daemon_unix()?; - } - - #[cfg(windows)] - { - spawn_daemon_windows()?; - } - - // Wait for daemon to start (max 3 seconds) - for i in 0..30 { - thread::sleep(Duration::from_millis(100)); - if is_daemon_running() { - log::info!("Daemon started successfully after {}ms", (i + 1) * 100); - return Ok(()); - } - } - - // Check if we got a state file at least - let state = read_state(); - if let Some(pid) = state.daemon_pid { - log::info!("Daemon appears to have started (PID {} in state file)", pid); - return Ok(()); - } - - Err("Daemon did not start within timeout".to_string()) -} - -#[cfg(target_os = "macos")] -fn spawn_daemon_macos() -> Result<(), String> { - use std::os::unix::process::CommandExt; - - // In dev mode, use direct spawn instead of launchctl - // This avoids issues with plist paths pointing to wrong binaries - if is_dev_mode() { - log::info!("Dev mode detected, using direct spawn instead of launchctl"); - - let daemon_path = get_daemon_path().ok_or_else(|| { - format!( - "Could not find daemon binary. Current exe: {:?}", - std::env::current_exe().ok() - ) - })?; - - log::info!("Spawning daemon from: {:?}", daemon_path); - - // Create a new process group so daemon survives parent exit - let mut cmd = Command::new(&daemon_path); - cmd - .arg("run") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .process_group(0); - - cmd - .spawn() - .map_err(|e| format!("Failed to spawn daemon: {}", e))?; - - return Ok(()); - } - - // Production mode: use launchctl for proper daemon management - // First, ensure the LaunchAgent plist is installed - let autostart_enabled = autostart::is_autostart_enabled(); - log::info!("LaunchAgent plist exists: {}", autostart_enabled); - - if !autostart_enabled { - log::info!("Installing LaunchAgent plist for daemon management"); - autostart::enable_autostart().map_err(|e| format!("Failed to install LaunchAgent: {}", e))?; - log::info!("LaunchAgent plist installed successfully"); - } - - // Load the launch agent via launchctl - log::info!("Loading daemon via launchctl..."); - autostart::load_launch_agent().map_err(|e| format!("Failed to load LaunchAgent: {}", e))?; - log::info!("launchctl load completed"); - - // Also explicitly start the agent in case it was already loaded but stopped - if let Err(e) = autostart::start_launch_agent() { - log::debug!("launchctl start note (non-fatal): {}", e); - } - - Ok(()) -} - -#[cfg(target_os = "linux")] -fn spawn_daemon_unix() -> Result<(), String> { - use std::os::unix::process::CommandExt; - - let daemon_path = get_daemon_path().ok_or_else(|| { - format!( - "Could not find daemon binary. Current exe: {:?}", - std::env::current_exe().ok() - ) - })?; - - log::info!("Spawning daemon from: {:?}", daemon_path); - - // Create a new process group so daemon survives parent exit - let mut cmd = Command::new(&daemon_path); - cmd - .arg("run") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .process_group(0); - - cmd - .spawn() - .map_err(|e| format!("Failed to spawn daemon: {}", e))?; - - Ok(()) -} - -#[cfg(windows)] -fn spawn_daemon_windows() -> Result<(), String> { - use std::os::windows::process::CommandExt; - const DETACHED_PROCESS: u32 = 0x00000008; - const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; - - let daemon_path = get_daemon_path().ok_or_else(|| { - format!( - "Could not find daemon binary. Current exe: {:?}", - std::env::current_exe().ok() - ) - })?; - - log::info!("Spawning daemon from: {:?}", daemon_path); - - Command::new(&daemon_path) - .arg("run") - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP) - .spawn() - .map_err(|e| format!("Failed to spawn daemon: {}", e))?; - - Ok(()) -} - -pub fn ensure_daemon_running() -> Result<(), String> { - if !is_daemon_running() { - spawn_daemon()?; - } - Ok(()) -} - -pub fn register_gui_pid() { - let path = get_state_path(); - let mut val: serde_json::Value = if path.exists() { - fs::read_to_string(&path) - .ok() - .and_then(|c| serde_json::from_str(&c).ok()) - .unwrap_or_else(|| serde_json::json!({})) - } else { - serde_json::json!({}) - }; - - if let Some(obj) = val.as_object_mut() { - obj.insert( - "gui_pid".to_string(), - serde_json::Value::Number(std::process::id().into()), - ); - } - - if let Ok(content) = serde_json::to_string_pretty(&val) { - let _ = fs::write(&path, content); - } -} diff --git a/src-tauri/src/daemon_ws.rs b/src-tauri/src/daemon_ws.rs deleted file mode 100644 index ed0052c..0000000 --- a/src-tauri/src/daemon_ws.rs +++ /dev/null @@ -1,134 +0,0 @@ -use axum::{ - extract::{ - ws::{Message, WebSocket, WebSocketUpgrade}, - State, - }, - response::IntoResponse, -}; -use futures_util::{SinkExt, StreamExt}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -use crate::events::{DaemonEmitter, DaemonEvent}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WsMessage { - #[serde(rename = "type")] - pub msg_type: String, - pub event: Option, - pub payload: Option, -} - -#[derive(Clone)] -pub struct WsState { - event_emitter: Option>, -} - -impl WsState { - pub fn new() -> Self { - Self { - event_emitter: None, - } - } - - pub fn with_emitter(emitter: Arc) -> Self { - Self { - event_emitter: Some(emitter), - } - } -} - -impl Default for WsState { - fn default() -> Self { - Self::new() - } -} - -pub async fn ws_handler(ws: WebSocketUpgrade, State(state): State) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_socket(socket, state)) -} - -async fn handle_socket(socket: WebSocket, state: WsState) { - let (mut sender, mut receiver) = socket.split(); - - // Subscribe to daemon events if emitter is available - let mut event_rx = state.event_emitter.as_ref().map(|e| e.subscribe()); - - log::info!("[ws] Client connected"); - - // Send initial ping to confirm connection - let ping_msg = WsMessage { - msg_type: "connected".to_string(), - event: None, - payload: None, - }; - if let Ok(msg_str) = serde_json::to_string(&ping_msg) { - let _ = sender.send(Message::Text(msg_str.into())).await; - } - - loop { - tokio::select! { - // Handle incoming messages from client - Some(msg) = receiver.next() => { - match msg { - Ok(Message::Text(text)) => { - if let Ok(ws_msg) = serde_json::from_str::(&text) { - match ws_msg.msg_type.as_str() { - "ping" => { - let pong = WsMessage { - msg_type: "pong".to_string(), - event: None, - payload: None, - }; - if let Ok(msg_str) = serde_json::to_string(&pong) { - let _ = sender.send(Message::Text(msg_str.into())).await; - } - } - _ => { - log::debug!("[ws] Received unknown message type: {}", ws_msg.msg_type); - } - } - } - } - Ok(Message::Ping(data)) => { - let _ = sender.send(Message::Pong(data)).await; - } - Ok(Message::Close(_)) => { - log::info!("[ws] Client disconnected"); - break; - } - Err(e) => { - log::error!("[ws] Error receiving message: {}", e); - break; - } - _ => {} - } - } - - // Forward daemon events to client - Some(daemon_event) = async { - if let Some(ref mut rx) = event_rx { - rx.recv().await.ok() - } else { - std::future::pending::>().await - } - } => { - let ws_msg = WsMessage { - msg_type: "event".to_string(), - event: Some(daemon_event.event_type), - payload: Some(daemon_event.payload), - }; - if let Ok(msg_str) = serde_json::to_string(&ws_msg) { - if sender.send(Message::Text(msg_str.into())).await.is_err() { - log::error!("[ws] Failed to send event to client"); - break; - } - } - } - - else => break, - } - } - - log::info!("[ws] WebSocket connection closed"); -} diff --git a/src-tauri/src/events/mod.rs b/src-tauri/src/events/mod.rs index 159f494..985262e 100644 --- a/src-tauri/src/events/mod.rs +++ b/src-tauri/src/events/mod.rs @@ -1,10 +1,7 @@ use serde::Serialize; use std::sync::Arc; -use tokio::sync::broadcast; -/// Trait for emitting events to the frontend or connected clients. -/// This abstraction allows the same code to work in both GUI (Tauri) mode -/// and daemon mode (WebSocket broadcast). +/// Trait for emitting events to the frontend. /// /// Note: This trait uses `serde_json::Value` to be dyn-compatible. /// Use the convenience functions `emit()` and `emit_empty()` which accept @@ -37,49 +34,6 @@ impl EventEmitter for TauriEmitter { } } -/// Event message sent through the daemon's broadcast channel. -#[derive(Clone, Debug)] -pub struct DaemonEvent { - pub event_type: String, - pub payload: serde_json::Value, -} - -/// Daemon-based event emitter for background daemon mode. -/// Broadcasts events to all connected WebSocket clients. -#[derive(Clone)] -pub struct DaemonEmitter { - tx: broadcast::Sender, -} - -impl DaemonEmitter { - pub fn new(tx: broadcast::Sender) -> Self { - Self { tx } - } - - /// Create a new DaemonEmitter with a default channel capacity. - pub fn with_capacity(capacity: usize) -> (Self, broadcast::Receiver) { - let (tx, rx) = broadcast::channel(capacity); - (Self { tx }, rx) - } - - /// Subscribe to events from this emitter. - pub fn subscribe(&self) -> broadcast::Receiver { - self.tx.subscribe() - } -} - -impl EventEmitter for DaemonEmitter { - fn emit_value(&self, event: &str, payload: serde_json::Value) -> Result<(), String> { - let daemon_event = DaemonEvent { - event_type: event.to_string(), - payload, - }; - // Ignore send errors (no receivers connected) - let _ = self.tx.send(daemon_event); - Ok(()) - } -} - /// No-op emitter for testing or when events are not needed. #[derive(Clone, Default)] pub struct NoopEmitter; @@ -91,8 +45,7 @@ impl EventEmitter for NoopEmitter { } /// Global event emitter that can be set at runtime. -/// This allows managers to emit events without knowing whether they're -/// running in GUI or daemon mode. +/// This allows managers to emit events without holding an AppHandle directly. static GLOBAL_EMITTER: std::sync::OnceLock> = std::sync::OnceLock::new(); /// Set the global event emitter. This should be called once during app startup. @@ -136,30 +89,6 @@ mod tests { .is_ok()); } - #[test] - fn test_daemon_emitter() { - let (emitter, mut rx) = DaemonEmitter::with_capacity(16); - - // Emit an event - let _ = emitter.emit_value("test-event", serde_json::json!("hello")); - - // Check we received it - let event = rx.try_recv().unwrap(); - assert_eq!(event.event_type, "test-event"); - assert_eq!(event.payload, serde_json::json!("hello")); - } - - #[test] - fn test_daemon_emitter_no_receivers() { - let (tx, _) = broadcast::channel::(16); - let emitter = DaemonEmitter::new(tx); - - // Should not error even with no receivers - assert!(emitter - .emit_value("test-event", serde_json::json!("hello")) - .is_ok()); - } - #[test] fn test_emit_convenience_function() { // Test that emit() works with various types diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e16dedd..a319d7a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -52,11 +52,6 @@ mod wayfern_terms; pub mod cloud_auth; mod commercial_license; mod cookie_manager; -pub mod daemon; -pub mod daemon_client; -#[allow(dead_code)] -mod daemon_spawn; -pub mod daemon_ws; pub mod events; mod mcp_integrations; mod mcp_server; @@ -98,10 +93,10 @@ use downloaded_browsers_registry::{ use downloader::{cancel_download, download_browser}; use settings_manager::{ - decline_launch_on_login, dismiss_window_resize_warning, enable_launch_on_login, get_app_settings, - get_sync_settings, get_system_info, get_system_language, get_table_sorting_settings, - get_window_resize_warning_dismissed, open_log_directory, read_log_files, save_app_settings, - save_sync_settings, save_table_sorting_settings, should_show_launch_on_login_prompt, + dismiss_window_resize_warning, get_app_settings, get_sync_settings, get_system_info, + get_system_language, get_table_sorting_settings, get_window_resize_warning_dismissed, + open_log_directory, read_log_files, save_app_settings, save_sync_settings, + save_table_sorting_settings, }; use sync::{ @@ -196,7 +191,8 @@ impl WindowExt for WebviewWindow { } } -#[tauri::command] +// Called internally for deep-link / startup URL handling — not invoked from the +// frontend, so it is intentionally not a `#[tauri::command]`. async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), String> { log::info!("handle_url_open called with URL: {url}"); @@ -1175,6 +1171,34 @@ fn show_main_window(app_handle: &tauri::AppHandle) { } } +/// Update the tray menu labels with localized strings pushed from the frontend +/// (which owns the active language). The item ids are unchanged so the existing +/// menu-event handler keeps matching. +#[tauri::command] +fn update_tray_menu( + app_handle: tauri::AppHandle, + show_label: String, + quit_label: String, +) -> Result<(), String> { + use tauri::menu::{MenuBuilder, MenuItemBuilder}; + if let Some(tray) = app_handle.tray_by_id("main") { + let show_item = MenuItemBuilder::with_id("tray_show", show_label) + .build(&app_handle) + .map_err(|e| e.to_string())?; + let quit_item = MenuItemBuilder::with_id("tray_quit", quit_label) + .build(&app_handle) + .map_err(|e| e.to_string())?; + let menu = MenuBuilder::new(&app_handle) + .item(&show_item) + .separator() + .item(&quit_item) + .build() + .map_err(|e| e.to_string())?; + tray.set_menu(Some(menu)).map_err(|e| e.to_string())?; + } + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let args: Vec = env::args().collect(); @@ -1248,14 +1272,6 @@ pub fn run() { mgr.ensure_icons_extracted(); } - // Daemon (tray icon) is currently disabled — clean up any existing autostart - if daemon::autostart::is_autostart_enabled() { - log::info!("Removing daemon autostart (daemon is disabled)"); - if let Err(e) = daemon::autostart::disable_autostart() { - log::warn!("Failed to remove daemon autostart: {e}"); - } - } - // Create the main window programmatically #[allow(unused_variables)] let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()) @@ -1275,6 +1291,12 @@ pub fn run() { // System tray so the user can keep the app running after the close // dialog's "Minimize" action hides the window. + // + // These initial labels are bootstrap defaults only — the frontend pushes + // localized labels via `update_tray_menu` on mount and on every language + // change (the active language lives in the webview). The tray menu is only + // ever opened after the user minimizes to tray, by which point the + // frontend has already localized it, so these strings are never shown. { use tauri::menu::{MenuBuilder, MenuItemBuilder}; use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; @@ -2066,6 +2088,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ confirm_quit, hide_to_tray, + update_tray_menu, get_supported_browsers, is_browser_supported_on_platform, download_browser, @@ -2096,9 +2119,6 @@ pub fn run() { save_app_settings, read_log_files, open_log_directory, - should_show_launch_on_login_prompt, - enable_launch_on_login, - decline_launch_on_login, get_table_sorting_settings, save_table_sorting_settings, get_system_language, @@ -2216,7 +2236,6 @@ pub fn run() { disconnect_vpn, get_vpn_status, list_active_vpn_connections, - handle_url_open, // Cloud auth commands cloud_auth::cloud_exchange_device_code, cloud_auth::cloud_get_user, diff --git a/src-tauri/src/proxy_runner.rs b/src-tauri/src/proxy_runner.rs index e3095f2..780e5ce 100644 --- a/src-tauri/src/proxy_runner.rs +++ b/src-tauri/src/proxy_runner.rs @@ -28,7 +28,6 @@ fn unsuffixed_binary_name(base_name: &str) -> String { { match base_name { "donut-proxy" => "donut-proxy.exe".to_string(), - "donut-daemon" => "donut-daemon.exe".to_string(), _ => String::new(), } } diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index d2a38e8..b3ada91 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -50,8 +50,6 @@ pub struct AppSettings { #[serde(default)] pub mcp_token: Option, // Displayed token for user to copy (not persisted, loaded from encrypted file) #[serde(default)] - pub launch_on_login_declined: bool, // User permanently declined the launch-on-login prompt - #[serde(default)] pub language: Option, // ISO 639-1: "en", "es", "pt", "fr", "zh", "ja", "ko", "ru", or None for system default #[serde(default)] pub window_resize_warning_dismissed: bool, @@ -93,7 +91,6 @@ impl Default for AppSettings { mcp_enabled: false, mcp_port: None, mcp_token: None, - launch_on_login_declined: false, language: None, window_resize_warning_dismissed: false, disable_auto_updates: false, @@ -183,17 +180,6 @@ impl SettingsManager { Ok(()) } - pub fn should_show_launch_on_login_prompt(&self) -> Result> { - // Daemon is currently disabled, never show this prompt - Ok(false) - } - - pub fn decline_launch_on_login(&self) -> Result<(), Box> { - let mut settings = self.load_settings()?; - settings.launch_on_login_declined = true; - self.save_settings(&settings) - } - fn get_vault_password() -> String { env!("DONUT_BROWSER_VAULT_PASSWORD").to_string() } @@ -795,7 +781,6 @@ pub async fn save_app_settings( if let Ok(content) = std::fs::read_to_string(manager.get_settings_file()) { if let Ok(current) = serde_json::from_str::(&content) { settings.window_resize_warning_dismissed = current.window_resize_warning_dismissed; - settings.launch_on_login_declined = current.launch_on_login_declined; } } @@ -919,28 +904,6 @@ pub async fn open_log_directory(app_handle: tauri::AppHandle) -> Result<(), Stri Ok(()) } -#[tauri::command] -pub async fn should_show_launch_on_login_prompt() -> Result { - let manager = SettingsManager::instance(); - manager - .should_show_launch_on_login_prompt() - .map_err(|e| format!("Failed to check launch on login prompt setting: {e}")) -} - -#[tauri::command] -pub async fn enable_launch_on_login() -> Result<(), String> { - crate::daemon::autostart::enable_autostart() - .map_err(|e| format!("Failed to enable autostart: {e}")) -} - -#[tauri::command] -pub async fn decline_launch_on_login() -> Result<(), String> { - let manager = SettingsManager::instance(); - manager - .decline_launch_on_login() - .map_err(|e| format!("Failed to decline launch on login: {e}")) -} - #[tauri::command] pub async fn get_table_sorting_settings() -> Result { let manager = SettingsManager::instance(); @@ -1182,7 +1145,6 @@ mod tests { mcp_enabled: false, mcp_port: None, mcp_token: None, - launch_on_login_declined: false, language: None, window_resize_warning_dismissed: false, disable_auto_updates: false, @@ -1247,29 +1209,6 @@ mod tests { ); } - #[test] - fn test_should_show_launch_on_login_prompt() { - let (manager, _temp_dir, _guard) = create_test_settings_manager(); - - let result = manager.should_show_launch_on_login_prompt(); - assert!(result.is_ok(), "Should not fail"); - - let _should_show = result.unwrap(); - } - - #[test] - fn test_decline_launch_on_login() { - let (manager, _temp_dir, _guard) = create_test_settings_manager(); - - let settings = manager.load_settings().unwrap(); - assert!(!settings.launch_on_login_declined); - - manager.decline_launch_on_login().unwrap(); - - let settings = manager.load_settings().unwrap(); - assert!(settings.launch_on_login_declined); - } - #[test] fn test_load_corrupted_settings_file() { let (manager, _temp_dir, _guard) = create_test_settings_manager(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4fb82f5..1c6e150 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -19,7 +19,7 @@ "active": true, "targets": ["app", "dmg", "nsis", "deb", "rpm", "appimage"], "category": "Productivity", - "externalBin": ["binaries/donut-proxy", "binaries/donut-daemon"], + "externalBin": ["binaries/donut-proxy"], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/src/app/page.tsx b/src/app/page.tsx index 16da79d..de16b6f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -23,7 +23,6 @@ import { GroupManagementDialog } from "@/components/group-management-dialog"; import HomeHeader from "@/components/home-header"; import { ImportProfileDialog } from "@/components/import-profile-dialog"; import { IntegrationsDialog } from "@/components/integrations-dialog"; -import { LaunchOnLoginDialog } from "@/components/launch-on-login-dialog"; import { PermissionDialog } from "@/components/permission-dialog"; import { ProfilesDataTable } from "@/components/profile-data-table"; import { @@ -215,8 +214,6 @@ export default function Home() { const [passwordDialogMode, setPasswordDialogMode] = useState("set"); const pendingLaunchAfterUnlockRef = useRef(null); - const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false); - const [launchOnLoginDialogOpen, setLaunchOnLoginDialogOpen] = useState(false); const [windowResizeWarningOpen, setWindowResizeWarningOpen] = useState(false); const [windowResizeWarningBrowserType, setWindowResizeWarningBrowserType] = useState(undefined); @@ -546,24 +543,6 @@ export default function Home() { } }, [handleUrlOpen, hasCheckedStartupUrl]); - const checkStartupPrompt = useCallback(async () => { - // Only check once during app startup to prevent reopening after dismissing notifications - if (hasCheckedStartupPrompt) return; - - try { - const shouldShow = await invoke( - "should_show_launch_on_login_prompt", - ); - if (shouldShow) { - setLaunchOnLoginDialogOpen(true); - } - } catch (error) { - console.error("Failed to check startup prompt:", error); - } finally { - setHasCheckedStartupPrompt(true); - } - }, [hasCheckedStartupPrompt]); - // Handle profile errors from useProfileEvents hook useEffect(() => { if (profilesError) { @@ -1190,9 +1169,6 @@ export default function Home() { }, [profiles, t]); useEffect(() => { - // Check for startup default browser prompt - void checkStartupPrompt(); - // Listen for URL open events and get cleanup function const setupListeners = async () => { const cleanup = await listenForUrlEvents(); @@ -1235,7 +1211,6 @@ export default function Home() { }; }, [ checkForUpdates, - checkStartupPrompt, listenForUrlEvents, checkCurrentUrl, checkMissingBinaries, @@ -1337,11 +1312,13 @@ export default function Home() { showToast({ id: "browser-support-ending-warning", type: "error", - title: "Browser support ending soon", - description: `Support for the following profiles will be removed on March 15, 2026: ${unsupportedNames}. Please migrate to Wayfern or Camoufox profiles.`, + title: t("browserSupport.endingSoonTitle"), + description: t("browserSupport.endingSoonDescription", { + profiles: unsupportedNames, + }), duration: 15000, action: { - label: "Learn more", + label: t("common.buttons.learnMore"), onClick: () => { const event = new CustomEvent("url-open-request", { detail: "https://github.com/zhom/donutbrowser/discussions", @@ -1351,7 +1328,7 @@ export default function Home() { }, }); } - }, [profiles]); + }, [profiles, t]); // Re-check Wayfern terms when a browser download completes useEffect(() => { @@ -1851,14 +1828,6 @@ export default function Home() { onClose={checkTrialStatus} /> - {/* Launch on Login Dialog - shown on every startup until enabled or declined */} - { - setLaunchOnLoginDialogOpen(false); - }} - /> - { @@ -29,6 +29,24 @@ export function CloseConfirmDialog() { }; }, []); + // The native tray menu is built in Rust and cannot read the active language, + // so push localized labels to it on mount and whenever the language changes. + useEffect(() => { + const syncTrayMenu = () => { + void invoke("update_tray_menu", { + showLabel: t("tray.show"), + quitLabel: t("tray.quit"), + }).catch(() => { + // Tray is desktop-only; ignore on platforms without one. + }); + }; + syncTrayMenu(); + i18n.on("languageChanged", syncTrayMenu); + return () => { + i18n.off("languageChanged", syncTrayMenu); + }; + }, [t, i18n]); + const handleMinimize = async () => { setIsOpen(false); try { diff --git a/src/components/launch-on-login-dialog.tsx b/src/components/launch-on-login-dialog.tsx deleted file mode 100644 index 8541d91..0000000 --- a/src/components/launch-on-login-dialog.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client"; - -import { invoke } from "@tauri-apps/api/core"; -import { useCallback, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { LoadingButton } from "@/components/loading-button"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { showErrorToast, showSuccessToast } from "@/lib/toast-utils"; - -interface LaunchOnLoginDialogProps { - isOpen: boolean; - onClose: () => void; -} - -export function LaunchOnLoginDialog({ - isOpen, - onClose, -}: LaunchOnLoginDialogProps) { - const { t } = useTranslation(); - const [isEnabling, setIsEnabling] = useState(false); - const [isDeclining, setIsDeclining] = useState(false); - - const handleEnable = useCallback(async () => { - setIsEnabling(true); - try { - await invoke("enable_launch_on_login"); - showSuccessToast(t("launchOnLogin.enableSuccess")); - onClose(); - } catch (error) { - console.error("Failed to enable launch on login:", error); - showErrorToast(t("launchOnLogin.enableFailed"), { - description: - error instanceof Error ? error.message : t("launchOnLogin.tryAgain"), - }); - } finally { - setIsEnabling(false); - } - }, [onClose, t]); - - const handleDecline = useCallback(async () => { - setIsDeclining(true); - try { - await invoke("decline_launch_on_login"); - onClose(); - } catch (error) { - console.error("Failed to decline launch on login:", error); - showErrorToast(t("launchOnLogin.declineFailed"), { - description: - error instanceof Error ? error.message : t("launchOnLogin.tryAgain"), - }); - } finally { - setIsDeclining(false); - } - }, [onClose, t]); - - return ( - - { - e.preventDefault(); - }} - onPointerDownOutside={(e) => { - e.preventDefault(); - }} - onInteractOutside={(e) => { - e.preventDefault(); - }} - > - - {t("launchOnLogin.title")} - - -

- {t("launchOnLogin.description")} -

- - - - - {t("launchOnLogin.enableButton")} - - -
-
- ); -} diff --git a/src/components/shared-camoufox-config-form.tsx b/src/components/shared-camoufox-config-form.tsx index b3dab3c..0fa42ac 100644 --- a/src/components/shared-camoufox-config-form.tsx +++ b/src/components/shared-camoufox-config-form.tsx @@ -422,7 +422,9 @@ export function SharedCamoufoxConfigForm({ e.target.value || undefined, ); }} - placeholder="Mozilla/5.0..." + placeholder={t("common.placeholders.example", { + value: "Mozilla/5.0...", + })} />
@@ -436,7 +438,9 @@ export function SharedCamoufoxConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., MacIntel, Win32" + placeholder={t("common.placeholders.example", { + value: "MacIntel, Win32", + })} />
@@ -452,7 +456,9 @@ export function SharedCamoufoxConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., 5.0 (Macintosh)" + placeholder={t("common.placeholders.example", { + value: "5.0 (Macintosh)", + })} />
@@ -487,7 +493,7 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 8" + placeholder={t("common.placeholders.example", { value: "8" })} />
@@ -504,7 +510,7 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 0" + placeholder={t("common.placeholders.example", { value: "0" })} />
@@ -549,7 +555,9 @@ export function SharedCamoufoxConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., en-US" + placeholder={t("common.placeholders.example", { + value: "en-US", + })} />
@@ -573,7 +581,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1920" + placeholder={t("common.placeholders.example", { + value: "1920", + })} />
@@ -590,7 +600,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1080" + placeholder={t("common.placeholders.example", { + value: "1080", + })} />
@@ -607,7 +619,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1920" + placeholder={t("common.placeholders.example", { + value: "1920", + })} />
@@ -624,7 +638,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1055" + placeholder={t("common.placeholders.example", { + value: "1055", + })} />
@@ -641,7 +657,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 30" + placeholder={t("common.placeholders.example", { + value: "30", + })} />
@@ -658,7 +676,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 30" + placeholder={t("common.placeholders.example", { + value: "30", + })} />
@@ -682,7 +702,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1512" + placeholder={t("common.placeholders.example", { + value: "1512", + })} />
@@ -699,7 +721,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 886" + placeholder={t("common.placeholders.example", { + value: "886", + })} />
@@ -716,7 +740,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1512" + placeholder={t("common.placeholders.example", { + value: "1512", + })} />
@@ -733,7 +759,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 886" + placeholder={t("common.placeholders.example", { + value: "886", + })} />
@@ -748,7 +776,7 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 0" + placeholder={t("common.placeholders.example", { value: "0" })} />
@@ -763,7 +791,7 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 0" + placeholder={t("common.placeholders.example", { value: "0" })} />
@@ -786,7 +814,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseFloat(e.target.value) : undefined, ); }} - placeholder="e.g., 41.0019" + placeholder={t("common.placeholders.example", { + value: "41.0019", + })} />
@@ -802,7 +832,9 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseFloat(e.target.value) : undefined, ); }} - placeholder="e.g., 28.9645" + placeholder={t("common.placeholders.example", { + value: "28.9645", + })} />
@@ -817,7 +849,9 @@ export function SharedCamoufoxConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., America/New_York" + placeholder={t("common.placeholders.example", { + value: "America/New_York", + })} />
@@ -840,7 +874,9 @@ export function SharedCamoufoxConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., tr" + placeholder={t("common.placeholders.example", { + value: "tr", + })} />
@@ -854,7 +890,9 @@ export function SharedCamoufoxConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., TR" + placeholder={t("common.placeholders.example", { + value: "TR", + })} />
@@ -868,7 +906,9 @@ export function SharedCamoufoxConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., Latn" + placeholder={t("common.placeholders.example", { + value: "Latn", + })} />
@@ -891,7 +931,9 @@ export function SharedCamoufoxConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., Mesa" + placeholder={t("common.placeholders.example", { + value: "Mesa", + })} />
@@ -1053,7 +1095,7 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseFloat(e.target.value) : undefined, ); }} - placeholder="e.g., 0" + placeholder={t("common.placeholders.example", { value: "0" })} />
@@ -1071,7 +1113,7 @@ export function SharedCamoufoxConfigForm({ e.target.value ? parseFloat(e.target.value) : undefined, ); }} - placeholder="e.g., 0" + placeholder={t("common.placeholders.example", { value: "0" })} />
@@ -1240,7 +1282,9 @@ export function SharedCamoufoxConfigForm({ : undefined, ); }} - placeholder="e.g., 1920" + placeholder={t("common.placeholders.example", { + value: "1920", + })} />
@@ -1259,7 +1303,9 @@ export function SharedCamoufoxConfigForm({ : undefined, ); }} - placeholder="e.g., 1080" + placeholder={t("common.placeholders.example", { + value: "1080", + })} />
@@ -1278,7 +1324,9 @@ export function SharedCamoufoxConfigForm({ : undefined, ); }} - placeholder="e.g., 800" + placeholder={t("common.placeholders.example", { + value: "800", + })} />
@@ -1297,7 +1345,9 @@ export function SharedCamoufoxConfigForm({ : undefined, ); }} - placeholder="e.g., 600" + placeholder={t("common.placeholders.example", { + value: "600", + })} />
diff --git a/src/components/wayfern-config-form.tsx b/src/components/wayfern-config-form.tsx index d4addd4..aa9bf33 100644 --- a/src/components/wayfern-config-form.tsx +++ b/src/components/wayfern-config-form.tsx @@ -302,7 +302,9 @@ export function WayfernConfigForm({ e.target.value || undefined, ); }} - placeholder="Mozilla/5.0..." + placeholder={t("common.placeholders.example", { + value: "Mozilla/5.0...", + })} />
@@ -334,7 +336,9 @@ export function WayfernConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., 10.0.0" + placeholder={t("common.placeholders.example", { + value: "10.0.0", + })} />
@@ -348,7 +352,9 @@ export function WayfernConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., Google Chrome" + placeholder={t("common.placeholders.example", { + value: "Google Chrome", + })} />
@@ -364,7 +370,9 @@ export function WayfernConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., 143" + placeholder={t("common.placeholders.example", { + value: "143", + })} />
@@ -388,7 +396,7 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 8" + placeholder={t("common.placeholders.example", { value: "8" })} />
@@ -405,7 +413,7 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 0" + placeholder={t("common.placeholders.example", { value: "0" })} />
@@ -422,7 +430,7 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 8" + placeholder={t("common.placeholders.example", { value: "8" })} />
@@ -446,7 +454,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1920" + placeholder={t("common.placeholders.example", { + value: "1920", + })} />
@@ -463,7 +473,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1080" + placeholder={t("common.placeholders.example", { + value: "1080", + })} />
@@ -481,7 +493,9 @@ export function WayfernConfigForm({ e.target.value ? parseFloat(e.target.value) : undefined, ); }} - placeholder="e.g., 1.0" + placeholder={t("common.placeholders.example", { + value: "1.0", + })} />
@@ -498,7 +512,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1920" + placeholder={t("common.placeholders.example", { + value: "1920", + })} />
@@ -515,7 +531,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1040" + placeholder={t("common.placeholders.example", { + value: "1040", + })} />
@@ -532,7 +550,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 24" + placeholder={t("common.placeholders.example", { + value: "24", + })} />
@@ -556,7 +576,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1920" + placeholder={t("common.placeholders.example", { + value: "1920", + })} />
@@ -573,7 +595,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1040" + placeholder={t("common.placeholders.example", { + value: "1040", + })} />
@@ -590,7 +614,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 1920" + placeholder={t("common.placeholders.example", { + value: "1920", + })} />
@@ -607,7 +633,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 940" + placeholder={t("common.placeholders.example", { + value: "940", + })} />
@@ -622,7 +650,7 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 0" + placeholder={t("common.placeholders.example", { value: "0" })} />
@@ -637,7 +665,7 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 0" + placeholder={t("common.placeholders.example", { value: "0" })} />
@@ -660,7 +688,9 @@ export function WayfernConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., en-US" + placeholder={t("common.placeholders.example", { + value: "en-US", + })} />
@@ -740,7 +770,9 @@ export function WayfernConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., America/New_York" + placeholder={t("common.placeholders.example", { + value: "America/New_York", + })} />
@@ -775,7 +807,9 @@ export function WayfernConfigForm({ e.target.value ? parseFloat(e.target.value) : undefined, ); }} - placeholder="e.g., 40.7128" + placeholder={t("common.placeholders.example", { + value: "40.7128", + })} />
@@ -791,7 +825,9 @@ export function WayfernConfigForm({ e.target.value ? parseFloat(e.target.value) : undefined, ); }} - placeholder="e.g., -74.0060" + placeholder={t("common.placeholders.example", { + value: "-74.0060", + })} />
@@ -806,7 +842,9 @@ export function WayfernConfigForm({ e.target.value ? parseFloat(e.target.value) : undefined, ); }} - placeholder="e.g., 100" + placeholder={t("common.placeholders.example", { + value: "100", + })} />
@@ -829,7 +867,9 @@ export function WayfernConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., Intel" + placeholder={t("common.placeholders.example", { + value: "Intel", + })} />
@@ -926,7 +966,9 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 48000" + placeholder={t("common.placeholders.example", { + value: "48000", + })} />
@@ -943,7 +985,7 @@ export function WayfernConfigForm({ e.target.value ? parseInt(e.target.value, 10) : undefined, ); }} - placeholder="e.g., 2" + placeholder={t("common.placeholders.example", { value: "2" })} />
@@ -987,7 +1029,9 @@ export function WayfernConfigForm({ e.target.value ? parseFloat(e.target.value) : undefined, ); }} - placeholder="e.g., 0.85" + placeholder={t("common.placeholders.example", { + value: "0.85", + })} /> @@ -1008,7 +1052,9 @@ export function WayfernConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., Google Inc." + placeholder={t("common.placeholders.example", { + value: "Google Inc.", + })} />
@@ -1038,7 +1084,9 @@ export function WayfernConfigForm({ e.target.value || undefined, ); }} - placeholder="e.g., 20030107" + placeholder={t("common.placeholders.example", { + value: "20030107", + })} />
@@ -1197,7 +1245,9 @@ export function WayfernConfigForm({ : undefined, ); }} - placeholder="e.g., 1920" + placeholder={t("common.placeholders.example", { + value: "1920", + })} />
@@ -1216,7 +1266,9 @@ export function WayfernConfigForm({ : undefined, ); }} - placeholder="e.g., 1080" + placeholder={t("common.placeholders.example", { + value: "1080", + })} />
@@ -1235,7 +1287,9 @@ export function WayfernConfigForm({ : undefined, ); }} - placeholder="e.g., 800" + placeholder={t("common.placeholders.example", { + value: "800", + })} />
@@ -1254,7 +1308,9 @@ export function WayfernConfigForm({ : undefined, ); }} - placeholder="e.g., 600" + placeholder={t("common.placeholders.example", { + value: "600", + })} />
diff --git a/src/hooks/use-app-update-notifications.tsx b/src/hooks/use-app-update-notifications.tsx index 226fd0f..fcfa8f7 100644 --- a/src/hooks/use-app-update-notifications.tsx +++ b/src/hooks/use-app-update-notifications.tsx @@ -71,7 +71,7 @@ export function useAppUpdateNotifications() { percentage: 0, speed: undefined, eta: undefined, - message: "Starting update...", + message: t("appUpdate.toast.startingUpdate"), }); await invoke("download_and_prepare_app_update", { diff --git a/src/hooks/use-browser-download.ts b/src/hooks/use-browser-download.ts index 48f8cb3..a2bc08c 100644 --- a/src/hooks/use-browser-download.ts +++ b/src/hooks/use-browser-download.ts @@ -443,7 +443,7 @@ export function useBrowserDownload() { showToast({ id: "geoip-download", type: "download", - title: "Downloading GeoIP database", + title: i18n.t("browserDownload.toast.geoipDownloading"), stage: "downloading", progress: { percentage, @@ -455,7 +455,7 @@ export function useBrowserDownload() { showToast({ id: "geoip-download", type: "download", - title: "GeoIP database downloaded successfully!", + title: i18n.t("browserDownload.toast.geoipDownloaded"), stage: "completed", }); } diff --git a/src/hooks/use-update-notifications.tsx b/src/hooks/use-update-notifications.tsx index 0dc22b0..4e69aca 100644 --- a/src/hooks/use-update-notifications.tsx +++ b/src/hooks/use-update-notifications.tsx @@ -62,8 +62,12 @@ export function useUpdateNotifications( showToast({ id: `auto-update-started-${browser}-${newVersion}`, type: "loading", - title: `${browserDisplayName} update started`, - description: `Version ${newVersion} download will begin shortly. Browser launch is disabled until update completes.`, + title: i18n.t("versionUpdater.toast.updateStarted", { + browser: browserDisplayName, + }), + description: i18n.t("versionUpdater.toast.updateStartedDescription", { + version: newVersion, + }), duration: 4000, }); @@ -83,8 +87,11 @@ export function useUpdateNotifications( showToast({ id: `auto-update-skip-download-${browser}-${newVersion}`, type: "success", - title: `${browserDisplayName} ${newVersion} already available`, - description: "Updating profile configurations...", + title: i18n.t("versionUpdater.toast.alreadyAvailable", { + browser: browserDisplayName, + version: newVersion, + }), + description: i18n.t("versionUpdater.toast.updatingProfiles"), duration: 3000, }); } else { @@ -92,8 +99,11 @@ export function useUpdateNotifications( showToast({ id: `auto-update-download-starting-${browser}-${newVersion}`, type: "loading", - title: `Starting ${browserDisplayName} ${newVersion} download`, - description: "Download progress will be shown below...", + title: i18n.t("versionUpdater.toast.downloadStarting", { + browser: browserDisplayName, + version: newVersion, + }), + description: i18n.t("versionUpdater.toast.downloadProgressBelow"), duration: 4000, }); @@ -115,24 +125,36 @@ export function useUpdateNotifications( // Show success message based on whether profiles were updated if (updatedProfiles.length > 0) { - const profileText = + const description = updatedProfiles.length === 1 - ? `Profile "${updatedProfiles[0]}" has been updated` - : `${updatedProfiles.length} profiles have been updated`; + ? i18n.t("versionUpdater.toast.singleProfileUpdated", { + name: updatedProfiles[0], + version: newVersion, + }) + : i18n.t("versionUpdater.toast.multipleProfilesUpdated", { + count: updatedProfiles.length, + version: newVersion, + }); showToast({ id: `auto-update-success-${browser}-${newVersion}`, type: "success", - title: `${browserDisplayName} update completed`, - description: `${profileText} to version ${newVersion}. You can now launch your browsers with the latest version.`, + title: i18n.t("versionUpdater.toast.updateCompleted", { + browser: browserDisplayName, + }), + description, duration: 6000, }); } else { showToast({ id: `auto-update-success-${browser}-${newVersion}`, type: "success", - title: `${browserDisplayName} update completed`, - description: `Version ${newVersion} is now available. Running profiles will use the new version when restarted.`, + title: i18n.t("versionUpdater.toast.updateCompleted", { + browser: browserDisplayName, + }), + description: i18n.t("versionUpdater.toast.versionAvailable", { + version: newVersion, + }), duration: 6000, }); } diff --git a/src/hooks/use-version-updater.ts b/src/hooks/use-version-updater.ts index 78a6a23..83504ee 100644 --- a/src/hooks/use-version-updater.ts +++ b/src/hooks/use-version-updater.ts @@ -139,7 +139,13 @@ export function useVersionUpdater() { try { // Show auto-update start notification showAutoUpdateToast(browserDisplayName, new_version, { - description: `Downloading ${browserDisplayName} ${new_version} automatically. Progress will be shown below.`, + description: i18n.t( + "versionUpdater.toast.autoDownloadStarted", + { + browser: browserDisplayName, + version: new_version, + }, + ), }); // Dismiss the update notification in the backend diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 78b2962..2405f03 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -33,7 +33,8 @@ "minimize": "Minimize", "saving": "Saving…", "saved": "Saved", - "copied": "Copied" + "copied": "Copied", + "learnMore": "Learn more" }, "status": { "active": "Active", @@ -99,6 +100,9 @@ "srOnly": { "copy": "Copy", "copied": "Copied" + }, + "placeholders": { + "example": "e.g., {{value}}" } }, "settings": { @@ -1524,17 +1528,6 @@ "creatingButton": "Creating...", "createButton": "Create" }, - "launchOnLogin": { - "title": "Enable Launch on Login?", - "description": "Running in the background helps keep your proxies and browsers alive.", - "declineButton": "Don't Ask Again", - "declining": "...", - "enableButton": "Enable", - "enableSuccess": "Launch on login enabled", - "enableFailed": "Failed to enable launch on login", - "declineFailed": "Failed to save preference", - "tryAgain": "Please try again" - }, "wayfernTerms": { "title": "Wayfern Terms and Conditions", "description": "Before using Donut Browser, you must read and agree to Wayfern's Terms and Conditions.", @@ -1680,7 +1673,8 @@ "viewRelease": "View Release", "later": "Later", "uploading": "Uploading", - "downloading": "Downloading" + "downloading": "Downloading", + "startingUpdate": "Starting update..." } }, "browserDownload": { @@ -1694,7 +1688,9 @@ "extractionFailedDescription": "The corrupt file was deleted. It will be re-downloaded on next attempt.", "extracting": "Extracting browser files... Please do not close the app.", "verifying": "Verifying browser files...", - "downloadingRolling": "Downloading rolling release build..." + "downloadingRolling": "Downloading rolling release build...", + "geoipDownloading": "Downloading GeoIP database", + "geoipDownloaded": "GeoIP database downloaded successfully!" } }, "versionUpdater": { @@ -1712,7 +1708,12 @@ "updateSuccessDescription": "Found {{newVersions}} new versions across {{successfulUpdates}} browsers. Auto-downloads will start shortly.", "upToDate": "No new browser versions found", "upToDateDescription": "All browser versions are up to date", - "updateAllFailed": "Failed to update browser versions" + "updateAllFailed": "Failed to update browser versions", + "updateStarted": "{{browser}} update started", + "updateStartedDescription": "Version {{version}} download will begin shortly. Browser launch is disabled until update completes.", + "downloadStarting": "Starting {{browser}} {{version}} download", + "downloadProgressBelow": "Download progress will be shown below...", + "autoDownloadStarted": "Downloading {{browser}} {{version}} automatically. Progress will be shown below." } }, "profilePassword": { @@ -1918,5 +1919,13 @@ "description": "Would you like to send the app to the system tray or quit?", "minimize": "Minimize to Tray", "quit": "Quit" + }, + "tray": { + "show": "Show Donut Browser", + "quit": "Quit" + }, + "browserSupport": { + "endingSoonTitle": "Browser support ending soon", + "endingSoonDescription": "Support for the following profiles will be removed on March 15, 2026: {{profiles}}. Please migrate to Wayfern or Camoufox profiles." } } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 9191c56..2d2ff0f 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -33,7 +33,8 @@ "minimize": "Minimizar", "saving": "Guardando…", "saved": "Guardado", - "copied": "Copiado" + "copied": "Copiado", + "learnMore": "Más información" }, "status": { "active": "Activo", @@ -99,6 +100,9 @@ "srOnly": { "copy": "Copiar", "copied": "Copiado" + }, + "placeholders": { + "example": "p. ej., {{value}}" } }, "settings": { @@ -1524,17 +1528,6 @@ "creatingButton": "Creando...", "createButton": "Crear" }, - "launchOnLogin": { - "title": "¿Activar inicio al iniciar sesión?", - "description": "Ejecutarse en segundo plano ayuda a mantener vivos los proxies y navegadores.", - "declineButton": "No volver a preguntar", - "declining": "...", - "enableButton": "Activar", - "enableSuccess": "Inicio al iniciar sesión activado", - "enableFailed": "Error al activar el inicio al iniciar sesión", - "declineFailed": "Error al guardar la preferencia", - "tryAgain": "Por favor, inténtalo de nuevo" - }, "wayfernTerms": { "title": "Términos y condiciones de Wayfern", "description": "Antes de usar Donut Browser, debes leer y aceptar los Términos y Condiciones de Wayfern.", @@ -1680,7 +1673,8 @@ "viewRelease": "Ver lanzamiento", "later": "Más tarde", "uploading": "Subiendo", - "downloading": "Descargando" + "downloading": "Descargando", + "startingUpdate": "Iniciando actualización..." } }, "browserDownload": { @@ -1694,7 +1688,9 @@ "extractionFailedDescription": "El archivo dañado fue eliminado. Se volverá a descargar en el próximo intento.", "extracting": "Extrayendo archivos del navegador... No cierre la aplicación.", "verifying": "Verificando archivos del navegador...", - "downloadingRolling": "Descargando compilación rolling release..." + "downloadingRolling": "Descargando compilación rolling release...", + "geoipDownloading": "Descargando base de datos GeoIP", + "geoipDownloaded": "¡Base de datos GeoIP descargada correctamente!" } }, "versionUpdater": { @@ -1712,7 +1708,12 @@ "updateSuccessDescription": "Se encontraron {{newVersions}} nuevas versiones en {{successfulUpdates}} navegadores. Las descargas automáticas comenzarán pronto.", "upToDate": "No se encontraron nuevas versiones del navegador", "upToDateDescription": "Todas las versiones del navegador están actualizadas", - "updateAllFailed": "Error al actualizar las versiones del navegador" + "updateAllFailed": "Error al actualizar las versiones del navegador", + "updateStarted": "Actualización de {{browser}} iniciada", + "updateStartedDescription": "La descarga de la versión {{version}} comenzará en breve. El inicio del navegador está deshabilitado hasta que finalice la actualización.", + "downloadStarting": "Iniciando la descarga de {{browser}} {{version}}", + "downloadProgressBelow": "El progreso de la descarga se mostrará a continuación...", + "autoDownloadStarted": "Descargando {{browser}} {{version}} automáticamente. El progreso se mostrará a continuación." } }, "profilePassword": { @@ -1918,5 +1919,13 @@ "description": "¿Quieres enviar la aplicación a la bandeja del sistema o salir?", "minimize": "Minimizar a la bandeja", "quit": "Salir" + }, + "tray": { + "show": "Mostrar Donut Browser", + "quit": "Salir" + }, + "browserSupport": { + "endingSoonTitle": "El soporte del navegador finalizará pronto", + "endingSoonDescription": "El soporte para los siguientes perfiles se eliminará el 15 de marzo de 2026: {{profiles}}. Migra a perfiles de Wayfern o Camoufox." } } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index fa77510..c7a703f 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -33,7 +33,8 @@ "minimize": "Réduire", "saving": "Enregistrement…", "saved": "Enregistré", - "copied": "Copié" + "copied": "Copié", + "learnMore": "En savoir plus" }, "status": { "active": "Actif", @@ -99,6 +100,9 @@ "srOnly": { "copy": "Copier", "copied": "Copié" + }, + "placeholders": { + "example": "p. ex. {{value}}" } }, "settings": { @@ -1524,17 +1528,6 @@ "creatingButton": "Création...", "createButton": "Créer" }, - "launchOnLogin": { - "title": "Activer le démarrage à la connexion ?", - "description": "Tourner en arrière-plan permet de garder vos proxys et navigateurs actifs.", - "declineButton": "Ne plus demander", - "declining": "...", - "enableButton": "Activer", - "enableSuccess": "Démarrage à la connexion activé", - "enableFailed": "Échec de l'activation du démarrage à la connexion", - "declineFailed": "Échec de l'enregistrement de la préférence", - "tryAgain": "Veuillez réessayer" - }, "wayfernTerms": { "title": "Conditions générales de Wayfern", "description": "Avant d'utiliser Donut Browser, vous devez lire et accepter les Conditions Générales de Wayfern.", @@ -1680,7 +1673,8 @@ "viewRelease": "Voir la version", "later": "Plus tard", "uploading": "Envoi", - "downloading": "Téléchargement" + "downloading": "Téléchargement", + "startingUpdate": "Démarrage de la mise à jour..." } }, "browserDownload": { @@ -1694,7 +1688,9 @@ "extractionFailedDescription": "Le fichier corrompu a été supprimé. Il sera retéléchargé lors de la prochaine tentative.", "extracting": "Extraction des fichiers du navigateur... Ne fermez pas l'application.", "verifying": "Vérification des fichiers du navigateur...", - "downloadingRolling": "Téléchargement de la version rolling release..." + "downloadingRolling": "Téléchargement de la version rolling release...", + "geoipDownloading": "Téléchargement de la base de données GeoIP", + "geoipDownloaded": "Base de données GeoIP téléchargée avec succès !" } }, "versionUpdater": { @@ -1712,7 +1708,12 @@ "updateSuccessDescription": "{{newVersions}} nouvelles versions trouvées sur {{successfulUpdates}} navigateurs. Les téléchargements automatiques commenceront sous peu.", "upToDate": "Aucune nouvelle version de navigateur trouvée", "upToDateDescription": "Toutes les versions des navigateurs sont à jour", - "updateAllFailed": "Échec de la mise à jour des versions des navigateurs" + "updateAllFailed": "Échec de la mise à jour des versions des navigateurs", + "updateStarted": "Mise à jour de {{browser}} démarrée", + "updateStartedDescription": "Le téléchargement de la version {{version}} va bientôt commencer. Le lancement du navigateur est désactivé jusqu'à la fin de la mise à jour.", + "downloadStarting": "Démarrage du téléchargement de {{browser}} {{version}}", + "downloadProgressBelow": "La progression du téléchargement sera affichée ci-dessous...", + "autoDownloadStarted": "Téléchargement automatique de {{browser}} {{version}}. La progression sera affichée ci-dessous." } }, "profilePassword": { @@ -1918,5 +1919,13 @@ "description": "Voulez-vous envoyer l'application dans la zone de notification ou quitter ?", "minimize": "Réduire dans la barre d'état", "quit": "Quitter" + }, + "tray": { + "show": "Afficher Donut Browser", + "quit": "Quitter" + }, + "browserSupport": { + "endingSoonTitle": "La prise en charge du navigateur prend bientôt fin", + "endingSoonDescription": "La prise en charge des profils suivants sera supprimée le 15 mars 2026 : {{profiles}}. Veuillez migrer vers des profils Wayfern ou Camoufox." } } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index f6e7bc1..94c47ec 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -33,7 +33,8 @@ "minimize": "最小化", "saving": "保存中…", "saved": "保存しました", - "copied": "コピーしました" + "copied": "コピーしました", + "learnMore": "詳細" }, "status": { "active": "アクティブ", @@ -99,6 +100,9 @@ "srOnly": { "copy": "コピー", "copied": "コピーしました" + }, + "placeholders": { + "example": "例: {{value}}" } }, "settings": { @@ -1524,17 +1528,6 @@ "creatingButton": "作成中...", "createButton": "作成" }, - "launchOnLogin": { - "title": "ログイン時に起動しますか?", - "description": "バックグラウンドで実行することで、プロキシとブラウザを維持できます。", - "declineButton": "今後は表示しない", - "declining": "...", - "enableButton": "有効にする", - "enableSuccess": "ログイン時の起動を有効にしました", - "enableFailed": "ログイン時の起動を有効にできませんでした", - "declineFailed": "設定の保存に失敗しました", - "tryAgain": "もう一度お試しください" - }, "wayfernTerms": { "title": "Wayfern 利用規約", "description": "Donut Browser を使用する前に、Wayfern の利用規約を読み、同意する必要があります。", @@ -1680,7 +1673,8 @@ "viewRelease": "リリースを見る", "later": "後で", "uploading": "アップロード中", - "downloading": "ダウンロード中" + "downloading": "ダウンロード中", + "startingUpdate": "更新を開始しています..." } }, "browserDownload": { @@ -1694,7 +1688,9 @@ "extractionFailedDescription": "破損したファイルは削除されました。次回の試行時に再ダウンロードされます。", "extracting": "ブラウザファイルを展開中... アプリを閉じないでください。", "verifying": "ブラウザファイルを検証中...", - "downloadingRolling": "ローリングリリースビルドをダウンロード中..." + "downloadingRolling": "ローリングリリースビルドをダウンロード中...", + "geoipDownloading": "GeoIP データベースをダウンロード中", + "geoipDownloaded": "GeoIP データベースのダウンロードが完了しました!" } }, "versionUpdater": { @@ -1712,7 +1708,12 @@ "updateSuccessDescription": "{{successfulUpdates}} 個のブラウザに {{newVersions}} 個の新しいバージョンが見つかりました。自動ダウンロードがまもなく開始します。", "upToDate": "新しいブラウザのバージョンは見つかりませんでした", "upToDateDescription": "すべてのブラウザバージョンは最新です", - "updateAllFailed": "ブラウザバージョンの更新に失敗しました" + "updateAllFailed": "ブラウザバージョンの更新に失敗しました", + "updateStarted": "{{browser}} の更新を開始しました", + "updateStartedDescription": "バージョン {{version}} のダウンロードがまもなく開始されます。更新が完了するまでブラウザの起動は無効になります。", + "downloadStarting": "{{browser}} {{version}} のダウンロードを開始しています", + "downloadProgressBelow": "ダウンロードの進行状況は下に表示されます...", + "autoDownloadStarted": "{{browser}} {{version}} を自動的にダウンロードしています。進行状況は下に表示されます。" } }, "profilePassword": { @@ -1918,5 +1919,13 @@ "description": "アプリをシステムトレイに格納しますか、それとも終了しますか?", "minimize": "トレイに格納", "quit": "終了" + }, + "tray": { + "show": "Donut Browser を表示", + "quit": "終了" + }, + "browserSupport": { + "endingSoonTitle": "ブラウザのサポートが間もなく終了します", + "endingSoonDescription": "次のプロファイルのサポートは 2026 年 3 月 15 日に削除されます: {{profiles}}。Wayfern または Camoufox のプロファイルに移行してください。" } } diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index 58479e2..2a652da 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -33,7 +33,8 @@ "minimize": "최소화", "saving": "저장 중…", "saved": "저장됨", - "copied": "복사됨" + "copied": "복사됨", + "learnMore": "자세히 알아보기" }, "status": { "active": "활성", @@ -99,6 +100,9 @@ "srOnly": { "copy": "복사", "copied": "복사됨" + }, + "placeholders": { + "example": "예: {{value}}" } }, "settings": { @@ -1524,17 +1528,6 @@ "creatingButton": "생성 중...", "createButton": "생성" }, - "launchOnLogin": { - "title": "로그인 시 실행을 활성화하시겠습니까?", - "description": "백그라운드에서 실행하면 프록시와 브라우저를 계속 유지할 수 있습니다.", - "declineButton": "다시 묻지 않음", - "declining": "...", - "enableButton": "활성화", - "enableSuccess": "로그인 시 실행이 활성화되었습니다", - "enableFailed": "로그인 시 실행 활성화 실패", - "declineFailed": "환경 설정 저장 실패", - "tryAgain": "다시 시도하세요" - }, "wayfernTerms": { "title": "Wayfern 이용 약관", "description": "도넛 브라우저를 사용하기 전에 Wayfern의 이용 약관을 읽고 동의해야 합니다.", @@ -1680,7 +1673,8 @@ "viewRelease": "릴리스 보기", "later": "나중에", "uploading": "업로드 중", - "downloading": "다운로드 중" + "downloading": "다운로드 중", + "startingUpdate": "업데이트를 시작하는 중..." } }, "browserDownload": { @@ -1694,7 +1688,9 @@ "extractionFailedDescription": "손상된 파일이 삭제되었습니다. 다음 시도 시 다시 다운로드됩니다.", "extracting": "브라우저 파일 압축 해제 중... 앱을 닫지 마세요.", "verifying": "브라우저 파일 확인 중...", - "downloadingRolling": "롤링 릴리스 빌드 다운로드 중..." + "downloadingRolling": "롤링 릴리스 빌드 다운로드 중...", + "geoipDownloading": "GeoIP 데이터베이스 다운로드 중", + "geoipDownloaded": "GeoIP 데이터베이스를 성공적으로 다운로드했습니다!" } }, "versionUpdater": { @@ -1712,7 +1708,12 @@ "updateSuccessDescription": "{{successfulUpdates}}개 브라우저에서 {{newVersions}}개의 새 버전을 찾았습니다. 자동 다운로드가 곧 시작됩니다.", "upToDate": "새 브라우저 버전이 없습니다", "upToDateDescription": "모든 브라우저 버전이 최신입니다", - "updateAllFailed": "브라우저 버전 업데이트 실패" + "updateAllFailed": "브라우저 버전 업데이트 실패", + "updateStarted": "{{browser}} 업데이트를 시작했습니다", + "updateStartedDescription": "버전 {{version}} 다운로드가 곧 시작됩니다. 업데이트가 완료될 때까지 브라우저 실행이 비활성화됩니다.", + "downloadStarting": "{{browser}} {{version}} 다운로드를 시작하는 중", + "downloadProgressBelow": "다운로드 진행 상황이 아래에 표시됩니다...", + "autoDownloadStarted": "{{browser}} {{version}}을(를) 자동으로 다운로드하는 중입니다. 진행 상황이 아래에 표시됩니다." } }, "profilePassword": { @@ -1918,5 +1919,13 @@ "description": "앱을 시스템 트레이로 보내시겠습니까, 아니면 종료하시겠습니까?", "minimize": "트레이로 최소화", "quit": "종료" + }, + "tray": { + "show": "Donut Browser 표시", + "quit": "종료" + }, + "browserSupport": { + "endingSoonTitle": "브라우저 지원이 곧 종료됩니다", + "endingSoonDescription": "다음 프로필에 대한 지원이 2026년 3월 15일에 제거됩니다: {{profiles}}. Wayfern 또는 Camoufox 프로필로 마이그레이션하세요." } } diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 9173e49..7f4a4ea 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -33,7 +33,8 @@ "minimize": "Minimizar", "saving": "Salvando…", "saved": "Salvo", - "copied": "Copiado" + "copied": "Copiado", + "learnMore": "Saiba mais" }, "status": { "active": "Ativo", @@ -99,6 +100,9 @@ "srOnly": { "copy": "Copiar", "copied": "Copiado" + }, + "placeholders": { + "example": "ex.: {{value}}" } }, "settings": { @@ -1524,17 +1528,6 @@ "creatingButton": "Criando...", "createButton": "Criar" }, - "launchOnLogin": { - "title": "Ativar inicialização no login?", - "description": "Rodar em segundo plano ajuda a manter seus proxies e navegadores ativos.", - "declineButton": "Não perguntar novamente", - "declining": "...", - "enableButton": "Ativar", - "enableSuccess": "Inicialização no login ativada", - "enableFailed": "Falha ao ativar a inicialização no login", - "declineFailed": "Falha ao salvar a preferência", - "tryAgain": "Tente novamente" - }, "wayfernTerms": { "title": "Termos e condições da Wayfern", "description": "Antes de usar o Donut Browser, você deve ler e concordar com os Termos e Condições da Wayfern.", @@ -1680,7 +1673,8 @@ "viewRelease": "Ver lançamento", "later": "Mais tarde", "uploading": "Enviando", - "downloading": "Baixando" + "downloading": "Baixando", + "startingUpdate": "Iniciando atualização..." } }, "browserDownload": { @@ -1694,7 +1688,9 @@ "extractionFailedDescription": "O arquivo corrompido foi excluído. Será baixado novamente na próxima tentativa.", "extracting": "Extraindo arquivos do navegador... Não feche o aplicativo.", "verifying": "Verificando arquivos do navegador...", - "downloadingRolling": "Baixando build rolling release..." + "downloadingRolling": "Baixando build rolling release...", + "geoipDownloading": "Baixando banco de dados GeoIP", + "geoipDownloaded": "Banco de dados GeoIP baixado com sucesso!" } }, "versionUpdater": { @@ -1712,7 +1708,12 @@ "updateSuccessDescription": "Foram encontradas {{newVersions}} novas versões em {{successfulUpdates}} navegadores. Os downloads automáticos começarão em breve.", "upToDate": "Nenhuma nova versão de navegador encontrada", "upToDateDescription": "Todas as versões dos navegadores estão atualizadas", - "updateAllFailed": "Falha ao atualizar as versões dos navegadores" + "updateAllFailed": "Falha ao atualizar as versões dos navegadores", + "updateStarted": "Atualização do {{browser}} iniciada", + "updateStartedDescription": "O download da versão {{version}} começará em breve. O início do navegador está desativado até a atualização ser concluída.", + "downloadStarting": "Iniciando o download do {{browser}} {{version}}", + "downloadProgressBelow": "O progresso do download será mostrado abaixo...", + "autoDownloadStarted": "Baixando {{browser}} {{version}} automaticamente. O progresso será mostrado abaixo." } }, "profilePassword": { @@ -1918,5 +1919,13 @@ "description": "Você deseja enviar o aplicativo para a bandeja do sistema ou sair?", "minimize": "Minimizar para a bandeja", "quit": "Sair" + }, + "tray": { + "show": "Mostrar Donut Browser", + "quit": "Sair" + }, + "browserSupport": { + "endingSoonTitle": "O suporte ao navegador terminará em breve", + "endingSoonDescription": "O suporte aos seguintes perfis será removido em 15 de março de 2026: {{profiles}}. Migre para perfis Wayfern ou Camoufox." } } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 38bb80f..00b17d3 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -33,7 +33,8 @@ "minimize": "Свернуть", "saving": "Сохраняем…", "saved": "Сохранено", - "copied": "Скопировано" + "copied": "Скопировано", + "learnMore": "Подробнее" }, "status": { "active": "Активен", @@ -99,6 +100,9 @@ "srOnly": { "copy": "Скопировать", "copied": "Скопировано" + }, + "placeholders": { + "example": "напр., {{value}}" } }, "settings": { @@ -1524,17 +1528,6 @@ "creatingButton": "Создание...", "createButton": "Создать" }, - "launchOnLogin": { - "title": "Запускать при входе?", - "description": "Работа в фоновом режиме помогает поддерживать прокси и браузеры активными.", - "declineButton": "Больше не спрашивать", - "declining": "...", - "enableButton": "Включить", - "enableSuccess": "Запуск при входе включен", - "enableFailed": "Не удалось включить запуск при входе", - "declineFailed": "Не удалось сохранить настройку", - "tryAgain": "Пожалуйста, попробуйте снова" - }, "wayfernTerms": { "title": "Условия использования Wayfern", "description": "Прежде чем использовать Donut Browser, необходимо прочитать и согласиться с Условиями использования Wayfern.", @@ -1680,7 +1673,8 @@ "viewRelease": "Посмотреть релиз", "later": "Позже", "uploading": "Загрузка", - "downloading": "Скачивание" + "downloading": "Скачивание", + "startingUpdate": "Запуск обновления..." } }, "browserDownload": { @@ -1694,7 +1688,9 @@ "extractionFailedDescription": "Повреждённый файл удалён. Он будет повторно загружен при следующей попытке.", "extracting": "Распаковка файлов браузера... Не закрывайте приложение.", "verifying": "Проверка файлов браузера...", - "downloadingRolling": "Загрузка rolling release сборки..." + "downloadingRolling": "Загрузка rolling release сборки...", + "geoipDownloading": "Загрузка базы данных GeoIP", + "geoipDownloaded": "База данных GeoIP успешно загружена!" } }, "versionUpdater": { @@ -1712,7 +1708,12 @@ "updateSuccessDescription": "Найдено {{newVersions}} новых версий для {{successfulUpdates}} браузеров. Автоматическая загрузка начнётся в ближайшее время.", "upToDate": "Новых версий браузеров не найдено", "upToDateDescription": "Все версии браузеров актуальны", - "updateAllFailed": "Не удалось обновить версии браузеров" + "updateAllFailed": "Не удалось обновить версии браузеров", + "updateStarted": "Обновление {{browser}} началось", + "updateStartedDescription": "Загрузка версии {{version}} скоро начнётся. Запуск браузера отключён до завершения обновления.", + "downloadStarting": "Запуск загрузки {{browser}} {{version}}", + "downloadProgressBelow": "Прогресс загрузки будет показан ниже...", + "autoDownloadStarted": "Автоматическая загрузка {{browser}} {{version}}. Прогресс будет показан ниже." } }, "profilePassword": { @@ -1918,5 +1919,13 @@ "description": "Свернуть приложение в системный трей или выйти?", "minimize": "Свернуть в трей", "quit": "Выйти" + }, + "tray": { + "show": "Показать Donut Browser", + "quit": "Выход" + }, + "browserSupport": { + "endingSoonTitle": "Поддержка браузера скоро завершится", + "endingSoonDescription": "Поддержка следующих профилей будет прекращена 15 марта 2026 г.: {{profiles}}. Перейдите на профили Wayfern или Camoufox." } } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 8ad2e16..3e094c0 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -33,7 +33,8 @@ "minimize": "最小化", "saving": "正在保存…", "saved": "已保存", - "copied": "已复制" + "copied": "已复制", + "learnMore": "了解更多" }, "status": { "active": "活跃", @@ -99,6 +100,9 @@ "srOnly": { "copy": "复制", "copied": "已复制" + }, + "placeholders": { + "example": "例如:{{value}}" } }, "settings": { @@ -1524,17 +1528,6 @@ "creatingButton": "正在创建...", "createButton": "创建" }, - "launchOnLogin": { - "title": "启用登录时启动?", - "description": "在后台运行有助于保持代理和浏览器存活。", - "declineButton": "不再询问", - "declining": "...", - "enableButton": "启用", - "enableSuccess": "已启用登录时启动", - "enableFailed": "启用登录时启动失败", - "declineFailed": "保存偏好失败", - "tryAgain": "请重试" - }, "wayfernTerms": { "title": "Wayfern 条款和条件", "description": "在使用 Donut Browser 之前,你必须阅读并同意 Wayfern 的条款和条件。", @@ -1680,7 +1673,8 @@ "viewRelease": "查看版本", "later": "稍后", "uploading": "上传中", - "downloading": "下载中" + "downloading": "下载中", + "startingUpdate": "正在开始更新..." } }, "browserDownload": { @@ -1694,7 +1688,9 @@ "extractionFailedDescription": "损坏的文件已删除。下次尝试时将重新下载。", "extracting": "正在提取浏览器文件...请不要关闭应用。", "verifying": "正在验证浏览器文件...", - "downloadingRolling": "正在下载滚动发布版本..." + "downloadingRolling": "正在下载滚动发布版本...", + "geoipDownloading": "正在下载 GeoIP 数据库", + "geoipDownloaded": "GeoIP 数据库下载成功!" } }, "versionUpdater": { @@ -1712,7 +1708,12 @@ "updateSuccessDescription": "在 {{successfulUpdates}} 个浏览器中发现 {{newVersions}} 个新版本。自动下载即将开始。", "upToDate": "未发现新的浏览器版本", "upToDateDescription": "所有浏览器版本都是最新的", - "updateAllFailed": "更新浏览器版本失败" + "updateAllFailed": "更新浏览器版本失败", + "updateStarted": "{{browser}} 更新已开始", + "updateStartedDescription": "版本 {{version}} 即将开始下载。更新完成前浏览器启动将被禁用。", + "downloadStarting": "正在开始下载 {{browser}} {{version}}", + "downloadProgressBelow": "下载进度将显示在下方...", + "autoDownloadStarted": "正在自动下载 {{browser}} {{version}}。进度将显示在下方。" } }, "profilePassword": { @@ -1918,5 +1919,13 @@ "description": "您想将应用最小化到系统托盘还是退出?", "minimize": "最小化到托盘", "quit": "退出" + }, + "tray": { + "show": "显示 Donut Browser", + "quit": "退出" + }, + "browserSupport": { + "endingSoonTitle": "浏览器支持即将结束", + "endingSoonDescription": "以下配置文件的支持将于 2026 年 3 月 15 日移除:{{profiles}}。请迁移到 Wayfern 或 Camoufox 配置文件。" } }