feat: ephemeral profiles

This commit is contained in:
zhom
2026-02-22 10:17:25 +04:00
parent 2e193987df
commit 3732d3a6e1
21 changed files with 354 additions and 33 deletions
+1
View File
@@ -605,6 +605,7 @@ async fn create_profile(
camoufox_config,
wayfern_config,
request.group_id.clone(),
false,
)
.await
{
+1
View File
@@ -522,6 +522,7 @@ mod tests {
sync_enabled: false,
last_sync: None,
host_os: None,
ephemeral: false,
}
}
+35 -5
View File
@@ -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<dyn std::error::Error + Send + Sync> { 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<dyn std::error::Error + Send + Sync> {
@@ -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<dyn std::error::Error + Send + Sync> { 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<dyn std::error::Error + Send + Sync> {
@@ -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,
+25 -2
View File
@@ -582,10 +582,15 @@ impl CamoufoxManager {
profile: BrowserProfile,
config: CamoufoxConfig,
url: Option<String>,
override_profile_path: Option<std::path::PathBuf>,
) -> Result<CamoufoxLaunchResult, String> {
// 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,
+159
View File
@@ -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<HashMap<String, PathBuf>> = Mutex::new(HashMap::new());
}
pub fn create_ephemeral_dir(profile_id: &str) -> Result<PathBuf, String> {
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<PathBuf> {
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());
}
}
+4
View File
@@ -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}");
+38 -10
View File
@@ -48,6 +48,7 @@ impl ProfileManager {
camoufox_config: Option<CamoufoxConfig>,
wayfern_config: Option<WayfernConfig>,
group_id: Option<String>,
ephemeral: bool,
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
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<bool, Box<dyn std::error::Error + Send + Sync>> {
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<bool, Box<dyn std::error::Error + Send + Sync>> {
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<CamoufoxConfig>,
wayfern_config: Option<WayfernConfig>,
group_id: Option<String>,
ephemeral: bool,
) -> Result<BrowserProfile, String> {
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<CamoufoxConfig>,
wayfern_config: Option<WayfernConfig>,
group_id: Option<String>,
ephemeral: Option<bool>,
) -> Result<BrowserProfile, String> {
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
}
+2
View File
@@ -45,6 +45,8 @@ pub struct BrowserProfile {
pub last_sync: Option<u64>, // Timestamp of last successful sync (epoch seconds)
#[serde(default)]
pub host_os: Option<String>, // OS where profile was created ("macos", "windows", "linux")
#[serde(default)]
pub ephemeral: bool,
}
pub fn default_release_type() -> String {
+1
View File
@@ -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
+8
View File
@@ -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<WayfernLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
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
}
+2
View File
@@ -494,6 +494,7 @@ export default function Home() {
camoufoxConfig?: CamoufoxConfig;
wayfernConfig?: WayfernConfig;
groupId?: string;
ephemeral?: boolean;
}) => {
try {
await invoke<BrowserProfile>("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
+28
View File
@@ -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<void>;
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<BrowserReleaseTypes>();
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
const [releaseTypesError, setReleaseTypesError] = useState<string | null>(
@@ -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({
/>
</div>
{/* Ephemeral Toggle */}
<div className="flex items-center gap-2">
<Checkbox
id="ephemeral"
checked={ephemeral}
onCheckedChange={(checked) =>
setEphemeral(checked === true)
}
/>
<div className="flex flex-col">
<Label
htmlFor="ephemeral"
className="cursor-pointer"
>
Ephemeral
</Label>
<span className="text-xs text-muted-foreground">
Browser data is deleted when closed
</span>
</div>
</div>
{selectedBrowser === "wayfern" ? (
// Wayfern Configuration
<div className="space-y-6">
+21 -9
View File
@@ -1940,7 +1940,14 @@ export function ProfilesDataTable({
}
}}
>
{display}
<span className="flex items-center gap-1">
{display}
{profile.ephemeral && (
<span className="px-1 py-0.5 text-[10px] leading-none rounded bg-muted text-muted-foreground font-medium">
Ephemeral
</span>
)}
</span>
</button>
);
},
@@ -2397,6 +2404,7 @@ export function ProfilesDataTable({
)}
{(profile.browser === "camoufox" ||
profile.browser === "wayfern") &&
!profile.ephemeral &&
meta.onCopyCookiesToProfile && (
<DropdownMenuItem
onClick={() => {
@@ -2414,6 +2422,7 @@ export function ProfilesDataTable({
)}
{(profile.browser === "camoufox" ||
profile.browser === "wayfern") &&
!profile.ephemeral &&
meta.onImportCookies && (
<DropdownMenuItem
onClick={() => {
@@ -2431,6 +2440,7 @@ export function ProfilesDataTable({
)}
{(profile.browser === "camoufox" ||
profile.browser === "wayfern") &&
!profile.ephemeral &&
meta.onExportCookies && (
<DropdownMenuItem
onClick={() => {
@@ -2446,14 +2456,16 @@ export function ProfilesDataTable({
</span>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
meta.onCloneProfile?.(profile);
}}
disabled={isDisabled}
>
Clone Profile
</DropdownMenuItem>
{!profile.ephemeral && (
<DropdownMenuItem
onClick={() => {
meta.onCloneProfile?.(profile);
}}
disabled={isDisabled}
>
Clone Profile
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
setProfileToDelete(profile);
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -158,7 +158,10 @@
"copyCookies": "Cookieをコピー",
"configure": "設定",
"clone": "プロファイルを複製"
}
},
"ephemeral": "一時的",
"ephemeralDescription": "ブラウザを閉じるとデータが削除されます",
"ephemeralBadge": "一時的"
},
"createProfile": {
"title": "新しいプロファイルを作成",
+4 -1
View File
@@ -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",
+4 -1
View File
@@ -158,7 +158,10 @@
"copyCookies": "Копировать Cookie",
"configure": "Настроить",
"clone": "Клонировать профиль"
}
},
"ephemeral": "Временный",
"ephemeralDescription": "Данные браузера удаляются при закрытии",
"ephemeralBadge": "Временный"
},
"createProfile": {
"title": "Создать новый профиль",
+4 -1
View File
@@ -158,7 +158,10 @@
"copyCookies": "复制 Cookies",
"configure": "配置",
"clone": "克隆配置文件"
}
},
"ephemeral": "临时",
"ephemeralDescription": "关闭浏览器时数据将被删除",
"ephemeralBadge": "临时"
},
"createProfile": {
"title": "创建新配置文件",
+1
View File
@@ -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";