mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-29 23:27:51 +02:00
feat: add profile groups
This commit is contained in:
@@ -518,6 +518,7 @@ mod tests {
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
camoufox_config: None,
|
||||
group_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ pub struct BrowserProfile {
|
||||
pub release_type: String, // "stable" or "nightly"
|
||||
#[serde(default)]
|
||||
pub camoufox_config: Option<CamoufoxConfig>, // Camoufox configuration
|
||||
#[serde(default)]
|
||||
pub group_id: Option<String>, // Reference to profile group
|
||||
}
|
||||
|
||||
fn default_release_type() -> String {
|
||||
@@ -1351,6 +1353,28 @@ impl BrowserRunner {
|
||||
release_type: &str,
|
||||
proxy_id: Option<String>,
|
||||
camoufox_config: Option<CamoufoxConfig>,
|
||||
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
|
||||
self.create_profile_with_group(
|
||||
name,
|
||||
browser,
|
||||
version,
|
||||
release_type,
|
||||
proxy_id,
|
||||
camoufox_config,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn create_profile_with_group(
|
||||
&self,
|
||||
name: &str,
|
||||
browser: &str,
|
||||
version: &str,
|
||||
release_type: &str,
|
||||
proxy_id: Option<String>,
|
||||
camoufox_config: Option<CamoufoxConfig>,
|
||||
group_id: Option<String>,
|
||||
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
|
||||
println!("Attempting to create profile: {name}");
|
||||
|
||||
@@ -1384,6 +1408,7 @@ impl BrowserRunner {
|
||||
last_launch: None,
|
||||
release_type: release_type.to_string(),
|
||||
camoufox_config: camoufox_config.clone(),
|
||||
group_id: group_id.clone(),
|
||||
};
|
||||
|
||||
// Save profile info
|
||||
@@ -1614,6 +1639,73 @@ impl BrowserRunner {
|
||||
Ok(cleaned_up)
|
||||
}
|
||||
|
||||
pub fn assign_profiles_to_group(
|
||||
&self,
|
||||
profile_names: Vec<String>,
|
||||
group_id: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let profiles = self.list_profiles()?;
|
||||
|
||||
for profile_name in profile_names {
|
||||
let mut profile = profiles
|
||||
.iter()
|
||||
.find(|p| p.name == profile_name)
|
||||
.ok_or_else(|| format!("Profile '{profile_name}' not found"))?
|
||||
.clone();
|
||||
|
||||
// Check if browser is running
|
||||
if profile.process_id.is_some() {
|
||||
return Err(format!(
|
||||
"Cannot modify group for profile '{profile_name}' while browser is running. Please stop the browser first."
|
||||
).into());
|
||||
}
|
||||
|
||||
profile.group_id = group_id.clone();
|
||||
self.save_profile(&profile)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_multiple_profiles(
|
||||
&self,
|
||||
profile_names: Vec<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let profiles = self.list_profiles()?;
|
||||
|
||||
for profile_name in profile_names {
|
||||
let profile = profiles
|
||||
.iter()
|
||||
.find(|p| p.name == profile_name)
|
||||
.ok_or_else(|| format!("Profile '{profile_name}' not found"))?;
|
||||
|
||||
// Check if browser is running
|
||||
if profile.process_id.is_some() {
|
||||
return Err(
|
||||
format!(
|
||||
"Cannot delete profile '{profile_name}' while browser is running. Please stop the browser first."
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Delete the profile
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
|
||||
|
||||
if profile_uuid_dir.exists() {
|
||||
std::fs::remove_dir_all(&profile_uuid_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Always perform cleanup after profile deletion to remove unused binaries
|
||||
if let Err(e) = self.cleanup_unused_binaries_internal() {
|
||||
println!("Warning: Failed to cleanup unused binaries: {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_common_firefox_preferences(&self) -> Vec<String> {
|
||||
vec![
|
||||
// Disable default browser check
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
use directories::BaseDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProfileGroup {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GroupWithCount {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct GroupsData {
|
||||
groups: Vec<ProfileGroup>,
|
||||
}
|
||||
|
||||
pub struct GroupManager {
|
||||
base_dirs: BaseDirs,
|
||||
}
|
||||
|
||||
impl GroupManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_groups_file_path(&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("data");
|
||||
path.push("groups.json");
|
||||
path
|
||||
}
|
||||
|
||||
fn load_groups_data(&self) -> Result<GroupsData, Box<dyn std::error::Error>> {
|
||||
let groups_file = self.get_groups_file_path();
|
||||
|
||||
if !groups_file.exists() {
|
||||
return Ok(GroupsData { groups: Vec::new() });
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(groups_file)?;
|
||||
let groups_data: GroupsData = serde_json::from_str(&content)?;
|
||||
Ok(groups_data)
|
||||
}
|
||||
|
||||
fn save_groups_data(&self, groups_data: &GroupsData) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let groups_file = self.get_groups_file_path();
|
||||
|
||||
// Ensure the parent directory exists
|
||||
if let Some(parent) = groups_file.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(groups_data)?;
|
||||
fs::write(groups_file, json)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_all_groups(&self) -> Result<Vec<ProfileGroup>, Box<dyn std::error::Error>> {
|
||||
let groups_data = self.load_groups_data()?;
|
||||
Ok(groups_data.groups)
|
||||
}
|
||||
|
||||
pub fn create_group(&self, name: String) -> Result<ProfileGroup, Box<dyn std::error::Error>> {
|
||||
let mut groups_data = self.load_groups_data()?;
|
||||
|
||||
// Check if group with this name already exists
|
||||
if groups_data.groups.iter().any(|g| g.name == name) {
|
||||
return Err(format!("Group with name '{name}' already exists").into());
|
||||
}
|
||||
|
||||
let group = ProfileGroup {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name,
|
||||
};
|
||||
|
||||
groups_data.groups.push(group.clone());
|
||||
self.save_groups_data(&groups_data)?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
pub fn update_group(
|
||||
&self,
|
||||
id: String,
|
||||
name: String,
|
||||
) -> Result<ProfileGroup, Box<dyn std::error::Error>> {
|
||||
let mut groups_data = self.load_groups_data()?;
|
||||
|
||||
// Check if another group with this name already exists
|
||||
if groups_data
|
||||
.groups
|
||||
.iter()
|
||||
.any(|g| g.name == name && g.id != id)
|
||||
{
|
||||
return Err(format!("Group with name '{name}' already exists").into());
|
||||
}
|
||||
|
||||
let group = groups_data
|
||||
.groups
|
||||
.iter_mut()
|
||||
.find(|g| g.id == id)
|
||||
.ok_or_else(|| format!("Group with id '{id}' not found"))?;
|
||||
|
||||
group.name = name;
|
||||
let updated_group = group.clone();
|
||||
|
||||
self.save_groups_data(&groups_data)?;
|
||||
Ok(updated_group)
|
||||
}
|
||||
|
||||
pub fn delete_group(&self, id: String) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut groups_data = self.load_groups_data()?;
|
||||
|
||||
let initial_len = groups_data.groups.len();
|
||||
groups_data.groups.retain(|g| g.id != id);
|
||||
|
||||
if groups_data.groups.len() == initial_len {
|
||||
return Err(format!("Group with id '{id}' not found").into());
|
||||
}
|
||||
|
||||
self.save_groups_data(&groups_data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_groups_with_profile_counts(
|
||||
&self,
|
||||
profiles: &[crate::browser_runner::BrowserProfile],
|
||||
) -> Result<Vec<GroupWithCount>, Box<dyn std::error::Error>> {
|
||||
let groups = self.get_all_groups()?;
|
||||
let mut group_counts = HashMap::new();
|
||||
|
||||
// Count profiles in each group
|
||||
for profile in profiles {
|
||||
if let Some(group_id) = &profile.group_id {
|
||||
*group_counts.entry(group_id.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Create result with counts
|
||||
let mut result = Vec::new();
|
||||
for group in groups {
|
||||
let count = group_counts.get(&group.id).copied().unwrap_or(0);
|
||||
if count > 0 {
|
||||
result.push(GroupWithCount {
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add default group count (profiles without group_id)
|
||||
let default_count = profiles.iter().filter(|p| p.group_id.is_none()).count();
|
||||
if default_count > 0 {
|
||||
let default_group = GroupWithCount {
|
||||
id: "default".to_string(),
|
||||
name: "Default".to_string(),
|
||||
count: default_count,
|
||||
};
|
||||
result.insert(0, default_group);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref GROUP_MANAGER: Mutex<GroupManager> = Mutex::new(GroupManager::new());
|
||||
}
|
||||
|
||||
// Helper function to get groups with counts
|
||||
pub fn get_groups_with_counts(
|
||||
profiles: &[crate::browser_runner::BrowserProfile],
|
||||
) -> Vec<GroupWithCount> {
|
||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||
group_manager
|
||||
.get_groups_with_profile_counts(profiles)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// Tauri commands
|
||||
#[tauri::command]
|
||||
pub async fn get_profile_groups() -> Result<Vec<ProfileGroup>, String> {
|
||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||
group_manager
|
||||
.get_all_groups()
|
||||
.map_err(|e| format!("Failed to get profile groups: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_groups_with_profile_counts() -> Result<Vec<GroupWithCount>, String> {
|
||||
let browser_runner = crate::browser_runner::BrowserRunner::new();
|
||||
let profiles = browser_runner
|
||||
.list_profiles()
|
||||
.map_err(|e| format!("Failed to list profiles: {e}"))?;
|
||||
Ok(get_groups_with_counts(&profiles))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_profile_group(name: String) -> Result<ProfileGroup, String> {
|
||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||
group_manager
|
||||
.create_group(name)
|
||||
.map_err(|e| format!("Failed to create group: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_profile_group(group_id: String, name: String) -> Result<ProfileGroup, String> {
|
||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||
group_manager
|
||||
.update_group(group_id, name)
|
||||
.map_err(|e| format!("Failed to update group: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile_group(group_id: String) -> Result<(), String> {
|
||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||
group_manager
|
||||
.delete_group(group_id)
|
||||
.map_err(|e| format!("Failed to delete group: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn assign_profiles_to_group(
|
||||
profile_names: Vec<String>,
|
||||
group_id: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let browser_runner = crate::browser_runner::BrowserRunner::new();
|
||||
browser_runner
|
||||
.assign_profiles_to_group(profile_names, group_id)
|
||||
.map_err(|e| format!("Failed to assign profiles to group: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_selected_profiles(profile_names: Vec<String>) -> Result<(), String> {
|
||||
let browser_runner = crate::browser_runner::BrowserRunner::new();
|
||||
browser_runner
|
||||
.delete_multiple_profiles(profile_names)
|
||||
.map_err(|e| format!("Failed to delete profiles: {e}"))
|
||||
}
|
||||
@@ -19,6 +19,7 @@ mod download;
|
||||
mod downloaded_browsers;
|
||||
mod extraction;
|
||||
mod geoip_downloader;
|
||||
mod group_manager;
|
||||
|
||||
mod profile_importer;
|
||||
mod proxy_manager;
|
||||
@@ -66,6 +67,11 @@ use theme_detector::get_system_theme;
|
||||
|
||||
use system_utils::{get_system_locale, get_system_timezone};
|
||||
|
||||
use group_manager::{
|
||||
assign_profiles_to_group, create_profile_group, delete_profile_group, delete_selected_profiles,
|
||||
get_groups_with_profile_counts, get_profile_groups, update_profile_group,
|
||||
};
|
||||
|
||||
// Trait to extend WebviewWindow with transparent titlebar functionality
|
||||
pub trait WindowExt {
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -434,6 +440,13 @@ pub fn run() {
|
||||
update_camoufox_config,
|
||||
get_system_locale,
|
||||
get_system_timezone,
|
||||
get_profile_groups,
|
||||
get_groups_with_profile_counts,
|
||||
create_profile_group,
|
||||
update_profile_group,
|
||||
delete_profile_group,
|
||||
assign_profiles_to_group,
|
||||
delete_selected_profiles,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -690,6 +690,7 @@ impl ProfileImporter {
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
camoufox_config: None,
|
||||
group_id: None,
|
||||
};
|
||||
|
||||
// Save the profile metadata
|
||||
|
||||
@@ -412,26 +412,6 @@ impl ProxyManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Get proxy settings for a browser process ID
|
||||
#[allow(dead_code)]
|
||||
pub fn get_proxy_settings(&self, browser_pid: u32) -> Option<ProxySettings> {
|
||||
let proxies = self.active_proxies.lock().unwrap();
|
||||
proxies.get(&browser_pid).map(|proxy| ProxySettings {
|
||||
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,
|
||||
password: None,
|
||||
})
|
||||
}
|
||||
|
||||
// Get stored proxy info for a profile
|
||||
#[allow(dead_code)]
|
||||
pub fn get_profile_proxy_info(&self, profile_name: &str) -> Option<ProxySettings> {
|
||||
let profile_proxies = self.profile_proxies.lock().unwrap();
|
||||
profile_proxies.get(profile_name).cloned()
|
||||
}
|
||||
|
||||
// Update the PID mapping for an existing proxy
|
||||
pub fn update_proxy_pid(&self, old_pid: u32, new_pid: u32) -> Result<(), String> {
|
||||
let mut proxies = self.active_proxies.lock().unwrap();
|
||||
@@ -530,70 +510,6 @@ mod tests {
|
||||
Ok(nodecar_binary)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_proxy_manager_profile_persistence() {
|
||||
let proxy_manager = ProxyManager::new();
|
||||
|
||||
let proxy_settings = ProxySettings {
|
||||
proxy_type: "socks5".to_string(),
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 1080,
|
||||
username: None,
|
||||
password: None,
|
||||
};
|
||||
|
||||
// Test profile proxy info storage
|
||||
{
|
||||
let mut profile_proxies = proxy_manager.profile_proxies.lock().unwrap();
|
||||
profile_proxies.insert("test_profile".to_string(), proxy_settings.clone());
|
||||
}
|
||||
|
||||
// Test retrieval
|
||||
let retrieved = proxy_manager.get_profile_proxy_info("test_profile");
|
||||
assert!(retrieved.is_some());
|
||||
let retrieved = retrieved.unwrap();
|
||||
assert_eq!(retrieved.proxy_type, "socks5");
|
||||
assert_eq!(retrieved.host, "127.0.0.1");
|
||||
assert_eq!(retrieved.port, 1080);
|
||||
|
||||
// Test non-existent profile
|
||||
let non_existent = proxy_manager.get_profile_proxy_info("non_existent");
|
||||
assert!(non_existent.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_proxy_manager_active_proxy_tracking() {
|
||||
let proxy_manager = ProxyManager::new();
|
||||
|
||||
let proxy_info = ProxyInfo {
|
||||
id: "test_proxy_123".to_string(),
|
||||
local_url: "http://localhost:8080".to_string(),
|
||||
upstream_host: "proxy.example.com".to_string(),
|
||||
upstream_port: 3128,
|
||||
upstream_type: "http".to_string(),
|
||||
local_port: 8080,
|
||||
};
|
||||
|
||||
let browser_pid = 54321u32;
|
||||
|
||||
// Add active proxy
|
||||
{
|
||||
let mut active_proxies = proxy_manager.active_proxies.lock().unwrap();
|
||||
active_proxies.insert(browser_pid, proxy_info.clone());
|
||||
}
|
||||
|
||||
// Test retrieval of proxy settings
|
||||
let proxy_settings = proxy_manager.get_proxy_settings(browser_pid);
|
||||
assert!(proxy_settings.is_some());
|
||||
let settings = proxy_settings.unwrap();
|
||||
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);
|
||||
assert!(non_existent.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxy_settings_validation() {
|
||||
// Test valid proxy settings
|
||||
@@ -647,10 +563,6 @@ mod tests {
|
||||
active_proxies.insert(browser_pid, proxy_info);
|
||||
}
|
||||
|
||||
// Read proxy
|
||||
let settings = pm.get_proxy_settings(browser_pid);
|
||||
assert!(settings.is_some());
|
||||
|
||||
browser_pid
|
||||
});
|
||||
handles.push(handle);
|
||||
|
||||
Reference in New Issue
Block a user