use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::sync::Mutex; use crate::events; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProfileGroup { pub id: String, pub name: String, #[serde(default)] pub sync_enabled: bool, #[serde(default)] pub last_sync: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GroupWithCount { pub id: String, pub name: String, pub count: usize, #[serde(default)] pub sync_enabled: bool, #[serde(default)] pub last_sync: Option, } #[derive(Debug, Serialize, Deserialize)] struct GroupsData { groups: Vec, } pub struct GroupManager; impl GroupManager { pub fn new() -> Self { Self } fn get_groups_file_path(&self) -> std::path::PathBuf { crate::app_dirs::data_subdir().join("groups.json") } fn load_groups_data(&self) -> Result> { 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> { 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, Box> { let groups_data = self.load_groups_data()?; Ok(groups_data.groups) } pub fn create_group( &self, _app_handle: &tauri::AppHandle, name: String, ) -> Result> { 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 sync_enabled = crate::sync::is_sync_configured(); let group = ProfileGroup { id: uuid::Uuid::new_v4().to_string(), name, sync_enabled, last_sync: None, }; groups_data.groups.push(group.clone()); self.save_groups_data(&groups_data)?; // Emit event for reactive UI updates if let Err(e) = events::emit_empty("groups-changed") { log::error!("Failed to emit groups-changed event: {e}"); } if group.sync_enabled { if let Some(scheduler) = crate::sync::get_global_scheduler() { let id = group.id.clone(); tauri::async_runtime::spawn(async move { scheduler.queue_group_sync(id).await; }); } } Ok(group) } pub fn update_group( &self, _app_handle: &tauri::AppHandle, id: String, name: String, ) -> Result> { 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)?; // Emit event for reactive UI updates if let Err(e) = events::emit_empty("groups-changed") { log::error!("Failed to emit groups-changed event: {e}"); } if updated_group.sync_enabled { if let Some(scheduler) = crate::sync::get_global_scheduler() { let id = updated_group.id.clone(); tauri::async_runtime::spawn(async move { scheduler.queue_group_sync(id).await; }); } } Ok(updated_group) } pub fn update_group_internal( &self, group: &ProfileGroup, ) -> Result<(), Box> { let mut groups_data = self.load_groups_data()?; if let Some(existing) = groups_data.groups.iter_mut().find(|g| g.id == group.id) { existing.name = group.name.clone(); existing.sync_enabled = group.sync_enabled; existing.last_sync = group.last_sync; self.save_groups_data(&groups_data)?; } Ok(()) } pub fn upsert_group_internal( &self, group: &ProfileGroup, ) -> Result<(), Box> { let mut groups_data = self.load_groups_data()?; if let Some(existing) = groups_data.groups.iter_mut().find(|g| g.id == group.id) { existing.name = group.name.clone(); existing.sync_enabled = group.sync_enabled; existing.last_sync = group.last_sync; } else { groups_data.groups.push(group.clone()); } self.save_groups_data(&groups_data)?; Ok(()) } pub fn delete_group_internal(&self, id: &str) -> Result<(), Box> { 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 delete_group( &self, app_handle: &tauri::AppHandle, id: String, ) -> Result<(), Box> { let mut groups_data = self.load_groups_data()?; // Remember if sync was enabled before deleting let was_sync_enabled = groups_data .groups .iter() .find(|g| g.id == id) .map(|g| g.sync_enabled) .unwrap_or(false); 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)?; // If sync was enabled, also delete from S3 if was_sync_enabled { let group_id_owned = id.clone(); let app_handle_clone = app_handle.clone(); tauri::async_runtime::spawn(async move { match crate::sync::SyncEngine::create_from_settings(&app_handle_clone).await { Ok(engine) => { if let Err(e) = engine.delete_group(&group_id_owned).await { log::warn!("Failed to delete group {} from sync: {}", group_id_owned, e); } else { log::info!("Group {} deleted from S3 sync storage", group_id_owned); } } Err(e) => { log::debug!("Sync not configured, skipping remote deletion: {}", e); } } }); } // Emit event for reactive UI updates if let Err(e) = events::emit_empty("groups-changed") { log::error!("Failed to emit groups-changed event: {e}"); } Ok(()) } pub fn get_groups_with_profile_counts( &self, profiles: &[crate::profile::BrowserProfile], ) -> Result, Box> { 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 including all groups (even those with 0 count) let mut result = Vec::new(); for group in groups { let count = group_counts.get(&group.id).copied().unwrap_or(0); result.push(GroupWithCount { id: group.id, name: group.name, count, sync_enabled: group.sync_enabled, last_sync: group.last_sync, }); } // Add default group count (profiles without group_id), always include even if 0 let default_count = profiles.iter().filter(|p| p.group_id.is_none()).count(); let default_group = GroupWithCount { id: "default".to_string(), name: "Default".to_string(), count: default_count, sync_enabled: false, last_sync: None, }; // Insert at the beginning for consistent ordering with UI expectations result.insert(0, default_group); Ok(result) } } // Global instance lazy_static::lazy_static! { pub static ref GROUP_MANAGER: Mutex = Mutex::new(GroupManager::new()); } // Helper function to get groups with counts pub fn get_groups_with_counts(profiles: &[crate::profile::BrowserProfile]) -> Vec { 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, 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, String> { let profile_manager = crate::profile::ProfileManager::instance(); let profiles = profile_manager .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( app_handle: tauri::AppHandle, name: String, ) -> Result { let group_manager = GROUP_MANAGER.lock().unwrap(); group_manager .create_group(&app_handle, name) .map_err(|e| format!("Failed to create group: {e}")) } #[tauri::command] pub async fn update_profile_group( app_handle: tauri::AppHandle, group_id: String, name: String, ) -> Result { let group_manager = GROUP_MANAGER.lock().unwrap(); group_manager .update_group(&app_handle, group_id, name) .map_err(|e| format!("Failed to update group: {e}")) } #[tauri::command] pub async fn delete_profile_group( app_handle: tauri::AppHandle, group_id: String, ) -> Result<(), String> { let group_manager = GROUP_MANAGER.lock().unwrap(); group_manager .delete_group(&app_handle, group_id) .map_err(|e| format!("Failed to delete group: {e}")) } #[tauri::command] pub async fn assign_profiles_to_group( app_handle: tauri::AppHandle, profile_ids: Vec, group_id: Option, ) -> Result<(), String> { let profile_manager = crate::profile::ProfileManager::instance(); profile_manager .assign_profiles_to_group(&app_handle, profile_ids, group_id) .map_err(|e| format!("Failed to assign profiles to group: {e}")) } #[tauri::command] pub async fn delete_selected_profiles( app_handle: tauri::AppHandle, profile_ids: Vec, ) -> Result<(), String> { let profile_manager = crate::profile::ProfileManager::instance(); profile_manager .delete_multiple_profiles(&app_handle, profile_ids) .map_err(|e| format!("Failed to delete profiles: {e}")) }