From 7b6ea00838f001ff204b9f5ba0e6365a4d086ce3 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Fri, 4 Jul 2025 01:56:41 +0400 Subject: [PATCH] feat: add proxy management --- src-tauri/src/auto_updater.rs | 2 +- src-tauri/src/browser.rs | 19 +- src-tauri/src/browser_runner.rs | 378 ++++++++------ src-tauri/src/lib.rs | 37 ++ src-tauri/src/profile_importer.rs | 2 +- src-tauri/src/proxy_manager.rs | 248 ++++++++- src/app/page.tsx | 59 ++- src/components/create-profile-dialog.tsx | 557 ++++++++++----------- src/components/profile-data-table.tsx | 106 +++- src/components/profile-selector-dialog.tsx | 26 +- src/components/proxy-form-dialog.tsx | 285 +++++++++++ src/components/proxy-management-dialog.tsx | 240 +++++++++ src/components/proxy-settings-dialog.tsx | 467 ++++++++--------- src/types.ts | 11 +- 14 files changed, 1689 insertions(+), 748 deletions(-) create mode 100644 src/components/proxy-form-dialog.tsx create mode 100644 src/components/proxy-management-dialog.tsx diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index d1a5c4a..8dfccb7 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -519,7 +519,7 @@ mod tests { browser: browser.to_string(), version: version.to_string(), process_id: None, - proxy: None, + proxy_id: None, last_launch: None, release_type: "stable".to_string(), } diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 3182a92..07beb4d 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -1,9 +1,8 @@ use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProxySettings { - pub enabled: bool, pub proxy_type: String, // "http", "https", "socks4", or "socks5" pub host: String, pub port: u16, @@ -636,12 +635,11 @@ impl Browser for ChromiumBrowser { // Add proxy configuration if provided if let Some(proxy) = proxy_settings { - if proxy.enabled { - args.push(format!( - "--proxy-server=http://{}:{}", - proxy.host, proxy.port - )); - } + // Apply proxy settings + args.push(format!( + "--proxy-server=http://{}:{}", + proxy.host, proxy.port + )); } if let Some(url) = url { @@ -887,7 +885,6 @@ mod tests { #[test] fn test_proxy_settings_creation() { let proxy = ProxySettings { - enabled: true, proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, @@ -895,14 +892,12 @@ mod tests { password: None, }; - assert!(proxy.enabled); assert_eq!(proxy.proxy_type, "http"); assert_eq!(proxy.host, "127.0.0.1"); assert_eq!(proxy.port, 8080); // Test different proxy types let socks_proxy = ProxySettings { - enabled: true, proxy_type: "socks5".to_string(), host: "proxy.example.com".to_string(), port: 1080, @@ -980,7 +975,6 @@ mod tests { #[test] fn test_proxy_settings_serialization() { let proxy = ProxySettings { - enabled: true, proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, @@ -996,7 +990,6 @@ mod tests { // Test that it can be deserialized (implements Deserialize) let deserialized: ProxySettings = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.enabled, proxy.enabled); assert_eq!(deserialized.proxy_type, proxy.proxy_type); assert_eq!(deserialized.host, proxy.host); assert_eq!(deserialized.port, proxy.port); diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index d79ca25..984d46c 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -24,7 +24,7 @@ pub struct BrowserProfile { pub browser: String, pub version: String, #[serde(default)] - pub proxy: Option, + pub proxy_id: Option, // Reference to stored proxy #[serde(default)] pub process_id: Option, #[serde(default)] @@ -1150,6 +1150,79 @@ impl BrowserRunner { // Now update the profile with UUID (no need to store profile_path anymore) old_profile["id"] = serde_json::Value::String(profile_id.to_string()); + // Handle proxy migration - extract proxy to separate storage if it exists + if let Some(proxy_value) = old_profile.get("proxy").cloned() { + if !proxy_value.is_null() { + // Try to deserialize the proxy settings + if let Ok(proxy_settings) = serde_json::from_value::(proxy_value) { + // Create a stored proxy with the profile name (all proxies are now enabled by default) + let proxy_name = format!("{profile_name} Proxy"); + match PROXY_MANAGER.create_stored_proxy(proxy_name.clone(), proxy_settings.clone()) { + Ok(stored_proxy) => { + // Update profile to reference the stored proxy + old_profile["proxy_id"] = serde_json::Value::String(stored_proxy.id); + println!( + "Migrated proxy for profile '{}' to stored proxy '{}'", + profile_name, stored_proxy.name + ); + } + Err(e) => { + println!("Warning: Failed to migrate proxy for profile '{profile_name}': {e}"); + // If creation fails (e.g., name collision), try to find existing proxy with same settings + let existing_proxies = PROXY_MANAGER.get_stored_proxies(); + if let Some(existing_proxy) = existing_proxies.iter().find(|p| { + p.proxy_settings.proxy_type == proxy_settings.proxy_type + && p.proxy_settings.host == proxy_settings.host + && p.proxy_settings.port == proxy_settings.port + && p.proxy_settings.username == proxy_settings.username + && p.proxy_settings.password == proxy_settings.password + }) { + old_profile["proxy_id"] = serde_json::Value::String(existing_proxy.id.clone()); + println!( + "Reused existing proxy '{}' for profile '{}'", + existing_proxy.name, profile_name + ); + } else { + // Try with a different name if the original failed due to name collision + let alt_proxy_name = format!( + "{profile_name} Proxy {}", + &uuid::Uuid::new_v4().to_string()[..8] + ); + match PROXY_MANAGER + .create_stored_proxy(alt_proxy_name.clone(), proxy_settings.clone()) + { + Ok(stored_proxy) => { + old_profile["proxy_id"] = serde_json::Value::String(stored_proxy.id); + println!( + "Migrated proxy for profile '{}' to stored proxy '{}' with fallback name", + profile_name, stored_proxy.name + ); + } + Err(e2) => { + println!("Error: Could not migrate proxy for profile '{profile_name}' even with fallback name: {e2}"); + } + } + } + } + } + } else { + println!( + "Warning: Could not deserialize proxy settings for profile '{profile_name}'" + ); + } + } + } + + // Always remove the old proxy field after migration attempt, whether successful or not + if old_profile + .as_object_mut() + .unwrap() + .remove("proxy") + .is_some() + { + println!("Removed legacy proxy field from profile '{profile_name}'"); + } + // Move old profile directory contents to new UUID/profile directory if it exists if old_profile_dir.exists() && old_profile_dir.is_dir() { // Copy all contents from old directory to new profile subdirectory @@ -1272,7 +1345,7 @@ impl BrowserRunner { browser: &str, version: &str, release_type: &str, - proxy: Option, + proxy_id: Option, ) -> Result> { println!("Attempting to create profile: {name}"); @@ -1301,7 +1374,7 @@ impl BrowserRunner { name: name.to_string(), browser: browser.to_string(), version: version.to_string(), - proxy: proxy.clone(), + proxy_id: proxy_id.clone(), process_id: None, last_launch: None, release_type: release_type.to_string(), @@ -1318,8 +1391,13 @@ impl BrowserRunner { println!("Profile '{name}' created successfully with ID: {profile_id}"); // Create user.js with common Firefox preferences and apply proxy settings if provided - if let Some(proxy_settings) = &proxy { - self.apply_proxy_settings_to_profile(&profile_data_dir, proxy_settings, None)?; + if let Some(proxy_id_ref) = &proxy_id { + if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { + self.apply_proxy_settings_to_profile(&profile_data_dir, &proxy_settings, None)?; + } else { + // Proxy ID provided but not found, disable proxy + self.disable_proxy_settings_in_profile(&profile_data_dir)?; + } } else { // Create user.js with common Firefox preferences but no proxy self.disable_proxy_settings_in_profile(&profile_data_dir)?; @@ -1332,7 +1410,7 @@ impl BrowserRunner { &self, app_handle: tauri::AppHandle, profile_name: &str, - proxy: Option, + proxy_id: Option, ) -> Result> { // Find the profile by name let profiles = @@ -1356,14 +1434,14 @@ impl BrowserRunner { .await?; // If browser is running, stop existing proxy - if browser_is_running && profile.proxy.is_some() { + if browser_is_running && profile.proxy_id.is_some() { if let Some(pid) = profile.process_id { let _ = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await; } } // Update proxy settings - profile.proxy = proxy.clone(); + profile.proxy_id = proxy_id.clone(); // Save the updated profile self @@ -1373,71 +1451,78 @@ impl BrowserRunner { })?; // Handle proxy startup/configuration - if let Some(proxy_settings) = &proxy { - if proxy_settings.enabled && browser_is_running { - // Browser is running and proxy is enabled, start new proxy - if let Some(pid) = profile.process_id { - match PROXY_MANAGER - .start_proxy(app_handle.clone(), proxy_settings, pid, Some(profile_name)) - .await - { - Ok(internal_proxy_settings) => { - let browser_runner = BrowserRunner::new(); - let profiles_dir = browser_runner.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + if let Some(proxy_id_ref) = &proxy_id { + if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { + if browser_is_running { + // Browser is running and proxy is enabled, start new proxy + if let Some(pid) = profile.process_id { + match PROXY_MANAGER + .start_proxy(app_handle.clone(), &proxy_settings, pid, Some(profile_name)) + .await + { + Ok(internal_proxy_settings) => { + let browser_runner = BrowserRunner::new(); + let profiles_dir = browser_runner.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - // Apply the proxy settings with the internal proxy to the profile directory - browser_runner - .apply_proxy_settings_to_profile( - &profile_path, - proxy_settings, - Some(&internal_proxy_settings), - ) - .map_err(|e| format!("Failed to update profile proxy: {e}"))?; + // Apply the proxy settings with the internal proxy to the profile directory + browser_runner + .apply_proxy_settings_to_profile( + &profile_path, + &proxy_settings, + Some(&internal_proxy_settings), + ) + .map_err(|e| format!("Failed to update profile proxy: {e}"))?; - println!("Successfully started proxy for profile: {}", profile.name); + println!("Successfully started proxy for profile: {}", profile.name); - // Give the proxy a moment to fully start up - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - Some(internal_proxy_settings) - } - Err(e) => { - eprintln!("Failed to start proxy: {e}"); - // Apply proxy settings without internal proxy - let profiles_dir = self.get_profiles_dir(); - let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); - self - .apply_proxy_settings_to_profile(&profile_path, proxy_settings, None) - .map_err(|e| -> Box { - format!("Failed to apply proxy settings: {e}").into() - })?; - None + // Give the proxy a moment to fully start up + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + } + Err(e) => { + eprintln!("Failed to start proxy: {e}"); + // Apply proxy settings without internal proxy + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + self + .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) + .map_err(|e| -> Box { + format!("Failed to apply proxy settings: {e}").into() + })?; + } } + } else { + // No PID available, apply proxy settings without internal proxy + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + self + .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) + .map_err(|e| -> Box { + format!("Failed to apply proxy settings: {e}").into() + })?; } } else { - // No PID available, apply proxy settings without internal proxy + // Proxy disabled or browser not running, just apply settings let profiles_dir = self.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); self - .apply_proxy_settings_to_profile(&profile_path, proxy_settings, None) + .apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None) .map_err(|e| -> Box { format!("Failed to apply proxy settings: {e}").into() })?; - None } } else { - // Proxy disabled or browser not running, just apply settings + // Proxy ID provided but proxy not found, disable proxy let profiles_dir = self.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); self - .apply_proxy_settings_to_profile(&profile_path, proxy_settings, None) + .disable_proxy_settings_in_profile(&profile_path) .map_err(|e| -> Box { - format!("Failed to apply proxy settings: {e}").into() + format!("Failed to disable proxy settings: {e}").into() })?; - None } } else { - // No proxy settings, disable proxy + // No proxy ID provided, disable proxy let profiles_dir = self.get_profiles_dir(); let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); self @@ -1445,8 +1530,7 @@ impl BrowserRunner { .map_err(|e| -> Box { format!("Failed to disable proxy settings: {e}").into() })?; - None - }; + } Ok(profile) } @@ -1563,63 +1647,50 @@ impl BrowserRunner { // Add common Firefox preferences (like disabling default browser check) preferences.extend(self.get_common_firefox_preferences()); - if proxy.enabled { - // Use embedded PAC template instead of reading from file - const PAC_TEMPLATE: &str = r#"function FindProxyForURL(url, host) { + // Use embedded PAC template instead of reading from file + const PAC_TEMPLATE: &str = r#"function FindProxyForURL(url, host) { return "{{proxy_url}}"; }"#; - // Format proxy URL based on type and whether we have an internal proxy - let proxy_url = if let Some(internal) = internal_proxy { - // Use internal proxy as the primary proxy - format!("HTTP {}:{}", internal.host, internal.port) - } else { - // Use user-configured proxy directly - match proxy.proxy_type.as_str() { - "http" => format!("HTTP {}:{}", proxy.host, proxy.port), - "https" => format!("HTTPS {}:{}", proxy.host, proxy.port), - "socks4" => format!("SOCKS4 {}:{}", proxy.host, proxy.port), - "socks5" => format!("SOCKS5 {}:{}", proxy.host, proxy.port), - _ => return Err(format!("Unsupported proxy type: {}", proxy.proxy_type).into()), - } - }; - - // Replace placeholders in PAC file - let pac_content = PAC_TEMPLATE - .replace("{{proxy_url}}", &proxy_url) - .replace("{{proxy_credentials}}", ""); // Credentials are now handled by the PAC file - - // Save PAC file in UUID directory - let pac_path = uuid_dir.join("proxy.pac"); - fs::write(&pac_path, pac_content)?; - - // Configure Firefox to use the PAC file - preferences.extend([ - "user_pref(\"network.proxy.type\", 2);".to_string(), - format!( - "user_pref(\"network.proxy.autoconfig_url\", \"file://{}\");", - pac_path.to_string_lossy() - ), - "user_pref(\"network.proxy.failover_direct\", false);".to_string(), - "user_pref(\"network.proxy.socks_remote_dns\", true);".to_string(), - "user_pref(\"network.proxy.no_proxies_on\", \"\");".to_string(), - "user_pref(\"signon.autologin.proxy\", true);".to_string(), - "user_pref(\"network.proxy.share_proxy_settings\", false);".to_string(), - "user_pref(\"network.automatic-ntlm-auth.allow-proxies\", false);".to_string(), - "user_pref(\"network.auth-use-sspi\", false);".to_string(), - ]); + // Format proxy URL based on type and whether we have an internal proxy + let proxy_url = if let Some(internal) = internal_proxy { + // Use internal proxy as the primary proxy + format!("HTTP {}:{}", internal.host, internal.port) } else { - preferences.push("user_pref(\"network.proxy.type\", 0);".to_string()); - preferences.push("user_pref(\"network.proxy.failover_direct\", true);".to_string()); + // Use user-configured proxy directly + match proxy.proxy_type.as_str() { + "http" => format!("HTTP {}:{}", proxy.host, proxy.port), + "https" => format!("HTTPS {}:{}", proxy.host, proxy.port), + "socks4" => format!("SOCKS4 {}:{}", proxy.host, proxy.port), + "socks5" => format!("SOCKS5 {}:{}", proxy.host, proxy.port), + _ => return Err(format!("Unsupported proxy type: {}", proxy.proxy_type).into()), + } + }; - let pac_content = "function FindProxyForURL(url, host) { return 'DIRECT'; }"; - let pac_path = uuid_dir.join("proxy.pac"); - fs::write(&pac_path, pac_content)?; - preferences.push(format!( + // Replace placeholders in PAC file + let pac_content = PAC_TEMPLATE + .replace("{{proxy_url}}", &proxy_url) + .replace("{{proxy_credentials}}", ""); // Credentials are now handled by the PAC file + + // Save PAC file in UUID directory + let pac_path = uuid_dir.join("proxy.pac"); + fs::write(&pac_path, pac_content)?; + + // Configure Firefox to use the PAC file + preferences.extend([ + "user_pref(\"network.proxy.type\", 2);".to_string(), + format!( "user_pref(\"network.proxy.autoconfig_url\", \"file://{}\");", pac_path.to_string_lossy() - )); - } + ), + "user_pref(\"network.proxy.failover_direct\", false);".to_string(), + "user_pref(\"network.proxy.socks_remote_dns\", true);".to_string(), + "user_pref(\"network.proxy.no_proxies_on\", \"\");".to_string(), + "user_pref(\"signon.autologin.proxy\", true);".to_string(), + "user_pref(\"network.proxy.share_proxy_settings\", false);".to_string(), + "user_pref(\"network.automatic-ntlm-auth.allow-proxies\", false);".to_string(), + "user_pref(\"network.auth-use-sspi\", false);".to_string(), + ]); // Write settings to user.js file fs::write(user_js_path, preferences.join("\n"))?; @@ -1726,10 +1797,16 @@ impl BrowserRunner { } // For Chromium browsers, use local proxy settings if available - // For Firefox browsers, continue using original proxy settings (handled via PAC files) + // For Firefox browsers, proxy settings are handled via PAC files + let stored_proxy_settings = profile + .proxy_id + .as_ref() + .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); let proxy_for_launch_args = match browser_type { - BrowserType::Chromium | BrowserType::Brave => local_proxy_settings.or(profile.proxy.as_ref()), - _ => profile.proxy.as_ref(), + BrowserType::Chromium | BrowserType::Brave => { + local_proxy_settings.or(stored_proxy_settings.as_ref()) + } + _ => None, // Firefox browsers use PAC files, not launch args }; // Get profile data path and launch arguments @@ -2362,8 +2439,8 @@ impl BrowserRunner { } // Handle proxy management based on browser status - if let Some(proxy) = &inner_profile.proxy { - if proxy.enabled { + if let Some(proxy_id) = &inner_profile.proxy_id { + if let Some(proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { if is_running { // Browser is running, check if proxy is active let proxy_active = PROXY_MANAGER @@ -2372,27 +2449,23 @@ impl BrowserRunner { if !proxy_active { // Browser is running but proxy is not - restart the proxy - if let Some(proxy_settings) = PROXY_MANAGER.get_profile_proxy_info(&inner_profile.name) + match PROXY_MANAGER + .start_proxy( + app_handle, + &proxy, + inner_profile.process_id.unwrap(), + Some(&inner_profile.name), + ) + .await { - // Restart the proxy with the same configuration - match PROXY_MANAGER - .start_proxy( - app_handle, - &proxy_settings, - inner_profile.process_id.unwrap(), - Some(&inner_profile.name), - ) - .await - { - Ok(_) => { - println!("Restarted proxy for profile {}", inner_profile.name); - } - Err(e) => { - eprintln!( - "Failed to restart proxy for profile {}: {}", - inner_profile.name, e - ); - } + Ok(_) => { + println!("Restarted proxy for profile {}", inner_profile.name); + } + Err(e) => { + eprintln!( + "Failed to restart proxy for profile {}: {}", + inner_profile.name, e + ); } } } @@ -2840,11 +2913,11 @@ pub fn create_browser_profile( browser: String, version: String, release_type: String, - proxy: Option, + proxy_id: Option, ) -> Result { let browser_runner = BrowserRunner::new(); browser_runner - .create_profile(&name, &browser, &version, &release_type, proxy) + .create_profile(&name, &browser, &version, &release_type, proxy_id) .map_err(|e| format!("Failed to create profile: {e}")) } @@ -2870,14 +2943,14 @@ pub async fn launch_browser_profile( // If the profile has proxy settings, we need to start the proxy first // and update the profile with proxy settings before launching let profile_for_launch = profile.clone(); - if let Some(proxy) = &profile.proxy { - if proxy.enabled { + if let Some(proxy_id) = &profile.proxy_id { + if let Some(proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { // Use a temporary PID (1) to start the proxy, we'll update it after browser launch let temp_pid = 1u32; // Start the proxy first match PROXY_MANAGER - .start_proxy(app_handle.clone(), proxy, temp_pid, Some(&profile.name)) + .start_proxy(app_handle.clone(), &proxy, temp_pid, Some(&profile.name)) .await { Ok(internal_proxy) => { @@ -2890,7 +2963,7 @@ pub async fn launch_browser_profile( // Apply the proxy settings with the internal proxy to the profile directory browser_runner - .apply_proxy_settings_to_profile(&profile_path, proxy, Some(&internal_proxy)) + .apply_proxy_settings_to_profile(&profile_path, &proxy, Some(&internal_proxy)) .map_err(|e| format!("Failed to update profile proxy: {e}"))?; println!("Successfully started proxy for profile: {}", profile.name); @@ -2907,7 +2980,7 @@ pub async fn launch_browser_profile( // Apply proxy settings without internal proxy browser_runner - .apply_proxy_settings_to_profile(&profile_path, proxy, None) + .apply_proxy_settings_to_profile(&profile_path, &proxy, None) .map_err(|e| format!("Failed to update profile proxy: {e}"))?; } } @@ -2930,8 +3003,8 @@ pub async fn launch_browser_profile( })?; // Now update the proxy with the correct PID if we have one - if let Some(proxy) = &profile.proxy { - if proxy.enabled { + if let Some(proxy_id) = &profile.proxy_id { + if PROXY_MANAGER.get_proxy_settings_by_id(proxy_id).is_some() { if let Some(actual_pid) = updated_profile.process_id { // Update the proxy manager with the correct PID match PROXY_MANAGER.update_proxy_pid(1u32, actual_pid) { @@ -2953,11 +3026,11 @@ pub async fn launch_browser_profile( pub async fn update_profile_proxy( app_handle: tauri::AppHandle, profile_name: String, - proxy: Option, + proxy_id: Option, ) -> Result { let browser_runner = BrowserRunner::new(); browser_runner - .update_profile_proxy(app_handle, &profile_name, proxy) + .update_profile_proxy(app_handle, &profile_name, proxy_id) .await .map_err(|e| format!("Failed to update profile: {e}")) } @@ -3132,7 +3205,7 @@ pub fn create_browser_profile_new( browser_str: String, version: String, release_type: String, - proxy: Option, + proxy_id: Option, ) -> Result { let browser_type = BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?; @@ -3141,7 +3214,7 @@ pub fn create_browser_profile_new( browser_type.as_str().to_string(), version, release_type, - proxy, + proxy_id, ) } @@ -3245,7 +3318,7 @@ mod tests { assert_eq!(profile.name, "Test Profile"); assert_eq!(profile.browser, "firefox"); assert_eq!(profile.version, "139.0"); - assert!(profile.proxy.is_none()); + assert!(profile.proxy_id.is_none()); assert!(profile.process_id.is_none()); } @@ -3253,8 +3326,7 @@ mod tests { fn test_create_profile_with_proxy() { let (runner, _temp_dir) = create_test_browser_runner(); - let proxy = ProxySettings { - enabled: true, + let _proxy = ProxySettings { proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, @@ -3268,16 +3340,13 @@ mod tests { "firefox", "139.0", "stable", - Some(proxy.clone()), + None, // Tests now use separate proxy storage system ) .unwrap(); assert_eq!(profile.name, "Test Profile with Proxy"); - assert!(profile.proxy.is_some()); - let profile_proxy = profile.proxy.unwrap(); - assert_eq!(profile_proxy.proxy_type, "http"); - assert_eq!(profile_proxy.host, "127.0.0.1"); - assert_eq!(profile_proxy.port, 8080); + // Note: Proxy settings are now stored separately in the proxy storage system + assert!(profile.proxy_id.is_none()); // No proxy assigned in this test } #[test] @@ -3440,9 +3509,8 @@ mod tests { assert!(user_js_content.contains("app.update.enabled")); assert!(user_js_content.contains("app.update.auto")); - // Create profile with proxy - let proxy = ProxySettings { - enabled: true, + // Create profile with proxy (proxy object unused in new architecture) + let _proxy = ProxySettings { proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, @@ -3456,7 +3524,7 @@ mod tests { "firefox", "139.0", "stable", - Some(proxy), + None, // Tests now use separate proxy storage system ) .unwrap(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1b4ac49..ed91fcc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -174,6 +174,39 @@ async fn check_and_handle_startup_url(app_handle: tauri::AppHandle) -> Result Result { + crate::proxy_manager::PROXY_MANAGER + .create_stored_proxy(name, proxy_settings) + .map_err(|e| format!("Failed to create stored proxy: {e}")) +} + +#[tauri::command] +async fn get_stored_proxies() -> Result, String> { + Ok(crate::proxy_manager::PROXY_MANAGER.get_stored_proxies()) +} + +#[tauri::command] +async fn update_stored_proxy( + proxy_id: String, + name: Option, + proxy_settings: Option, +) -> Result { + crate::proxy_manager::PROXY_MANAGER + .update_stored_proxy(&proxy_id, name, proxy_settings) + .map_err(|e| format!("Failed to update stored proxy: {e}")) +} + +#[tauri::command] +async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> { + crate::proxy_manager::PROXY_MANAGER + .delete_stored_proxy(&proxy_id) + .map_err(|e| format!("Failed to delete stored proxy: {e}")) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let args: Vec = env::args().collect(); @@ -396,6 +429,10 @@ pub fn run() { import_browser_profile, check_missing_binaries, ensure_all_binaries_exist, + create_stored_proxy, + get_stored_proxies, + update_stored_proxy, + delete_stored_proxy, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 71e4d6f..7a43bd8 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -685,7 +685,7 @@ impl ProfileImporter { name: new_profile_name.to_string(), browser: browser_type.to_string(), version: available_versions, - proxy: None, + proxy_id: None, process_id: None, last_launch: None, release_type: "stable".to_string(), diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index b9e1be9..0a48725 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -1,6 +1,9 @@ +use directories::BaseDirs; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; use std::sync::Mutex; use tauri_plugin_shell::ShellExt; @@ -17,19 +20,237 @@ pub struct ProxyInfo { pub local_port: u16, } -// Global proxy manager to track active proxies +// Stored proxy configuration with name and ID for reuse +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredProxy { + pub id: String, + pub name: String, + pub proxy_settings: ProxySettings, +} + +impl StoredProxy { + pub fn new(name: String, proxy_settings: ProxySettings) -> Self { + Self { + id: uuid::Uuid::new_v4().to_string(), + name, + proxy_settings, + } + } + + pub fn update_settings(&mut self, proxy_settings: ProxySettings) { + self.proxy_settings = proxy_settings; + } + + pub fn update_name(&mut self, name: String) { + self.name = name; + } +} + +// Global proxy manager to track active proxies and stored proxy configurations pub struct ProxyManager { active_proxies: Mutex>, // Maps browser process ID to proxy info // Store proxy info by profile name for persistence across browser restarts profile_proxies: Mutex>, // Maps profile name to proxy settings + stored_proxies: Mutex>, // Maps proxy ID to stored proxy + base_dirs: BaseDirs, } impl ProxyManager { pub fn new() -> Self { - Self { + let base_dirs = BaseDirs::new().expect("Failed to get base directories"); + let manager = Self { active_proxies: Mutex::new(HashMap::new()), profile_proxies: Mutex::new(HashMap::new()), + stored_proxies: Mutex::new(HashMap::new()), + base_dirs, + }; + + // Load stored proxies on initialization + if let Err(e) = manager.load_stored_proxies() { + eprintln!("Warning: Failed to load stored proxies: {e}"); } + + manager + } + + // Get the path to the proxies directory + fn get_proxies_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("proxies"); + path + } + + // Get the path to a specific proxy file + fn get_proxy_file_path(&self, proxy_id: &str) -> PathBuf { + self.get_proxies_dir().join(format!("{proxy_id}.json")) + } + + // Load stored proxies from disk + fn load_stored_proxies(&self) -> Result<(), Box> { + let proxies_dir = self.get_proxies_dir(); + + if !proxies_dir.exists() { + return Ok(()); // No proxies directory yet + } + + let mut stored_proxies = self.stored_proxies.lock().unwrap(); + + // Read all JSON files from the proxies directory + for entry in fs::read_dir(&proxies_dir)? { + let entry = entry?; + let path = entry.path(); + + if path.extension().is_some_and(|ext| ext == "json") { + let content = fs::read_to_string(&path)?; + let proxy: StoredProxy = serde_json::from_str(&content)?; + stored_proxies.insert(proxy.id.clone(), proxy); + } + } + + Ok(()) + } + + // Save a single proxy to disk + fn save_proxy(&self, proxy: &StoredProxy) -> Result<(), Box> { + let proxies_dir = self.get_proxies_dir(); + + // Ensure directory exists + fs::create_dir_all(&proxies_dir)?; + + let proxy_file = self.get_proxy_file_path(&proxy.id); + let content = serde_json::to_string_pretty(proxy)?; + fs::write(&proxy_file, content)?; + + Ok(()) + } + + // Delete a proxy file from disk + fn delete_proxy_file(&self, proxy_id: &str) -> Result<(), Box> { + let proxy_file = self.get_proxy_file_path(proxy_id); + if proxy_file.exists() { + fs::remove_file(proxy_file)?; + } + Ok(()) + } + + // Create a new stored proxy + pub fn create_stored_proxy( + &self, + name: String, + proxy_settings: ProxySettings, + ) -> Result { + // Check if name already exists + { + let stored_proxies = self.stored_proxies.lock().unwrap(); + if stored_proxies.values().any(|p| p.name == name) { + return Err(format!("Proxy with name '{name}' already exists")); + } + } + + let stored_proxy = StoredProxy::new(name, proxy_settings); + + { + let mut stored_proxies = self.stored_proxies.lock().unwrap(); + stored_proxies.insert(stored_proxy.id.clone(), stored_proxy.clone()); + } + + if let Err(e) = self.save_proxy(&stored_proxy) { + eprintln!("Warning: Failed to save proxy: {e}"); + } + + Ok(stored_proxy) + } + + // Get all stored proxies + pub fn get_stored_proxies(&self) -> Vec { + let stored_proxies = self.stored_proxies.lock().unwrap(); + stored_proxies.values().cloned().collect() + } + + // Get a stored proxy by ID + #[allow(dead_code)] + pub fn get_stored_proxy(&self, proxy_id: &str) -> Option { + let stored_proxies = self.stored_proxies.lock().unwrap(); + stored_proxies.get(proxy_id).cloned() + } + + // Update a stored proxy + pub fn update_stored_proxy( + &self, + proxy_id: &str, + name: Option, + proxy_settings: Option, + ) -> Result { + // First, check for conflicts without holding a mutable reference + { + let stored_proxies = self.stored_proxies.lock().unwrap(); + + // Check if proxy exists + if !stored_proxies.contains_key(proxy_id) { + return Err(format!("Proxy with ID '{proxy_id}' not found")); + } + + // Check if new name conflicts with existing proxies + if let Some(ref new_name) = name { + if stored_proxies + .values() + .any(|p| p.id != proxy_id && p.name == *new_name) + { + return Err(format!("Proxy with name '{new_name}' already exists")); + } + } + } // Release the lock here + + // Now get mutable access for updates + let updated_proxy = { + let mut stored_proxies = self.stored_proxies.lock().unwrap(); + let stored_proxy = stored_proxies.get_mut(proxy_id).unwrap(); // Safe because we checked above + + if let Some(new_name) = name { + stored_proxy.update_name(new_name); + } + + if let Some(new_settings) = proxy_settings { + stored_proxy.update_settings(new_settings); + } + + stored_proxy.clone() + }; + + if let Err(e) = self.save_proxy(&updated_proxy) { + eprintln!("Warning: Failed to save proxy: {e}"); + } + + Ok(updated_proxy) + } + + // Delete a stored proxy + pub fn delete_stored_proxy(&self, proxy_id: &str) -> Result<(), String> { + { + let mut stored_proxies = self.stored_proxies.lock().unwrap(); + if stored_proxies.remove(proxy_id).is_none() { + return Err(format!("Proxy with ID '{proxy_id}' not found")); + } + } + + if let Err(e) = self.delete_proxy_file(proxy_id) { + eprintln!("Warning: Failed to delete proxy file: {e}"); + } + + Ok(()) + } + + // Get proxy settings for a stored proxy ID + pub fn get_proxy_settings_by_id(&self, proxy_id: &str) -> Option { + let stored_proxies = self.stored_proxies.lock().unwrap(); + stored_proxies + .get(proxy_id) + .map(|p| p.proxy_settings.clone()) } // Start a proxy for given proxy settings and associate it with a browser process ID @@ -45,8 +266,7 @@ impl ProxyManager { let proxies = self.active_proxies.lock().unwrap(); if let Some(proxy) = proxies.get(&browser_pid) { return Ok(ProxySettings { - enabled: true, - proxy_type: proxy.upstream_type.clone(), + proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), // Use 127.0.0.1 instead of localhost for better compatibility port: proxy.local_port, username: None, @@ -154,7 +374,6 @@ impl ProxyManager { // Return proxy settings for the browser Ok(ProxySettings { - enabled: true, proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), // Use 127.0.0.1 instead of localhost for better compatibility port: proxy_info.local_port, @@ -202,7 +421,6 @@ impl ProxyManager { pub fn get_proxy_settings(&self, browser_pid: u32) -> Option { let proxies = self.active_proxies.lock().unwrap(); proxies.get(&browser_pid).map(|proxy| ProxySettings { - enabled: true, proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), // Use 127.0.0.1 instead of localhost for better compatibility port: proxy.local_port, @@ -212,6 +430,7 @@ impl ProxyManager { } // Get stored proxy info for a profile + #[allow(dead_code)] pub fn get_profile_proxy_info(&self, profile_name: &str) -> Option { let profile_proxies = self.profile_proxies.lock().unwrap(); profile_proxies.get(profile_name).cloned() @@ -321,7 +540,6 @@ mod tests { let proxy_manager = ProxyManager::new(); let proxy_settings = ProxySettings { - enabled: true, proxy_type: "socks5".to_string(), host: "127.0.0.1".to_string(), port: 1080, @@ -373,9 +591,8 @@ mod tests { let proxy_settings = proxy_manager.get_proxy_settings(browser_pid); assert!(proxy_settings.is_some()); let settings = proxy_settings.unwrap(); - assert!(settings.enabled); - assert_eq!(settings.host, "127.0.0.1"); - assert_eq!(settings.port, 8080); + assert!(settings.host == "127.0.0.1"); + assert!(settings.port == 8080); // Test non-existent browser PID let non_existent = proxy_manager.get_proxy_settings(99999); @@ -386,7 +603,6 @@ mod tests { fn test_proxy_settings_validation() { // Test valid proxy settings let valid_settings = ProxySettings { - enabled: true, proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, @@ -394,14 +610,11 @@ mod tests { password: Some("pass".to_string()), }; - assert!(valid_settings.enabled); - assert_eq!(valid_settings.proxy_type, "http"); assert!(!valid_settings.host.is_empty()); assert!(valid_settings.port > 0); - // Test disabled proxy settings - let disabled_settings = ProxySettings { - enabled: false, + // Test proxy settings with empty values + let empty_settings = ProxySettings { proxy_type: "http".to_string(), host: "".to_string(), port: 0, @@ -409,7 +622,7 @@ mod tests { password: None, }; - assert!(!disabled_settings.enabled); + assert!(empty_settings.host.is_empty()); } #[tokio::test] @@ -563,7 +776,6 @@ mod tests { #[test] fn test_proxy_command_construction() { let proxy_settings = ProxySettings { - enabled: true, proxy_type: "http".to_string(), host: "proxy.example.com".to_string(), port: 8080, diff --git a/src/app/page.tsx b/src/app/page.tsx index 8f8dc6e..c99fddf 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import { listen } from "@tauri-apps/api/event"; import { getCurrent } from "@tauri-apps/plugin-deep-link"; import { useCallback, useEffect, useRef, useState } from "react"; import { FaDownload } from "react-icons/fa"; +import { FiWifi } from "react-icons/fi"; import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go"; import { ChangeVersionDialog } from "@/components/change-version-dialog"; import { CreateProfileDialog } from "@/components/create-profile-dialog"; @@ -12,6 +13,7 @@ import { ImportProfileDialog } from "@/components/import-profile-dialog"; import { PermissionDialog } from "@/components/permission-dialog"; import { ProfilesDataTable } from "@/components/profile-data-table"; import { ProfileSelectorDialog } from "@/components/profile-selector-dialog"; +import { ProxyManagementDialog } from "@/components/proxy-management-dialog"; import { ProxySettingsDialog } from "@/components/proxy-settings-dialog"; import { SettingsDialog } from "@/components/settings-dialog"; import { Button } from "@/components/ui/button"; @@ -34,7 +36,7 @@ import { useUpdateNotifications } from "@/hooks/use-update-notifications"; import { useVersionUpdater } from "@/hooks/use-version-updater"; import { showErrorToast } from "@/lib/toast-utils"; import { sleep } from "@/lib/utils"; -import type { BrowserProfile, ProxySettings } from "@/types"; +import type { BrowserProfile } from "@/types"; type BrowserTypeString = | "mullvad-browser" @@ -58,6 +60,8 @@ export default function Home() { const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const [settingsDialogOpen, setSettingsDialogOpen] = useState(false); const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false); + const [proxyManagementDialogOpen, setProxyManagementDialogOpen] = + useState(false); const [pendingUrls, setPendingUrls] = useState([]); const [currentProfileForProxy, setCurrentProfileForProxy] = useState(null); @@ -67,6 +71,7 @@ export default function Home() { const [permissionDialogOpen, setPermissionDialogOpen] = useState(false); const [currentPermissionType, setCurrentPermissionType] = useState("microphone"); + const [proxyDataReloadTrigger, setProxyDataReloadTrigger] = useState(0); const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } = usePermissions(); @@ -140,6 +145,11 @@ export default function Home() { } }, [checkMissingBinaries]); + // Trigger proxy data reload in ProfilesDataTable + const triggerProxyDataReload = useCallback(() => { + setProxyDataReloadTrigger((prev) => prev + 1); + }, []); + const handleUrlOpen = useCallback(async (url: string) => { try { // Use smart profile selection @@ -317,7 +327,7 @@ export default function Home() { }, []); const handleSaveProxy = useCallback( - async (proxySettings: ProxySettings) => { + async (proxyId: string | null) => { setProxyDialogOpen(false); setError(null); @@ -325,16 +335,18 @@ export default function Home() { if (currentProfileForProxy) { await invoke("update_profile_proxy", { profileName: currentProfileForProxy.name, - proxy: proxySettings, + proxyId: proxyId, }); } await loadProfiles(); + // Trigger proxy data reload in the table + triggerProxyDataReload(); } catch (err: unknown) { console.error("Failed to update proxy settings:", err); setError(`Failed to update proxy settings: ${JSON.stringify(err)}`); } }, - [currentProfileForProxy, loadProfiles], + [currentProfileForProxy, loadProfiles, triggerProxyDataReload], ); const handleCreateProfile = useCallback( @@ -343,30 +355,25 @@ export default function Home() { browserStr: BrowserTypeString; version: string; releaseType: string; - proxy?: ProxySettings; + proxyId?: string; }) => { setError(null); try { - const profile = await invoke( + const _profile = await invoke( "create_browser_profile_new", { name: profileData.name, browserStr: profileData.browserStr, version: profileData.version, releaseType: profileData.releaseType, + proxyId: profileData.proxyId, }, ); - // Update proxy if provided - if (profileData.proxy) { - await invoke("update_profile_proxy", { - profileName: profile.name, - proxy: profileData.proxy, - }); - } - await loadProfiles(); + // Trigger proxy data reload in the table + triggerProxyDataReload(); } catch (error) { setError( `Failed to create profile: ${ @@ -376,7 +383,7 @@ export default function Home() { throw error; } }, - [loadProfiles], + [loadProfiles, triggerProxyDataReload], ); const [runningProfiles, setRunningProfiles] = useState>( @@ -607,6 +614,14 @@ export default function Home() { Settings + { + setProxyManagementDialogOpen(true); + }} + > + + Proxies + { setImportProfileDialogOpen(true); @@ -645,6 +660,9 @@ export default function Home() { onChangeVersion={openChangeVersionDialog} runningProfiles={runningProfiles} isUpdating={isUpdating} + onReloadProxyData={ + proxyDataReloadTrigger > 0 ? triggerProxyDataReload : undefined + } /> @@ -655,8 +673,8 @@ export default function Home() { onClose={() => { setProxyDialogOpen(false); }} - onSave={(proxy: ProxySettings) => void handleSaveProxy(proxy)} - initialSettings={currentProfileForProxy?.proxy} + onSave={handleSaveProxy} + initialProxyId={currentProfileForProxy?.proxy_id} browserType={currentProfileForProxy?.browser} /> @@ -692,6 +710,13 @@ export default function Home() { onImportComplete={() => void loadProfiles()} /> + { + setProxyManagementDialogOpen(false); + }} + /> + {pendingUrls.map((pendingUrl) => ( Promise; } @@ -80,13 +77,11 @@ export function CreateProfileDialog({ ); const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false); - // Proxy settings - const [proxyEnabled, setProxyEnabled] = useState(false); - const [proxyType, setProxyType] = useState("http"); - const [proxyHost, setProxyHost] = useState(""); - const [proxyPort, setProxyPort] = useState(8080); - const [proxyUsername, setProxyUsername] = useState(""); - const [proxyPassword, setProxyPassword] = useState(""); + // Proxy settings - now using stored proxy selection + const [selectedProxyId, setSelectedProxyId] = useState(null); + const [storedProxies, setStoredProxies] = useState([]); + const [isLoadingProxies, setIsLoadingProxies] = useState(false); + const [showProxyForm, setShowProxyForm] = useState(false); const { downloadBrowser, @@ -136,6 +131,19 @@ export function CreateProfileDialog({ } }, []); + const loadStoredProxies = useCallback(async () => { + try { + setIsLoadingProxies(true); + const proxies = await invoke("get_stored_proxies"); + setStoredProxies(proxies); + } catch (error) { + console.error("Failed to load stored proxies:", error); + toast.error("Failed to load available proxies"); + } finally { + setIsLoadingProxies(false); + } + }, []); + const loadReleaseTypes = useCallback(async (browser: string) => { try { setIsLoadingReleaseTypes(true); @@ -191,12 +199,37 @@ export function CreateProfileDialog({ // Helper to determine if proxy should be disabled for the selected browser const isProxyDisabled = selectedBrowser === "tor-browser"; - // Update proxy enabled state when browser changes to tor-browser + // Update proxy selection when browser changes to tor-browser useEffect(() => { - if (selectedBrowser === "tor-browser" && proxyEnabled) { - setProxyEnabled(false); + if (selectedBrowser === "tor-browser" && selectedProxyId) { + setSelectedProxyId(null); } - }, [selectedBrowser, proxyEnabled]); + }, [selectedBrowser, selectedProxyId]); + + const handleCreateProxy = useCallback(() => { + setShowProxyForm(true); + }, []); + + const handleProxySaved = useCallback((savedProxy: StoredProxy) => { + setStoredProxies((prev) => { + const existingIndex = prev.findIndex((p) => p.id === savedProxy.id); + if (existingIndex >= 0) { + // Update existing proxy + const updated = [...prev]; + updated[existingIndex] = savedProxy; + return updated; + } else { + // Add new proxy + return [...prev, savedProxy]; + } + }); + setSelectedProxyId(savedProxy.id); + setShowProxyForm(false); + }, []); + + const handleProxyFormClose = useCallback(() => { + setShowProxyForm(false); + }, []); const handleCreate = useCallback(async () => { if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return; @@ -219,34 +252,18 @@ export function CreateProfileDialog({ setIsCreating(true); try { - const proxy = - proxyEnabled && !isProxyDisabled - ? { - enabled: true, - proxy_type: proxyType, - host: proxyHost, - port: proxyPort, - username: proxyUsername || undefined, - password: proxyPassword || undefined, - } - : undefined; - await onCreateProfile({ name: profileName.trim(), browserStr: selectedBrowser, version, releaseType: selectedReleaseType, - proxy, + proxyId: isProxyDisabled ? undefined : (selectedProxyId ?? undefined), }); // Reset form setProfileName(""); setSelectedReleaseType(null); - setProxyEnabled(false); - setProxyHost(""); - setProxyPort(8080); - setProxyUsername(""); - setProxyPassword(""); + setSelectedProxyId(null); onClose(); } catch (error) { console.error("Failed to create profile:", error); @@ -258,14 +275,9 @@ export function CreateProfileDialog({ selectedBrowser, selectedReleaseType, onCreateProfile, - proxyEnabled, isProxyDisabled, + selectedProxyId, onClose, - proxyHost, - proxyPassword, - proxyPort, - proxyType, - proxyUsername, releaseTypes.nightly, releaseTypes.stable, validateProfileName, @@ -286,14 +298,14 @@ export function CreateProfileDialog({ selectedReleaseType && selectedVersion && isVersionDownloaded(selectedVersion) && - (!proxyEnabled || isProxyDisabled || (proxyHost && proxyPort)) && !nameError; useEffect(() => { if (isOpen) { void loadExistingProfiles(); + void loadStoredProxies(); } - }, [isOpen, loadExistingProfiles]); + }, [isOpen, loadExistingProfiles, loadStoredProxies]); useEffect(() => { if (isOpen && selectedBrowser) { @@ -305,260 +317,239 @@ export function CreateProfileDialog({ }, [isOpen, selectedBrowser, loadDownloadedVersions, loadReleaseTypes]); return ( - - - - Create New Profile - + <> + + + + Create New Profile + -
- {/* Profile Name */} -
- - { - setProfileName(e.target.value); - }} - placeholder="Enter profile name" - className={nameError ? "border-red-500" : ""} - /> - {nameError &&

{nameError}

} -
- - {/* Browser Selection */} -
- - -
- - {selectedBrowser ? ( +
+ {/* Profile Name */}
- - {isLoadingReleaseTypes ? ( -
- Loading release types... -
- ) : Object.keys(releaseTypes).length === 0 ? ( - - - No releases are available for{" "} - {getBrowserDisplayName(selectedBrowser)}. - - - ) : ( -
- {(!releaseTypes.stable || !releaseTypes.nightly) && ( - - - Only {(releaseTypes.stable && "Stable") ?? "Nightly"}{" "} - releases are available for{" "} - {getBrowserDisplayName(selectedBrowser)}. - - + + { + setProfileName(e.target.value); + }} + placeholder="Enter profile name" + className={nameError ? "border-red-500" : ""} + /> + {nameError &&

{nameError}

} +
+ + {/* Browser Selection */} +
+ + +
+ + {selectedBrowser ? ( +
+ + {isLoadingReleaseTypes ? ( +
+ Loading release types... +
+ ) : Object.keys(releaseTypes).length === 0 ? ( + + + No releases are available for{" "} + {getBrowserDisplayName(selectedBrowser)}. + + + ) : ( +
+ {(!releaseTypes.stable || !releaseTypes.nightly) && ( + + + Only {(releaseTypes.stable && "Stable") ?? "Nightly"}{" "} + releases are available for{" "} + {getBrowserDisplayName(selectedBrowser)}. + + + )} + + { + void handleDownload(); + }} + placeholder="Select release type..." + downloadedVersions={downloadedVersions} + /> +
+ )} +
+ ) : null} + + {/* Proxy Settings */} +
+
+
+ + {!isProxyDisabled && ( + + + + + +

Create a new proxy configuration

+
+
)} - - { - void handleDownload(); - }} - placeholder="Select release type..." - downloadedVersions={downloadedVersions} - />
- )} -
- ) : null} - {/* Proxy Settings */} -
-
- {isProxyDisabled ? ( - - -
- - -
-
- -

- Tor Browser has its own built-in proxy system and - doesn't support additional proxy configuration -

-
-
- ) : ( - <> - { - setProxyEnabled(checked as boolean); + {isProxyDisabled ? ( + + +
+

+ Tor Browser has its own built-in proxy system and + doesn't support additional proxy configuration. +

+
+
+ +

+ Tor Browser manages its own proxy routing automatically +

+
+
+ ) : ( + + disabled={isLoadingProxies} + > - + - {["http", "https", "socks4", "socks5"].map((type) => ( - - {type.toUpperCase()} + No Proxy + {storedProxies.map((proxy) => ( + + {proxy.name} ))} -
+ )} -
- - { - setProxyHost(e.target.value); - }} - placeholder="e.g. 127.0.0.1" - /> -
- -
- - { - setProxyPort(Number.parseInt(e.target.value, 10) || 0); - }} - placeholder="e.g. 8080" - min="1" - max="65535" - /> -
- -
- - { - setProxyUsername(e.target.value); - }} - placeholder="Proxy username" - /> -
- -
- - { - setProxyPassword(e.target.value); - }} - placeholder="Proxy password" - /> -
- - )} + {!isProxyDisabled && + storedProxies.length === 0 && + !isLoadingProxies && ( +

+ No saved proxies available. Use the "Create Proxy" button + above to create proxy configurations. +

+ )} +
+
-
- - - void handleCreate()} - disabled={!canCreate} - > - Create Profile - - - -
+ + + void handleCreate()} + disabled={!canCreate} + > + Create Profile + + +
+
+ + + ); } diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 9dd684d..1ae63f3 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -8,6 +8,7 @@ import { type SortingState, useReactTable, } from "@tanstack/react-table"; +import { invoke } from "@tauri-apps/api/core"; import * as React from "react"; import { CiCircleCheck } from "react-icons/ci"; import { IoEllipsisHorizontal } from "react-icons/io5"; @@ -44,7 +45,7 @@ import { } from "@/components/ui/tooltip"; import { useTableSorting } from "@/hooks/use-table-sorting"; import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; -import type { BrowserProfile } from "@/types"; +import type { BrowserProfile, StoredProxy } from "@/types"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; @@ -58,6 +59,7 @@ interface ProfilesDataTableProps { onChangeVersion: (profile: BrowserProfile) => void; runningProfiles: Set; isUpdating?: (browser: string) => boolean; + onReloadProxyData?: () => void | Promise; } export function ProfilesDataTable({ @@ -70,6 +72,7 @@ export function ProfilesDataTable({ onChangeVersion, runningProfiles, isUpdating = () => false, + onReloadProxyData, }: ProfilesDataTableProps) { const { getTableSorting, updateSorting, isLoaded } = useTableSorting(); const [sorting, setSorting] = React.useState([]); @@ -83,12 +86,65 @@ export function ProfilesDataTable({ React.useState(""); const [deleteError, setDeleteError] = React.useState(null); const [isClient, setIsClient] = React.useState(false); + const [storedProxies, setStoredProxies] = React.useState([]); + + // Helper function to check if a profile has a proxy + const hasProxy = React.useCallback( + (profile: BrowserProfile): boolean => { + if (!profile.proxy_id) return false; + const proxy = storedProxies.find((p) => p.id === profile.proxy_id); + return proxy !== undefined; + }, + [storedProxies], + ); + + // Helper function to get proxy info for a profile + const getProxyInfo = React.useCallback( + (profile: BrowserProfile): StoredProxy | null => { + if (!profile.proxy_id) return null; + return storedProxies.find((p) => p.id === profile.proxy_id) ?? null; + }, + [storedProxies], + ); + + // Helper function to get proxy name for display + const getProxyDisplayName = React.useCallback( + (profile: BrowserProfile): string => { + if (!profile.proxy_id) return "Disabled"; + const proxy = storedProxies.find((p) => p.id === profile.proxy_id); + return proxy?.name ?? "Unknown Proxy"; + }, + [storedProxies], + ); // Ensure we're on the client side to prevent hydration mismatches React.useEffect(() => { setIsClient(true); }, []); + // Load stored proxies + const loadStoredProxies = React.useCallback(async () => { + try { + const proxiesList = await invoke("get_stored_proxies"); + setStoredProxies(proxiesList); + } catch (error) { + console.error("Failed to load stored proxies:", error); + } + }, []); + + React.useEffect(() => { + if (isClient) { + void loadStoredProxies(); + } + }, [isClient, loadStoredProxies]); + + // Reload proxy data when requested from parent + React.useEffect(() => { + if (onReloadProxyData) { + void loadStoredProxies(); + } + }, [onReloadProxyData, loadStoredProxies]); + // Update local sorting state when settings are loaded React.useEffect(() => { if (isLoaded && isClient) { @@ -320,32 +376,41 @@ export function ProfilesDataTable({ header: "Proxy", cell: ({ row }) => { const profile = row.original; - const hasProxy = profile.proxy?.enabled; - const regularText = hasProxy ? profile.proxy?.proxy_type : "Disabled"; - const regularTooltipText = hasProxy - ? `${profile.proxy?.proxy_type.toUpperCase()} proxy enabled (${ - profile.proxy?.host - }:${profile.proxy?.port})` - : "No proxy configured"; + const profileHasProxy = hasProxy(profile); + const proxyDisplayName = getProxyDisplayName(profile); + const proxyInfo = getProxyInfo(profile); + + const tooltipText = + profile.browser === "tor-browser" + ? "Proxies are not supported for TOR browser" + : profileHasProxy && proxyInfo + ? `${proxyDisplayName}, ${proxyInfo.proxy_settings.proxy_type.toUpperCase()} (${ + proxyInfo.proxy_settings.host + }:${proxyInfo.proxy_settings.port})` + : "No proxy configured"; + return (
- {hasProxy && ( + {profileHasProxy && ( )} - - {profile.browser === "tor-browser" - ? "Not supported" - : regularText} - + + {proxyDisplayName.length > 10 ? ( + + {proxyDisplayName.slice(0, 10)}... + + ) : ( + + {profile.browser === "tor-browser" + ? "Not supported" + : proxyDisplayName} + + )}
- - {profile.browser === "tor-browser" - ? "Proxies are not supported for TOR browser" - : regularTooltipText} - + {tooltipText}
); }, @@ -426,6 +491,9 @@ export function ProfilesDataTable({ onKillProfile, onProxySettings, onChangeVersion, + getProxyInfo, + hasProxy, + getProxyDisplayName, ], ); diff --git a/src/components/profile-selector-dialog.tsx b/src/components/profile-selector-dialog.tsx index 6c4b2ae..1e9fb82 100644 --- a/src/components/profile-selector-dialog.tsx +++ b/src/components/profile-selector-dialog.tsx @@ -28,7 +28,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; -import type { BrowserProfile } from "@/types"; +import type { BrowserProfile, StoredProxy } from "@/types"; interface ProfileSelectorDialogProps { isOpen: boolean; @@ -47,6 +47,17 @@ export function ProfileSelectorDialog({ const [selectedProfile, setSelectedProfile] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isLaunching, setIsLaunching] = useState(false); + const [storedProxies, setStoredProxies] = useState([]); + + // Helper function to check if a profile has a proxy + const hasProxy = useCallback( + (profile: BrowserProfile): boolean => { + if (!profile.proxy_id) return false; + const proxy = storedProxies.find((p) => p.id === profile.proxy_id); + return proxy !== undefined; + }, + [storedProxies], + ); // Helper function to determine if a profile can be used for opening links const canUseProfileForLinks = useCallback( @@ -86,15 +97,18 @@ export function ProfileSelectorDialog({ const loadProfiles = useCallback(async () => { setIsLoading(true); try { - const profileList = await invoke( - "list_browser_profiles", - ); + // Load both profiles and stored proxies + const [profileList, proxiesList] = await Promise.all([ + invoke("list_browser_profiles"), + invoke("get_stored_proxies"), + ]); // Sort profiles by name profileList.sort((a, b) => a.name.localeCompare(b.name)); - // Don't filter any profiles, show all of them + // Set both profiles and proxies setProfiles(profileList); + setStoredProxies(proxiesList); // Auto-select first available profile for link opening if (profileList.length > 0) { @@ -305,7 +319,7 @@ export function ProfileSelectorDialog({ {getBrowserDisplayName(profile.browser)} - {profile.proxy?.enabled && ( + {hasProxy(profile) && ( Proxy diff --git a/src/components/proxy-form-dialog.tsx b/src/components/proxy-form-dialog.tsx new file mode 100644 index 0000000..fbb5e33 --- /dev/null +++ b/src/components/proxy-form-dialog.tsx @@ -0,0 +1,285 @@ +"use client"; + +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; +import { toast } from "sonner"; +import { LoadingButton } from "@/components/loading-button"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { StoredProxy } from "@/types"; + +interface ProxyFormData { + name: string; + proxy_type: string; + host: string; + port: number; + username: string; + password: string; +} + +interface ProxyFormDialogProps { + isOpen: boolean; + onClose: () => void; + onSave: (proxy: StoredProxy) => void; + editingProxy?: StoredProxy | null; +} + +export function ProxyFormDialog({ + isOpen, + onClose, + onSave, + editingProxy, +}: ProxyFormDialogProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: "", + proxy_type: "http", + host: "", + port: 8080, + username: "", + password: "", + }); + + const resetForm = useCallback(() => { + setFormData({ + name: "", + proxy_type: "http", + host: "", + port: 8080, + username: "", + password: "", + }); + }, []); + + // Load editing proxy data when dialog opens + useEffect(() => { + if (isOpen) { + if (editingProxy) { + setFormData({ + name: editingProxy.name, + proxy_type: editingProxy.proxy_settings.proxy_type, + host: editingProxy.proxy_settings.host, + port: editingProxy.proxy_settings.port, + username: editingProxy.proxy_settings.username || "", + password: editingProxy.proxy_settings.password || "", + }); + } else { + resetForm(); + } + } + }, [isOpen, editingProxy, resetForm]); + + const handleSubmit = useCallback(async () => { + if (!formData.name.trim()) { + toast.error("Proxy name is required"); + return; + } + + if (!formData.host.trim() || !formData.port) { + toast.error("Host and port are required"); + return; + } + + setIsSubmitting(true); + try { + const proxySettings = { + proxy_type: formData.proxy_type, + host: formData.host.trim(), + port: formData.port, + username: formData.username.trim() || undefined, + password: formData.password.trim() || undefined, + }; + + let savedProxy: StoredProxy; + + if (editingProxy) { + // Update existing proxy + savedProxy = await invoke("update_stored_proxy", { + proxyId: editingProxy.id, + name: formData.name.trim(), + proxySettings, + }); + toast.success("Proxy updated successfully"); + } else { + // Create new proxy + savedProxy = await invoke("create_stored_proxy", { + name: formData.name.trim(), + proxySettings, + }); + toast.success("Proxy created successfully"); + } + + onSave(savedProxy); + onClose(); + } catch (error) { + console.error("Failed to save proxy:", error); + const errorMessage = + error instanceof Error ? error.message : String(error); + toast.error(`Failed to save proxy: ${errorMessage}`); + } finally { + setIsSubmitting(false); + } + }, [formData, editingProxy, onSave, onClose]); + + const handleClose = useCallback(() => { + if (!isSubmitting) { + onClose(); + } + }, [isSubmitting, onClose]); + + const isFormValid = + formData.name.trim() && + formData.host.trim() && + formData.port > 0 && + formData.port <= 65535; + + return ( + + + + + {editingProxy ? "Edit Proxy" : "Create New Proxy"} + + + +
+
+ + + setFormData({ ...formData, name: e.target.value }) + } + placeholder="e.g. Office Proxy, Home VPN, etc." + disabled={isSubmitting} + /> +
+ +
+ + +
+ +
+
+ + + setFormData({ ...formData, host: e.target.value }) + } + placeholder="e.g. 127.0.0.1" + disabled={isSubmitting} + /> +
+ +
+ + + setFormData({ + ...formData, + port: parseInt(e.target.value, 10) || 0, + }) + } + placeholder="e.g. 8080" + min="1" + max="65535" + disabled={isSubmitting} + /> +
+
+ +
+
+ + + setFormData({ + ...formData, + username: e.target.value, + }) + } + placeholder="Proxy username" + disabled={isSubmitting} + /> +
+ +
+ + + setFormData({ + ...formData, + password: e.target.value, + }) + } + placeholder="Proxy password" + disabled={isSubmitting} + /> +
+
+
+ + + + + {editingProxy ? "Update Proxy" : "Create Proxy"} + + +
+
+ ); +} diff --git a/src/components/proxy-management-dialog.tsx b/src/components/proxy-management-dialog.tsx new file mode 100644 index 0000000..35289fe --- /dev/null +++ b/src/components/proxy-management-dialog.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; +import { FiEdit2, FiPlus, FiTrash2, FiWifi } from "react-icons/fi"; +import { toast } from "sonner"; +import { ProxyFormDialog } from "@/components/proxy-form-dialog"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import type { StoredProxy } from "@/types"; + +interface ProxyManagementDialogProps { + isOpen: boolean; + onClose: () => void; +} + +export function ProxyManagementDialog({ + isOpen, + onClose, +}: ProxyManagementDialogProps) { + const [storedProxies, setStoredProxies] = useState([]); + const [loading, setLoading] = useState(false); + const [showProxyForm, setShowProxyForm] = useState(false); + const [editingProxy, setEditingProxy] = useState(null); + + const loadStoredProxies = useCallback(async () => { + try { + setLoading(true); + const proxies = await invoke("get_stored_proxies"); + setStoredProxies(proxies); + } catch (error) { + console.error("Failed to load stored proxies:", error); + toast.error("Failed to load proxies"); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + if (isOpen) { + loadStoredProxies(); + } + }, [isOpen, loadStoredProxies]); + + const handleDeleteProxy = useCallback(async (proxy: StoredProxy) => { + if ( + !confirm(`Are you sure you want to delete the proxy "${proxy.name}"?`) + ) { + return; + } + + try { + await invoke("delete_stored_proxy", { proxyId: proxy.id }); + setStoredProxies((prev) => prev.filter((p) => p.id !== proxy.id)); + toast.success("Proxy deleted successfully"); + } catch (error) { + console.error("Failed to delete proxy:", error); + toast.error("Failed to delete proxy"); + } + }, []); + + const handleCreateProxy = useCallback(() => { + setEditingProxy(null); + setShowProxyForm(true); + }, []); + + const handleEditProxy = useCallback((proxy: StoredProxy) => { + setEditingProxy(proxy); + setShowProxyForm(true); + }, []); + + const handleProxySaved = useCallback((savedProxy: StoredProxy) => { + setStoredProxies((prev) => { + const existingIndex = prev.findIndex((p) => p.id === savedProxy.id); + if (existingIndex >= 0) { + // Update existing proxy + const updated = [...prev]; + updated[existingIndex] = savedProxy; + return updated; + } else { + // Add new proxy + return [...prev, savedProxy]; + } + }); + setShowProxyForm(false); + setEditingProxy(null); + }, []); + + const handleProxyFormClose = useCallback(() => { + setShowProxyForm(false); + setEditingProxy(null); + }, []); + + const trimName = useCallback((name: string) => { + return name.length > 30 ? `${name.substring(0, 30)}...` : name; + }, []); + + return ( + <> + + + +
+ + Proxy Management +
+
+ +
+ {/* Header with Create Button */} +
+
+

Stored Proxies

+

+ Manage your saved proxy configurations for reuse across + profiles +

+
+ +
+ + {/* Proxy List - Scrollable */} +
+ {loading ? ( +
+

+ Loading proxies... +

+
+ ) : storedProxies.length === 0 ? ( +
+ +

+ No proxies configured +

+

+ Create your first proxy configuration to get started +

+ +
+ ) : ( +
+ {storedProxies.map((proxy) => ( +
+
+ {proxy.name.length > 30 ? ( + + + + {trimName(proxy.name)} + + + + + {proxy.name} + + + + ) : ( + + {proxy.name} + + )} +
+
+ + + + + +

Edit proxy

+
+
+ + + + + +

Delete proxy

+
+
+
+
+ ))} +
+ )} +
+
+ + + + +
+
+ + + + ); +} diff --git a/src/components/proxy-settings-dialog.tsx b/src/components/proxy-settings-dialog.tsx index a495723..45d25a3 100644 --- a/src/components/proxy-settings-dialog.tsx +++ b/src/components/proxy-settings-dialog.tsx @@ -1,8 +1,13 @@ "use client"; -import { useEffect, useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; +import { FiPlus } from "react-icons/fi"; +import { toast } from "sonner"; +import { ProxyFormDialog } from "@/components/proxy-form-dialog"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; +import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, @@ -10,35 +15,20 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; - -interface ProxySettings { - enabled: boolean; - proxy_type: string; - host: string; - port: number; - username?: string; - password?: string; -} +import { cn } from "@/lib/utils"; +import type { StoredProxy } from "@/types"; interface ProxySettingsDialogProps { isOpen: boolean; onClose: () => void; - onSave: (proxySettings: ProxySettings) => void; - initialSettings?: ProxySettings; + onSave: (proxyId: string | null) => void; + initialProxyId?: string | null; browserType?: string; } @@ -46,232 +36,245 @@ export function ProxySettingsDialog({ isOpen, onClose, onSave, - initialSettings, + initialProxyId, browserType, }: ProxySettingsDialogProps) { - const [settings, setSettings] = useState({ - enabled: initialSettings?.enabled ?? false, - proxy_type: initialSettings?.proxy_type ?? "http", - host: initialSettings?.host ?? "", - port: initialSettings?.port ?? 8080, - username: initialSettings?.username ?? "", - password: initialSettings?.password ?? "", - }); - - const [initialSettingsState, setInitialSettingsState] = - useState({ - enabled: false, - proxy_type: "http", - host: "", - port: 8080, - username: "", - password: "", - }); - - useEffect(() => { - if (isOpen && initialSettings) { - const newSettings = { - enabled: initialSettings.enabled, - proxy_type: initialSettings.proxy_type, - host: initialSettings.host, - port: initialSettings.port, - username: initialSettings.username ?? "", - password: initialSettings.password ?? "", - }; - setSettings(newSettings); - setInitialSettingsState(newSettings); - } else if (isOpen) { - const defaultSettings = { - enabled: false, - proxy_type: "http", - host: "", - port: 80, - username: "", - password: "", - }; - setSettings(defaultSettings); - setInitialSettingsState(defaultSettings); - } - }, [isOpen, initialSettings]); - - const handleSubmit = () => { - onSave(settings); - }; - - // Check if settings have changed - const hasChanged = () => { - return ( - settings.enabled !== initialSettingsState.enabled || - settings.proxy_type !== initialSettingsState.proxy_type || - settings.host !== initialSettingsState.host || - settings.port !== initialSettingsState.port || - settings.username !== initialSettingsState.username || - settings.password !== initialSettingsState.password - ); - }; + const [storedProxies, setStoredProxies] = useState([]); + const [selectedProxyId, setSelectedProxyId] = useState( + initialProxyId || null, + ); + const [loading, setLoading] = useState(false); + const [showProxyForm, setShowProxyForm] = useState(false); // Helper to determine if proxy should be disabled for the selected browser const isProxyDisabled = browserType === "tor-browser"; - // Update proxy enabled state when browser is tor-browser - useEffect(() => { - if (browserType === "tor-browser" && settings.enabled) { - setSettings((prev) => ({ ...prev, enabled: false })); + const loadStoredProxies = useCallback(async () => { + try { + setLoading(true); + const proxies = await invoke("get_stored_proxies"); + setStoredProxies(proxies); + } catch (error) { + console.error("Failed to load stored proxies:", error); + toast.error("Failed to load proxies"); + } finally { + setLoading(false); } - }, [browserType, settings.enabled]); + }, []); + + useEffect(() => { + if (isOpen) { + loadStoredProxies(); + if (isProxyDisabled) { + setSelectedProxyId(null); + } + } + }, [isOpen, isProxyDisabled, loadStoredProxies]); + + const handleCreateProxy = useCallback(() => { + setShowProxyForm(true); + }, []); + + const handleProxySaved = useCallback((savedProxy: StoredProxy) => { + setStoredProxies((prev) => { + const existingIndex = prev.findIndex((p) => p.id === savedProxy.id); + if (existingIndex >= 0) { + // Update existing proxy + const updated = [...prev]; + updated[existingIndex] = savedProxy; + return updated; + } else { + // Add new proxy + return [...prev, savedProxy]; + } + }); + setSelectedProxyId(savedProxy.id); + setShowProxyForm(false); + }, []); + + const handleProxyFormClose = useCallback(() => { + setShowProxyForm(false); + }, []); + + const handleSave = () => { + onSave(selectedProxyId); + }; + + const hasChanged = () => { + return selectedProxyId !== initialProxyId; + }; return ( - { - if (!open) { - onClose(); - } - }} - > - - - Proxy Settings - + <> + { + if (!open) { + onClose(); + } + }} + > + + + Proxy Settings + -
-
- {isProxyDisabled ? ( - - -
- - -
-
- -

- Tor Browser has its own built-in proxy system and - doesn't support additional proxy configuration -

-
-
- ) : ( +
+ {isProxyDisabled && ( +
+

+ Tor Browser has its own built-in proxy system and doesn't + support additional proxy configuration. +

+
+ )} + + {!isProxyDisabled && ( <> - { - setSettings({ ...settings, enabled: checked as boolean }); - }} - /> - + {/* Proxy Selection */} +
+
+ + + + + + +

Create a new proxy configuration

+
+
+
+ +
+ + + {loading ? ( +

+ Loading proxies... +

+ ) : ( + storedProxies.map((proxy) => ( + + )) + )} + + {!loading && storedProxies.length === 0 && ( +
+

+ No saved proxies available. +

+ +
+ )} +
+
)}
- {settings.enabled && !isProxyDisabled && ( - <> -
- - -
+ + + + + +
-
- - { - setSettings({ ...settings, host: e.target.value }); - }} - placeholder="e.g. 127.0.0.1" - /> -
- -
- - { - setSettings({ - ...settings, - port: Number.parseInt(e.target.value, 10) || 0, - }); - }} - placeholder="e.g. 8080" - min="1" - max="65535" - /> -
- -
- - { - setSettings({ ...settings, username: e.target.value }); - }} - placeholder="Proxy username" - /> -
- -
- - { - setSettings({ ...settings, password: e.target.value }); - }} - placeholder="Proxy password" - /> -
- - )} - - - - - - -
-
+ + ); } diff --git a/src/types.ts b/src/types.ts index c1b6ef0..723285e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,4 @@ export interface ProxySettings { - enabled: boolean; proxy_type: string; // "http", "https", "socks4", or "socks5" host: string; port: number; @@ -13,16 +12,22 @@ export interface TableSortingSettings { } export interface BrowserProfile { + id: string; // UUID of the profile name: string; browser: string; version: string; - profile_path: string; - proxy?: ProxySettings; + proxy_id?: string; // Reference to stored proxy process_id?: number; last_launch?: number; release_type: string; // "stable" or "nightly" } +export interface StoredProxy { + id: string; + name: string; + proxy_settings: ProxySettings; +} + export interface DetectedProfile { browser: string; name: string;