From 3732d3a6e1404e84c541dc639b4aae8eb96f8823 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:17:25 +0400 Subject: [PATCH] feat: ephemeral profiles --- src-tauri/src/api_server.rs | 1 + src-tauri/src/auto_updater.rs | 1 + src-tauri/src/browser_runner.rs | 40 +++++- src-tauri/src/camoufox_manager.rs | 27 +++- src-tauri/src/ephemeral_dirs.rs | 159 +++++++++++++++++++++++ src-tauri/src/lib.rs | 4 + src-tauri/src/profile/manager.rs | 48 +++++-- src-tauri/src/profile/types.rs | 2 + src-tauri/src/profile_importer.rs | 1 + src-tauri/src/wayfern_manager.rs | 8 ++ src/app/page.tsx | 2 + src/components/create-profile-dialog.tsx | 28 ++++ src/components/profile-data-table.tsx | 30 +++-- src/i18n/locales/en.json | 5 +- src/i18n/locales/es.json | 5 +- src/i18n/locales/fr.json | 5 +- src/i18n/locales/ja.json | 5 +- src/i18n/locales/pt.json | 5 +- src/i18n/locales/ru.json | 5 +- src/i18n/locales/zh.json | 5 +- src/types.ts | 1 + 21 files changed, 354 insertions(+), 33 deletions(-) create mode 100644 src-tauri/src/ephemeral_dirs.rs diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index ca32b14..038af91 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -605,6 +605,7 @@ async fn create_profile( camoufox_config, wayfern_config, request.group_id.clone(), + false, ) .await { diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index ca8ac23..97f0d30 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -522,6 +522,7 @@ mod tests { sync_enabled: false, last_sync: None, host_os: None, + ephemeral: false, } } diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 0434e52..c1ff3cb 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -229,6 +229,15 @@ impl BrowserRunner { log::warn!("Failed to configure Camoufox search engine: {e}"); } + // Create ephemeral dir for ephemeral profiles + let override_profile_path = if profile.ephemeral { + let dir = crate::ephemeral_dirs::create_ephemeral_dir(&profile.id.to_string()) + .map_err(|e| -> Box { e.into() })?; + Some(dir) + } else { + None + }; + // Launch Camoufox browser log::info!("Launching Camoufox for profile: {}", profile.name); let camoufox_result = self @@ -238,6 +247,7 @@ impl BrowserRunner { updated_profile.clone(), camoufox_config, url, + override_profile_path, ) .await .map_err(|e| -> Box { @@ -441,12 +451,19 @@ impl BrowserRunner { ); } + // Create ephemeral dir for ephemeral profiles + if profile.ephemeral { + crate::ephemeral_dirs::create_ephemeral_dir(&profile.id.to_string()) + .map_err(|e| -> Box { e.into() })?; + } + // Launch Wayfern browser log::info!("Launching Wayfern for profile: {}", profile.name); // Get profile path for Wayfern let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = updated_profile.get_profile_data_path(&profiles_dir); + let profile_data_path = + crate::ephemeral_dirs::get_effective_profile_path(&updated_profile, &profiles_dir); let profile_path_str = profile_data_path.to_string_lossy().to_string(); // Get proxy URL from config @@ -461,6 +478,7 @@ impl BrowserRunner { &wayfern_config, url.as_deref(), proxy_url, + profile.ephemeral, ) .await .map_err(|e| -> Box { @@ -793,7 +811,8 @@ impl BrowserRunner { if profile.browser == "camoufox" { // Get the profile path based on the UUID let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_data_path = + crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); // Check if the process is running @@ -847,7 +866,8 @@ impl BrowserRunner { // Handle Wayfern profiles using WayfernManager if profile.browser == "wayfern" { let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_data_path = + crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); // Check if the process is running @@ -1245,7 +1265,8 @@ impl BrowserRunner { if profile.browser == "camoufox" { // Search by profile path to find the running Camoufox instance let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_data_path = + crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); log::info!( @@ -1663,6 +1684,10 @@ impl BrowserRunner { ); } + if profile.ephemeral { + crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); + } + log::info!( "Camoufox process cleanup completed for profile: {} (ID: {})", profile.name, @@ -1688,7 +1713,8 @@ impl BrowserRunner { // Handle Wayfern profiles using WayfernManager if profile.browser == "wayfern" { let profiles_dir = self.profile_manager.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_data_path = + crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); log::info!( @@ -1981,6 +2007,10 @@ impl BrowserRunner { ); } + if profile.ephemeral { + crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); + } + log::info!( "Wayfern process cleanup completed for profile: {} (ID: {})", profile.name, diff --git a/src-tauri/src/camoufox_manager.rs b/src-tauri/src/camoufox_manager.rs index ba9d35c..823f581 100644 --- a/src-tauri/src/camoufox_manager.rs +++ b/src-tauri/src/camoufox_manager.rs @@ -582,10 +582,15 @@ impl CamoufoxManager { profile: BrowserProfile, config: CamoufoxConfig, url: Option, + override_profile_path: Option, ) -> Result { // Get profile path - let profiles_dir = self.get_profiles_dir(); - let profile_path = profile.get_profile_data_path(&profiles_dir); + let profile_path = if let Some(ref override_path) = override_profile_path { + override_path.clone() + } else { + let profiles_dir = self.get_profiles_dir(); + profile.get_profile_data_path(&profiles_dir) + }; let profile_path_str = profile_path.to_string_lossy(); // Check if there's already a running instance for this profile @@ -597,6 +602,24 @@ impl CamoufoxManager { // Clean up any dead instances before launching let _ = self.cleanup_dead_instances().await; + // For ephemeral profiles, write Firefox prefs to keep all data inside the profile dir + if override_profile_path.is_some() { + let cache_dir = profile_path.join("cache2"); + let user_js_path = profile_path.join("user.js"); + let prefs = format!( + concat!( + "user_pref(\"browser.cache.disk.parent_directory\", \"{}\");\n", + "user_pref(\"browser.cache.disk.enable\", false);\n", + "user_pref(\"browser.cache.memory.enable\", true);\n", + "user_pref(\"browser.privatebrowsing.autostart\", true);\n", + ), + cache_dir.to_string_lossy().replace('\\', "\\\\"), + ); + if let Err(e) = std::fs::write(&user_js_path, prefs) { + log::warn!("Failed to write ephemeral user.js: {e}"); + } + } + self .launch_camoufox( &app_handle, diff --git a/src-tauri/src/ephemeral_dirs.rs b/src-tauri/src/ephemeral_dirs.rs new file mode 100644 index 0000000..19e5305 --- /dev/null +++ b/src-tauri/src/ephemeral_dirs.rs @@ -0,0 +1,159 @@ +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +use crate::profile::BrowserProfile; + +lazy_static::lazy_static! { + static ref EPHEMERAL_DIRS: Mutex> = Mutex::new(HashMap::new()); +} + +pub fn create_ephemeral_dir(profile_id: &str) -> Result { + let dir_name = format!("donut-ephemeral-{profile_id}"); + let dir_path = std::env::temp_dir().join(dir_name); + + std::fs::create_dir_all(&dir_path).map_err(|e| format!("Failed to create ephemeral dir: {e}"))?; + + EPHEMERAL_DIRS + .lock() + .map_err(|e| format!("Failed to lock ephemeral dirs: {e}"))? + .insert(profile_id.to_string(), dir_path.clone()); + + log::info!( + "Created ephemeral dir for profile {}: {}", + profile_id, + dir_path.display() + ); + + Ok(dir_path) +} + +pub fn get_ephemeral_dir(profile_id: &str) -> Option { + EPHEMERAL_DIRS.lock().ok()?.get(profile_id).cloned() +} + +pub fn remove_ephemeral_dir(profile_id: &str) { + let dir = EPHEMERAL_DIRS + .lock() + .ok() + .and_then(|mut map| map.remove(profile_id)); + + if let Some(dir_path) = dir { + if dir_path.exists() { + if let Err(e) = std::fs::remove_dir_all(&dir_path) { + log::warn!("Failed to remove ephemeral dir {}: {e}", dir_path.display()); + } else { + log::info!( + "Removed ephemeral dir for profile {}: {}", + profile_id, + dir_path.display() + ); + } + } + } +} + +pub fn cleanup_stale_dirs() { + let temp_dir = std::env::temp_dir(); + let entries = match std::fs::read_dir(&temp_dir) { + Ok(entries) => entries, + Err(e) => { + log::warn!("Failed to read temp dir for ephemeral cleanup: {e}"); + return; + } + }; + + for entry in entries.flatten() { + if let Some(name) = entry.file_name().to_str() { + if name.starts_with("donut-ephemeral-") && entry.path().is_dir() { + if let Err(e) = std::fs::remove_dir_all(entry.path()) { + log::warn!( + "Failed to clean up stale ephemeral dir {}: {e}", + entry.path().display() + ); + } else { + log::info!("Cleaned up stale ephemeral dir: {}", entry.path().display()); + } + } + } + } +} + +pub fn get_effective_profile_path(profile: &BrowserProfile, profiles_dir: &Path) -> PathBuf { + if profile.ephemeral { + if let Some(dir) = get_ephemeral_dir(&profile.id.to_string()) { + return dir; + } + } + profile.get_profile_data_path(profiles_dir) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_test_profile(id: uuid::Uuid, ephemeral: bool) -> BrowserProfile { + BrowserProfile { + id, + name: "test".to_string(), + browser: "camoufox".to_string(), + version: "1.0".to_string(), + proxy_id: None, + vpn_id: None, + process_id: None, + last_launch: None, + release_type: "stable".to_string(), + camoufox_config: None, + wayfern_config: None, + group_id: None, + tags: Vec::new(), + note: None, + sync_enabled: false, + last_sync: None, + host_os: None, + ephemeral, + } + } + + #[test] + fn test_ephemeral_dir_lifecycle() { + // Test create, get, effective path, remove, and cleanup all in sequence + // to avoid race conditions between parallel tests. + + // 1. Create and get + let profile_id = uuid::Uuid::new_v4(); + let id_str = profile_id.to_string(); + let dir = create_ephemeral_dir(&id_str).unwrap(); + assert!(dir.is_dir()); + assert_eq!(get_ephemeral_dir(&id_str), Some(dir.clone())); + + // 2. Effective path for ephemeral profile returns ephemeral dir + let ephemeral_profile = make_test_profile(profile_id, true); + let profiles_dir = std::env::temp_dir().join("test_profiles_ephemeral"); + assert_eq!( + get_effective_profile_path(&ephemeral_profile, &profiles_dir), + dir + ); + + // 3. Remove cleans up dir and map entry + remove_ephemeral_dir(&id_str); + assert!(!dir.exists()); + assert!(get_ephemeral_dir(&id_str).is_none()); + + // 4. Effective path for persistent profile returns normal path + let persistent_profile = make_test_profile(uuid::Uuid::new_v4(), false); + let expected = persistent_profile.get_profile_data_path(&profiles_dir); + assert_eq!( + get_effective_profile_path(&persistent_profile, &profiles_dir), + expected + ); + + // 5. Cleanup stale dirs + let stale_id = uuid::Uuid::new_v4().to_string(); + let stale_dir = std::env::temp_dir().join(format!("donut-ephemeral-{stale_id}")); + std::fs::create_dir_all(&stale_dir).unwrap(); + assert!(stale_dir.exists()); + cleanup_stale_dirs(); + assert!(!stale_dir.exists()); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b17446c..22f066f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -21,6 +21,7 @@ mod camoufox_manager; mod default_browser; mod downloaded_browsers_registry; mod downloader; +mod ephemeral_dirs; mod extraction; mod geoip_downloader; mod group_manager; @@ -759,6 +760,9 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_macos_permissions::init()) .setup(|app| { + // Clean up stale ephemeral profile dirs from previous sessions + ephemeral_dirs::cleanup_stale_dirs(); + // Start the daemon for tray icon if let Err(e) = daemon_spawn::ensure_daemon_running() { log::warn!("Failed to start daemon: {e}"); diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index 3245b2d..521e9a4 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -48,6 +48,7 @@ impl ProfileManager { camoufox_config: Option, wayfern_config: Option, group_id: Option, + ephemeral: bool, ) -> Result> { if proxy_id.is_some() && vpn_id.is_some() { return Err("Cannot set both proxy_id and vpn_id".into()); @@ -72,7 +73,9 @@ impl ProfileManager { // Create profile directory with UUID and profile subdirectory create_dir_all(&profile_uuid_dir)?; - create_dir_all(&profile_data_dir)?; + if !ephemeral { + create_dir_all(&profile_data_dir)?; + } // For Camoufox profiles, generate fingerprint during creation let final_camoufox_config = if browser == "camoufox" { @@ -162,6 +165,7 @@ impl ProfileManager { sync_enabled: false, last_sync: None, host_os: None, + ephemeral: false, }; match self @@ -277,6 +281,7 @@ impl ProfileManager { sync_enabled: false, last_sync: None, host_os: None, + ephemeral: false, }; match self @@ -324,6 +329,7 @@ impl ProfileManager { sync_enabled: false, last_sync: None, host_os: Some(get_host_os()), + ephemeral, }; // Save profile info @@ -337,16 +343,19 @@ impl ProfileManager { log::info!("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_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)?; + // Skip for ephemeral profiles since the data dir is created at launch time + if !ephemeral { + 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 { - // Proxy ID provided but not found, disable proxy + // Create user.js with common Firefox preferences but no 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)?; } // Emit profile creation event @@ -842,6 +851,7 @@ impl ProfileManager { sync_enabled: false, last_sync: None, host_os: Some(get_host_os()), + ephemeral: false, }; self.save_profile(&new_profile)?; @@ -1290,7 +1300,8 @@ impl ProfileManager { ) -> Result> { let launcher = self.camoufox_manager; let profiles_dir = self.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_data_path = + crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); // Check if there's a running Camoufox instance for this profile @@ -1334,6 +1345,10 @@ impl ProfileManager { } Ok(None) => { // No running instance found, clear process ID if set and stop proxy + if profile.ephemeral { + crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); + } + let profiles_dir = self.get_profiles_dir(); let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); let metadata_file = profile_uuid_dir.join("metadata.json"); @@ -1364,6 +1379,10 @@ impl ProfileManager { Err(e) => { // Error checking status, assume not running and clear process ID log::warn!("Warning: Failed to check Camoufox status: {e}"); + if profile.ephemeral { + crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); + } + let profiles_dir = self.get_profiles_dir(); let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); let metadata_file = profile_uuid_dir.join("metadata.json"); @@ -1405,7 +1424,8 @@ impl ProfileManager { ) -> Result> { let manager = self.wayfern_manager; let profiles_dir = self.get_profiles_dir(); - let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_data_path = + crate::ephemeral_dirs::get_effective_profile_path(profile, &profiles_dir); let profile_path_str = profile_data_path.to_string_lossy(); // Check if there's a running Wayfern instance for this profile @@ -1449,6 +1469,10 @@ impl ProfileManager { } None => { // No running instance found, clear process ID if set + if profile.ephemeral { + crate::ephemeral_dirs::remove_ephemeral_dir(&profile.id.to_string()); + } + let profiles_dir = self.get_profiles_dir(); let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); let metadata_file = profile_uuid_dir.join("metadata.json"); @@ -1870,6 +1894,7 @@ pub async fn create_browser_profile_with_group( camoufox_config: Option, wayfern_config: Option, group_id: Option, + ephemeral: bool, ) -> Result { let profile_manager = ProfileManager::instance(); profile_manager @@ -1884,6 +1909,7 @@ pub async fn create_browser_profile_with_group( camoufox_config, wayfern_config, group_id, + ephemeral, ) .await .map_err(|e| format!("Failed to create profile: {e}")) @@ -1984,6 +2010,7 @@ pub async fn create_browser_profile_new( camoufox_config: Option, wayfern_config: Option, group_id: Option, + ephemeral: Option, ) -> Result { let fingerprint_os = camoufox_config .as_ref() @@ -2010,6 +2037,7 @@ pub async fn create_browser_profile_new( camoufox_config, wayfern_config, group_id, + ephemeral.unwrap_or(false), ) .await } diff --git a/src-tauri/src/profile/types.rs b/src-tauri/src/profile/types.rs index d3eebd2..a982a58 100644 --- a/src-tauri/src/profile/types.rs +++ b/src-tauri/src/profile/types.rs @@ -45,6 +45,8 @@ pub struct BrowserProfile { pub last_sync: Option, // Timestamp of last successful sync (epoch seconds) #[serde(default)] pub host_os: Option, // OS where profile was created ("macos", "windows", "linux") + #[serde(default)] + pub ephemeral: bool, } pub fn default_release_type() -> String { diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 19c2c88..7bd9098 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -557,6 +557,7 @@ impl ProfileImporter { sync_enabled: false, last_sync: None, host_os: Some(crate::profile::types::get_host_os()), + ephemeral: false, }; // Save the profile metadata diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index 81a7f16..438bad5 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -380,6 +380,7 @@ impl WayfernManager { Ok(fingerprint_json) } + #[allow(clippy::too_many_arguments)] pub async fn launch_wayfern( &self, _app_handle: &AppHandle, @@ -388,6 +389,7 @@ impl WayfernManager { config: &WayfernConfig, url: Option<&str>, proxy_url: Option<&str>, + ephemeral: bool, ) -> Result> { let executable_path = if let Some(path) = &config.executable_path { let p = PathBuf::from(path); @@ -432,6 +434,11 @@ impl WayfernManager { args.push(format!("--proxy-server={proxy}")); } + if ephemeral { + args.push(format!("--disk-cache-dir={}/cache", profile_path)); + args.push("--incognito".to_string()); + } + // Don't add URL to args - we'll navigate via CDP after setting fingerprint // This ensures fingerprint is applied at navigation commit time @@ -713,6 +720,7 @@ impl WayfernManager { config, url, proxy_url, + profile.ephemeral, ) .await } diff --git a/src/app/page.tsx b/src/app/page.tsx index 7687964..fdc7495 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -494,6 +494,7 @@ export default function Home() { camoufoxConfig?: CamoufoxConfig; wayfernConfig?: WayfernConfig; groupId?: string; + ephemeral?: boolean; }) => { try { await invoke("create_browser_profile_new", { @@ -508,6 +509,7 @@ export default function Home() { groupId: profileData.groupId || (selectedGroupId !== "default" ? selectedGroupId : undefined), + ephemeral: profileData.ephemeral, }); // No need to manually reload - useProfileEvents will handle the update diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index 9a5f942..7323a08 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -8,6 +8,7 @@ import { LoadingButton } from "@/components/loading-button"; import { ProxyFormDialog } from "@/components/proxy-form-dialog"; import { SharedCamoufoxConfigForm } from "@/components/shared-camoufox-config-form"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -75,6 +76,7 @@ interface CreateProfileDialogProps { camoufoxConfig?: CamoufoxConfig; wayfernConfig?: WayfernConfig; groupId?: string; + ephemeral?: boolean; }) => Promise; selectedGroupId?: string; crossOsUnlocked?: boolean; @@ -164,6 +166,7 @@ export function CreateProfileDialog({ const { vpnConfigs } = useVpnEvents(); const [showProxyForm, setShowProxyForm] = useState(false); const [isCreating, setIsCreating] = useState(false); + const [ephemeral, setEphemeral] = useState(false); const [releaseTypes, setReleaseTypes] = useState(); const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false); const [releaseTypesError, setReleaseTypesError] = useState( @@ -382,6 +385,7 @@ export function CreateProfileDialog({ wayfernConfig: finalWayfernConfig, groupId: selectedGroupId !== "default" ? selectedGroupId : undefined, + ephemeral, }); } else { // Default to Camoufox @@ -405,6 +409,7 @@ export function CreateProfileDialog({ camoufoxConfig: finalCamoufoxConfig, groupId: selectedGroupId !== "default" ? selectedGroupId : undefined, + ephemeral, }); } } else { @@ -459,6 +464,7 @@ export function CreateProfileDialog({ setWayfernConfig({ os: getCurrentOS() as WayfernOS, // Reset to current OS }); + setEphemeral(false); onClose(); }; @@ -660,6 +666,28 @@ export function CreateProfileDialog({ /> + {/* Ephemeral Toggle */} +
+ + setEphemeral(checked === true) + } + /> +
+ + + Browser data is deleted when closed + +
+
+ {selectedBrowser === "wayfern" ? ( // Wayfern Configuration
diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index cfb83d3..85ec018 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -1940,7 +1940,14 @@ export function ProfilesDataTable({ } }} > - {display} + + {display} + {profile.ephemeral && ( + + Ephemeral + + )} + ); }, @@ -2397,6 +2404,7 @@ export function ProfilesDataTable({ )} {(profile.browser === "camoufox" || profile.browser === "wayfern") && + !profile.ephemeral && meta.onCopyCookiesToProfile && ( { @@ -2414,6 +2422,7 @@ export function ProfilesDataTable({ )} {(profile.browser === "camoufox" || profile.browser === "wayfern") && + !profile.ephemeral && meta.onImportCookies && ( { @@ -2431,6 +2440,7 @@ export function ProfilesDataTable({ )} {(profile.browser === "camoufox" || profile.browser === "wayfern") && + !profile.ephemeral && meta.onExportCookies && ( { @@ -2446,14 +2456,16 @@ export function ProfilesDataTable({ )} - { - meta.onCloneProfile?.(profile); - }} - disabled={isDisabled} - > - Clone Profile - + {!profile.ephemeral && ( + { + meta.onCloneProfile?.(profile); + }} + disabled={isDisabled} + > + Clone Profile + + )} { setProfileToDelete(profile); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 42f76c0..c112794 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -158,7 +158,10 @@ "copyCookies": "Copy Cookies", "configure": "Configure", "clone": "Clone Profile" - } + }, + "ephemeral": "Ephemeral", + "ephemeralDescription": "Browser data is deleted when closed", + "ephemeralBadge": "Ephemeral" }, "createProfile": { "title": "Create New Profile", diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 3f9e6ed..a7642bd 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -158,7 +158,10 @@ "copyCookies": "Copiar Cookies", "configure": "Configurar", "clone": "Clonar perfil" - } + }, + "ephemeral": "Efímero", + "ephemeralDescription": "Los datos del navegador se eliminan al cerrarlo", + "ephemeralBadge": "Efímero" }, "createProfile": { "title": "Crear Nuevo Perfil", diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 34034f2..e76dd99 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -158,7 +158,10 @@ "copyCookies": "Copier les cookies", "configure": "Configurer", "clone": "Cloner le profil" - } + }, + "ephemeral": "Éphémère", + "ephemeralDescription": "Les données du navigateur sont supprimées à la fermeture", + "ephemeralBadge": "Éphémère" }, "createProfile": { "title": "Créer un nouveau profil", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 9793742..ed8f01e 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -158,7 +158,10 @@ "copyCookies": "Cookieをコピー", "configure": "設定", "clone": "プロファイルを複製" - } + }, + "ephemeral": "一時的", + "ephemeralDescription": "ブラウザを閉じるとデータが削除されます", + "ephemeralBadge": "一時的" }, "createProfile": { "title": "新しいプロファイルを作成", diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index b37b6de..938a33f 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -158,7 +158,10 @@ "copyCookies": "Copiar Cookies", "configure": "Configurar", "clone": "Clonar perfil" - } + }, + "ephemeral": "Efêmero", + "ephemeralDescription": "Os dados do navegador são excluídos ao fechar", + "ephemeralBadge": "Efêmero" }, "createProfile": { "title": "Criar Novo Perfil", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 300f4c1..46d8889 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -158,7 +158,10 @@ "copyCookies": "Копировать Cookie", "configure": "Настроить", "clone": "Клонировать профиль" - } + }, + "ephemeral": "Временный", + "ephemeralDescription": "Данные браузера удаляются при закрытии", + "ephemeralBadge": "Временный" }, "createProfile": { "title": "Создать новый профиль", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index a8fcdbc..e1244fe 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -158,7 +158,10 @@ "copyCookies": "复制 Cookies", "configure": "配置", "clone": "克隆配置文件" - } + }, + "ephemeral": "临时", + "ephemeralDescription": "关闭浏览器时数据将被删除", + "ephemeralBadge": "临时" }, "createProfile": { "title": "创建新配置文件", diff --git a/src/types.ts b/src/types.ts index 5f49330..ee7d34b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,6 +29,7 @@ export interface BrowserProfile { sync_enabled?: boolean; // Whether sync is enabled for this profile last_sync?: number; // Timestamp of last successful sync (epoch seconds) host_os?: string; // OS where profile was created ("macos", "windows", "linux") + ephemeral?: boolean; } export type SyncStatus = "Disabled" | "Syncing" | "Synced" | "Error";