mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-23 09:50:58 +02:00
feat: profile cloning
This commit is contained in:
@@ -30,7 +30,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, ubuntu-22.04]
|
os: [macos-latest, ubuntu-22.04, windows-latest]
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ fn is_daemon_running() -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
fn is_dev_mode() -> bool {
|
fn is_dev_mode() -> bool {
|
||||||
if let Ok(current_exe) = std::env::current_exe() {
|
if let Ok(current_exe) = std::env::current_exe() {
|
||||||
let path_str = current_exe.to_string_lossy();
|
let path_str = current_exe.to_string_lossy();
|
||||||
@@ -184,11 +185,8 @@ pub fn spawn_daemon() -> Result<(), String> {
|
|||||||
|
|
||||||
// Check if we got a state file at least
|
// Check if we got a state file at least
|
||||||
let state = read_state();
|
let state = read_state();
|
||||||
if state.daemon_pid.is_some() {
|
if let Some(pid) = state.daemon_pid {
|
||||||
log::info!(
|
log::info!("Daemon appears to have started (PID {} in state file)", pid);
|
||||||
"Daemon appears to have started (PID {} in state file)",
|
|
||||||
state.daemon_pid.unwrap()
|
|
||||||
);
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ use browser_runner::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use profile::manager::{
|
use profile::manager::{
|
||||||
check_browser_status, create_browser_profile_new, delete_profile, list_browser_profiles,
|
check_browser_status, clone_profile, create_browser_profile_new, delete_profile,
|
||||||
rename_profile, update_camoufox_config, update_profile_note, update_profile_proxy,
|
list_browser_profiles, rename_profile, update_camoufox_config, update_profile_note,
|
||||||
update_profile_tags, update_wayfern_config,
|
update_profile_proxy, update_profile_tags, update_wayfern_config,
|
||||||
};
|
};
|
||||||
|
|
||||||
use browser_version_manager::{
|
use browser_version_manager::{
|
||||||
@@ -1092,6 +1092,7 @@ pub fn run() {
|
|||||||
download_browser,
|
download_browser,
|
||||||
cancel_download,
|
cancel_download,
|
||||||
delete_profile,
|
delete_profile,
|
||||||
|
clone_profile,
|
||||||
check_browser_exists,
|
check_browser_exists,
|
||||||
create_browser_profile_new,
|
create_browser_profile_new,
|
||||||
list_browser_profiles,
|
list_browser_profiles,
|
||||||
|
|||||||
@@ -780,6 +780,84 @@ impl ProfileManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_clone_name(&self, original_name: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
let profiles = self.list_profiles()?;
|
||||||
|
let existing_names: std::collections::HashSet<String> =
|
||||||
|
profiles.iter().map(|p| p.name.clone()).collect();
|
||||||
|
|
||||||
|
let candidate = format!("{original_name} (Copy)");
|
||||||
|
if !existing_names.contains(&candidate) {
|
||||||
|
return Ok(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 2.. {
|
||||||
|
let candidate = format!("{original_name} (Copy {i})");
|
||||||
|
if !existing_names.contains(&candidate) {
|
||||||
|
return Ok(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone_profile(
|
||||||
|
&self,
|
||||||
|
profile_id: &str,
|
||||||
|
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
|
||||||
|
let profile_uuid =
|
||||||
|
uuid::Uuid::parse_str(profile_id).map_err(|_| format!("Invalid profile ID: {profile_id}"))?;
|
||||||
|
let profiles = self.list_profiles()?;
|
||||||
|
let source = profiles
|
||||||
|
.into_iter()
|
||||||
|
.find(|p| p.id == profile_uuid)
|
||||||
|
.ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?;
|
||||||
|
|
||||||
|
if source.process_id.is_some() {
|
||||||
|
return Err(
|
||||||
|
"Cannot clone profile while browser is running. Please stop the browser first.".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_id = uuid::Uuid::new_v4();
|
||||||
|
let clone_name = self.generate_clone_name(&source.name)?;
|
||||||
|
|
||||||
|
let profiles_dir = self.get_profiles_dir();
|
||||||
|
let source_dir = profiles_dir.join(source.id.to_string());
|
||||||
|
let dest_dir = profiles_dir.join(new_id.to_string());
|
||||||
|
|
||||||
|
if source_dir.exists() {
|
||||||
|
crate::profile_importer::ProfileImporter::copy_directory_recursive(&source_dir, &dest_dir)?;
|
||||||
|
} else {
|
||||||
|
fs::create_dir_all(&dest_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_profile = BrowserProfile {
|
||||||
|
id: new_id,
|
||||||
|
name: clone_name,
|
||||||
|
browser: source.browser,
|
||||||
|
version: source.version,
|
||||||
|
proxy_id: source.proxy_id,
|
||||||
|
process_id: None,
|
||||||
|
last_launch: None,
|
||||||
|
release_type: source.release_type,
|
||||||
|
camoufox_config: source.camoufox_config,
|
||||||
|
wayfern_config: source.wayfern_config,
|
||||||
|
group_id: source.group_id,
|
||||||
|
tags: source.tags,
|
||||||
|
note: source.note,
|
||||||
|
sync_enabled: false,
|
||||||
|
last_sync: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.save_profile(&new_profile)?;
|
||||||
|
|
||||||
|
if let Err(e) = events::emit_empty("profiles-changed") {
|
||||||
|
log::warn!("Warning: Failed to emit profiles-changed event: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(new_profile)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update_camoufox_config(
|
pub async fn update_camoufox_config(
|
||||||
&self,
|
&self,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
@@ -1862,7 +1940,13 @@ pub async fn update_wayfern_config(
|
|||||||
.map_err(|e| format!("Failed to update Wayfern config: {e}"))
|
.map_err(|e| format!("Failed to update Wayfern config: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global singleton instance
|
#[tauri::command]
|
||||||
|
pub fn clone_profile(profile_id: String) -> Result<BrowserProfile, String> {
|
||||||
|
ProfileManager::instance()
|
||||||
|
.clone_profile(&profile_id)
|
||||||
|
.map_err(|e| format!("Failed to clone profile: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_profile(app_handle: tauri::AppHandle, profile_id: String) -> Result<(), String> {
|
pub fn delete_profile(app_handle: tauri::AppHandle, profile_id: String) -> Result<(), String> {
|
||||||
ProfileManager::instance()
|
ProfileManager::instance()
|
||||||
|
|||||||
@@ -592,7 +592,7 @@ impl ProfileImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively copy directory contents
|
/// Recursively copy directory contents
|
||||||
fn copy_directory_recursive(
|
pub fn copy_directory_recursive(
|
||||||
source: &Path,
|
source: &Path,
|
||||||
destination: &Path,
|
destination: &Path,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|||||||
@@ -488,6 +488,18 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleCloneProfile = useCallback(async (profile: BrowserProfile) => {
|
||||||
|
try {
|
||||||
|
await invoke<BrowserProfile>("clone_profile", {
|
||||||
|
profileId: profile.id,
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error("Failed to clone profile:", err);
|
||||||
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
showErrorToast(`Failed to clone profile: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDeleteProfile = useCallback(async (profile: BrowserProfile) => {
|
const handleDeleteProfile = useCallback(async (profile: BrowserProfile) => {
|
||||||
console.log("Attempting to delete profile:", profile.name);
|
console.log("Attempting to delete profile:", profile.name);
|
||||||
|
|
||||||
@@ -876,6 +888,7 @@ export default function Home() {
|
|||||||
profiles={filteredProfiles}
|
profiles={filteredProfiles}
|
||||||
onLaunchProfile={launchProfile}
|
onLaunchProfile={launchProfile}
|
||||||
onKillProfile={handleKillProfile}
|
onKillProfile={handleKillProfile}
|
||||||
|
onCloneProfile={handleCloneProfile}
|
||||||
onDeleteProfile={handleDeleteProfile}
|
onDeleteProfile={handleDeleteProfile}
|
||||||
onRenameProfile={handleRenameProfile}
|
onRenameProfile={handleRenameProfile}
|
||||||
onConfigureCamoufox={handleConfigureCamoufox}
|
onConfigureCamoufox={handleConfigureCamoufox}
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ type TableMeta = {
|
|||||||
// Overflow actions
|
// Overflow actions
|
||||||
onAssignProfilesToGroup?: (profileIds: string[]) => void;
|
onAssignProfilesToGroup?: (profileIds: string[]) => void;
|
||||||
onConfigureCamoufox?: (profile: BrowserProfile) => void;
|
onConfigureCamoufox?: (profile: BrowserProfile) => void;
|
||||||
|
onCloneProfile?: (profile: BrowserProfile) => void;
|
||||||
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
|
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
|
||||||
|
|
||||||
// Traffic snapshots (lightweight real-time data)
|
// Traffic snapshots (lightweight real-time data)
|
||||||
@@ -672,6 +673,7 @@ interface ProfilesDataTableProps {
|
|||||||
profiles: BrowserProfile[];
|
profiles: BrowserProfile[];
|
||||||
onLaunchProfile: (profile: BrowserProfile) => void | Promise<void>;
|
onLaunchProfile: (profile: BrowserProfile) => void | Promise<void>;
|
||||||
onKillProfile: (profile: BrowserProfile) => void | Promise<void>;
|
onKillProfile: (profile: BrowserProfile) => void | Promise<void>;
|
||||||
|
onCloneProfile: (profile: BrowserProfile) => void | Promise<void>;
|
||||||
onDeleteProfile: (profile: BrowserProfile) => void | Promise<void>;
|
onDeleteProfile: (profile: BrowserProfile) => void | Promise<void>;
|
||||||
onRenameProfile: (profileId: string, newName: string) => Promise<void>;
|
onRenameProfile: (profileId: string, newName: string) => Promise<void>;
|
||||||
onConfigureCamoufox: (profile: BrowserProfile) => void;
|
onConfigureCamoufox: (profile: BrowserProfile) => void;
|
||||||
@@ -695,6 +697,7 @@ export function ProfilesDataTable({
|
|||||||
profiles,
|
profiles,
|
||||||
onLaunchProfile,
|
onLaunchProfile,
|
||||||
onKillProfile,
|
onKillProfile,
|
||||||
|
onCloneProfile,
|
||||||
onDeleteProfile,
|
onDeleteProfile,
|
||||||
onRenameProfile,
|
onRenameProfile,
|
||||||
onConfigureCamoufox,
|
onConfigureCamoufox,
|
||||||
@@ -1310,6 +1313,7 @@ export function ProfilesDataTable({
|
|||||||
|
|
||||||
// Overflow actions
|
// Overflow actions
|
||||||
onAssignProfilesToGroup,
|
onAssignProfilesToGroup,
|
||||||
|
onCloneProfile,
|
||||||
onConfigureCamoufox,
|
onConfigureCamoufox,
|
||||||
onCopyCookiesToProfile,
|
onCopyCookiesToProfile,
|
||||||
|
|
||||||
@@ -1359,6 +1363,7 @@ export function ProfilesDataTable({
|
|||||||
onKillProfile,
|
onKillProfile,
|
||||||
onLaunchProfile,
|
onLaunchProfile,
|
||||||
onAssignProfilesToGroup,
|
onAssignProfilesToGroup,
|
||||||
|
onCloneProfile,
|
||||||
onConfigureCamoufox,
|
onConfigureCamoufox,
|
||||||
onCopyCookiesToProfile,
|
onCopyCookiesToProfile,
|
||||||
syncStatuses,
|
syncStatuses,
|
||||||
@@ -1999,6 +2004,14 @@ export function ProfilesDataTable({
|
|||||||
Copy Cookies to Profile
|
Copy Cookies to Profile
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
meta.onCloneProfile?.(profile);
|
||||||
|
}}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
Clone Profile
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setProfileToDelete(profile);
|
setProfileToDelete(profile);
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"copyCookies": "Copy Cookies",
|
"copyCookies": "Copy Cookies",
|
||||||
"configure": "Configure"
|
"configure": "Configure",
|
||||||
|
"clone": "Clone Profile"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"createProfile": {
|
"createProfile": {
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"copyCookies": "Copiar Cookies",
|
"copyCookies": "Copiar Cookies",
|
||||||
"configure": "Configurar"
|
"configure": "Configurar",
|
||||||
|
"clone": "Clonar perfil"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"createProfile": {
|
"createProfile": {
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
"edit": "Modifier",
|
"edit": "Modifier",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"copyCookies": "Copier les cookies",
|
"copyCookies": "Copier les cookies",
|
||||||
"configure": "Configurer"
|
"configure": "Configurer",
|
||||||
|
"clone": "Cloner le profil"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"createProfile": {
|
"createProfile": {
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
"edit": "編集",
|
"edit": "編集",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"copyCookies": "Cookieをコピー",
|
"copyCookies": "Cookieをコピー",
|
||||||
"configure": "設定"
|
"configure": "設定",
|
||||||
|
"clone": "プロファイルを複製"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"createProfile": {
|
"createProfile": {
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"delete": "Excluir",
|
"delete": "Excluir",
|
||||||
"copyCookies": "Copiar Cookies",
|
"copyCookies": "Copiar Cookies",
|
||||||
"configure": "Configurar"
|
"configure": "Configurar",
|
||||||
|
"clone": "Clonar perfil"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"createProfile": {
|
"createProfile": {
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
"edit": "Редактировать",
|
"edit": "Редактировать",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"copyCookies": "Копировать Cookie",
|
"copyCookies": "Копировать Cookie",
|
||||||
"configure": "Настроить"
|
"configure": "Настроить",
|
||||||
|
"clone": "Клонировать профиль"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"createProfile": {
|
"createProfile": {
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"copyCookies": "复制 Cookies",
|
"copyCookies": "复制 Cookies",
|
||||||
"configure": "配置"
|
"configure": "配置",
|
||||||
|
"clone": "克隆配置文件"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"createProfile": {
|
"createProfile": {
|
||||||
|
|||||||
Reference in New Issue
Block a user