mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-25 22:00:52 +02:00
refactor: make ui reactive for group changes
This commit is contained in:
+35
-272
@@ -4,6 +4,7 @@ use std::collections::HashMap;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
use tauri::Emitter;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ProfileGroup {
|
pub struct ProfileGroup {
|
||||||
@@ -97,7 +98,11 @@ impl GroupManager {
|
|||||||
Ok(groups_data.groups)
|
Ok(groups_data.groups)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_group(&self, name: String) -> Result<ProfileGroup, Box<dyn std::error::Error>> {
|
pub fn create_group(
|
||||||
|
&self,
|
||||||
|
app_handle: &tauri::AppHandle,
|
||||||
|
name: String,
|
||||||
|
) -> Result<ProfileGroup, Box<dyn std::error::Error>> {
|
||||||
let mut groups_data = self.load_groups_data()?;
|
let mut groups_data = self.load_groups_data()?;
|
||||||
|
|
||||||
// Check if group with this name already exists
|
// Check if group with this name already exists
|
||||||
@@ -113,11 +118,17 @@ impl GroupManager {
|
|||||||
groups_data.groups.push(group.clone());
|
groups_data.groups.push(group.clone());
|
||||||
self.save_groups_data(&groups_data)?;
|
self.save_groups_data(&groups_data)?;
|
||||||
|
|
||||||
|
// Emit event for reactive UI updates
|
||||||
|
if let Err(e) = app_handle.emit("groups-changed", ()) {
|
||||||
|
eprintln!("Failed to emit groups-changed event: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(group)
|
Ok(group)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_group(
|
pub fn update_group(
|
||||||
&self,
|
&self,
|
||||||
|
app_handle: &tauri::AppHandle,
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
) -> Result<ProfileGroup, Box<dyn std::error::Error>> {
|
) -> Result<ProfileGroup, Box<dyn std::error::Error>> {
|
||||||
@@ -142,10 +153,20 @@ impl GroupManager {
|
|||||||
let updated_group = group.clone();
|
let updated_group = group.clone();
|
||||||
|
|
||||||
self.save_groups_data(&groups_data)?;
|
self.save_groups_data(&groups_data)?;
|
||||||
|
|
||||||
|
// Emit event for reactive UI updates
|
||||||
|
if let Err(e) = app_handle.emit("groups-changed", ()) {
|
||||||
|
eprintln!("Failed to emit groups-changed event: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(updated_group)
|
Ok(updated_group)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_group(&self, id: String) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn delete_group(
|
||||||
|
&self,
|
||||||
|
app_handle: &tauri::AppHandle,
|
||||||
|
id: String,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut groups_data = self.load_groups_data()?;
|
let mut groups_data = self.load_groups_data()?;
|
||||||
|
|
||||||
let initial_len = groups_data.groups.len();
|
let initial_len = groups_data.groups.len();
|
||||||
@@ -156,6 +177,12 @@ impl GroupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.save_groups_data(&groups_data)?;
|
self.save_groups_data(&groups_data)?;
|
||||||
|
|
||||||
|
// Emit event for reactive UI updates
|
||||||
|
if let Err(e) = app_handle.emit("groups-changed", ()) {
|
||||||
|
eprintln!("Failed to emit groups-changed event: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,270 +230,6 @@ lazy_static::lazy_static! {
|
|||||||
pub static ref GROUP_MANAGER: Mutex<GroupManager> = Mutex::new(GroupManager::new());
|
pub static ref GROUP_MANAGER: Mutex<GroupManager> = Mutex::new(GroupManager::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::env;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
fn create_test_group_manager() -> (GroupManager, TempDir) {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp directory");
|
|
||||||
|
|
||||||
// Set up a temporary home directory for testing
|
|
||||||
env::set_var("HOME", temp_dir.path());
|
|
||||||
|
|
||||||
// Use per-test isolated data directory without relying on global env vars
|
|
||||||
let data_override = temp_dir.path().join("donutbrowser_test_data");
|
|
||||||
let manager = GroupManager::with_data_dir_override(&data_override);
|
|
||||||
(manager, temp_dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_group_manager_creation() {
|
|
||||||
let (_manager, _temp_dir) = create_test_group_manager();
|
|
||||||
// Test passes if no panic occurs
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_create_and_get_groups() {
|
|
||||||
let (manager, _temp_dir) = create_test_group_manager();
|
|
||||||
|
|
||||||
// Initially should have no groups
|
|
||||||
let groups = manager
|
|
||||||
.get_all_groups()
|
|
||||||
.expect("Should be able to get groups");
|
|
||||||
assert!(groups.is_empty(), "Should start with no groups");
|
|
||||||
|
|
||||||
// Create a group
|
|
||||||
let group_name = "Test Group".to_string();
|
|
||||||
let created_group = manager
|
|
||||||
.create_group(group_name.clone())
|
|
||||||
.expect("Should create group successfully");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
created_group.name, group_name,
|
|
||||||
"Created group should have correct name"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!created_group.id.is_empty(),
|
|
||||||
"Created group should have an ID"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify group was saved
|
|
||||||
let groups = manager
|
|
||||||
.get_all_groups()
|
|
||||||
.expect("Should be able to get groups");
|
|
||||||
assert_eq!(groups.len(), 1, "Should have one group");
|
|
||||||
assert_eq!(
|
|
||||||
groups[0].name, group_name,
|
|
||||||
"Retrieved group should have correct name"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
groups[0].id, created_group.id,
|
|
||||||
"Retrieved group should have correct ID"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_create_duplicate_group_fails() {
|
|
||||||
let (manager, _temp_dir) = create_test_group_manager();
|
|
||||||
|
|
||||||
let group_name = "Duplicate Group".to_string();
|
|
||||||
|
|
||||||
// Create first group
|
|
||||||
let _first_group = manager
|
|
||||||
.create_group(group_name.clone())
|
|
||||||
.expect("Should create first group");
|
|
||||||
|
|
||||||
// Try to create duplicate group
|
|
||||||
let result = manager.create_group(group_name.clone());
|
|
||||||
assert!(result.is_err(), "Should fail to create duplicate group");
|
|
||||||
|
|
||||||
let error_msg = result.unwrap_err().to_string();
|
|
||||||
assert!(
|
|
||||||
error_msg.contains("already exists"),
|
|
||||||
"Error should mention group already exists"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_group() {
|
|
||||||
let (manager, _temp_dir) = create_test_group_manager();
|
|
||||||
|
|
||||||
// Create a group
|
|
||||||
let original_name = "Original Name".to_string();
|
|
||||||
let created_group = manager
|
|
||||||
.create_group(original_name)
|
|
||||||
.expect("Should create group");
|
|
||||||
|
|
||||||
// Update the group
|
|
||||||
let new_name = "Updated Name".to_string();
|
|
||||||
let updated_group = manager
|
|
||||||
.update_group(created_group.id.clone(), new_name.clone())
|
|
||||||
.expect("Should update group successfully");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
updated_group.name, new_name,
|
|
||||||
"Updated group should have new name"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
updated_group.id, created_group.id,
|
|
||||||
"Updated group should keep same ID"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify update was persisted
|
|
||||||
let groups = manager.get_all_groups().expect("Should get groups");
|
|
||||||
assert_eq!(groups.len(), 1, "Should still have one group");
|
|
||||||
assert_eq!(
|
|
||||||
groups[0].name, new_name,
|
|
||||||
"Persisted group should have updated name"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_update_nonexistent_group_fails() {
|
|
||||||
let (manager, _temp_dir) = create_test_group_manager();
|
|
||||||
|
|
||||||
let result = manager.update_group("nonexistent-id".to_string(), "New Name".to_string());
|
|
||||||
assert!(result.is_err(), "Should fail to update nonexistent group");
|
|
||||||
|
|
||||||
let error_msg = result.unwrap_err().to_string();
|
|
||||||
assert!(
|
|
||||||
error_msg.contains("not found"),
|
|
||||||
"Error should mention group not found"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_delete_group() {
|
|
||||||
let (manager, _temp_dir) = create_test_group_manager();
|
|
||||||
|
|
||||||
// Create a group
|
|
||||||
let group_name = "To Delete".to_string();
|
|
||||||
let created_group = manager
|
|
||||||
.create_group(group_name)
|
|
||||||
.expect("Should create group");
|
|
||||||
|
|
||||||
// Verify group exists
|
|
||||||
let groups = manager.get_all_groups().expect("Should get groups");
|
|
||||||
assert_eq!(groups.len(), 1, "Should have one group");
|
|
||||||
|
|
||||||
// Delete the group
|
|
||||||
manager
|
|
||||||
.delete_group(created_group.id)
|
|
||||||
.expect("Should delete group successfully");
|
|
||||||
|
|
||||||
// Verify group was deleted
|
|
||||||
let groups = manager.get_all_groups().expect("Should get groups");
|
|
||||||
assert!(groups.is_empty(), "Should have no groups after deletion");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_delete_nonexistent_group_fails() {
|
|
||||||
let (manager, _temp_dir) = create_test_group_manager();
|
|
||||||
|
|
||||||
let result = manager.delete_group("nonexistent-id".to_string());
|
|
||||||
assert!(result.is_err(), "Should fail to delete nonexistent group");
|
|
||||||
|
|
||||||
let error_msg = result.unwrap_err().to_string();
|
|
||||||
assert!(
|
|
||||||
error_msg.contains("not found"),
|
|
||||||
"Error should mention group not found"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_groups_with_profile_counts() {
|
|
||||||
let (manager, _temp_dir) = create_test_group_manager();
|
|
||||||
|
|
||||||
// Create test groups
|
|
||||||
let group1 = manager
|
|
||||||
.create_group("Group 1".to_string())
|
|
||||||
.expect("Should create group 1");
|
|
||||||
let _group2 = manager
|
|
||||||
.create_group("Group 2".to_string())
|
|
||||||
.expect("Should create group 2");
|
|
||||||
|
|
||||||
// Create mock profiles
|
|
||||||
let profiles = vec![
|
|
||||||
crate::profile::BrowserProfile {
|
|
||||||
id: uuid::Uuid::new_v4(),
|
|
||||||
name: "Profile 1".to_string(),
|
|
||||||
browser: "firefox".to_string(),
|
|
||||||
version: "1.0".to_string(),
|
|
||||||
proxy_id: None,
|
|
||||||
process_id: None,
|
|
||||||
last_launch: None,
|
|
||||||
release_type: "stable".to_string(),
|
|
||||||
camoufox_config: None,
|
|
||||||
group_id: Some(group1.id.clone()),
|
|
||||||
tags: Vec::new(),
|
|
||||||
},
|
|
||||||
crate::profile::BrowserProfile {
|
|
||||||
id: uuid::Uuid::new_v4(),
|
|
||||||
name: "Profile 2".to_string(),
|
|
||||||
browser: "firefox".to_string(),
|
|
||||||
version: "1.0".to_string(),
|
|
||||||
proxy_id: None,
|
|
||||||
process_id: None,
|
|
||||||
last_launch: None,
|
|
||||||
release_type: "stable".to_string(),
|
|
||||||
camoufox_config: None,
|
|
||||||
group_id: Some(group1.id.clone()),
|
|
||||||
tags: Vec::new(),
|
|
||||||
},
|
|
||||||
crate::profile::BrowserProfile {
|
|
||||||
id: uuid::Uuid::new_v4(),
|
|
||||||
name: "Profile 3".to_string(),
|
|
||||||
browser: "firefox".to_string(),
|
|
||||||
version: "1.0".to_string(),
|
|
||||||
proxy_id: None,
|
|
||||||
process_id: None,
|
|
||||||
last_launch: None,
|
|
||||||
release_type: "stable".to_string(),
|
|
||||||
camoufox_config: None,
|
|
||||||
group_id: None, // Default group
|
|
||||||
tags: Vec::new(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let groups_with_counts = manager
|
|
||||||
.get_groups_with_profile_counts(&profiles)
|
|
||||||
.expect("Should get groups with counts");
|
|
||||||
|
|
||||||
// Should have default group + group1 + group2 (group2 has 0 profiles but should still appear)
|
|
||||||
assert_eq!(
|
|
||||||
groups_with_counts.len(),
|
|
||||||
3,
|
|
||||||
"Should include all groups, even those with 0 profiles"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check default group
|
|
||||||
let default_group = groups_with_counts
|
|
||||||
.iter()
|
|
||||||
.find(|g| g.id == "default")
|
|
||||||
.expect("Should have default group");
|
|
||||||
assert_eq!(
|
|
||||||
default_group.count, 1,
|
|
||||||
"Default group should have 1 profile"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check group1
|
|
||||||
let group1_with_count = groups_with_counts
|
|
||||||
.iter()
|
|
||||||
.find(|g| g.id == group1.id)
|
|
||||||
.expect("Should have group1");
|
|
||||||
assert_eq!(group1_with_count.count, 2, "Group1 should have 2 profiles");
|
|
||||||
|
|
||||||
// Check that group2 exists with 0 profiles
|
|
||||||
let group2_with_count = groups_with_counts
|
|
||||||
.iter()
|
|
||||||
.find(|g| g.name == "Group 2")
|
|
||||||
.expect("Should have group2 present even with 0 profiles");
|
|
||||||
assert_eq!(group2_with_count.count, 0, "Group2 should have 0 profiles");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get groups with counts
|
// Helper function to get groups with counts
|
||||||
pub fn get_groups_with_counts(profiles: &[crate::profile::BrowserProfile]) -> Vec<GroupWithCount> {
|
pub fn get_groups_with_counts(profiles: &[crate::profile::BrowserProfile]) -> Vec<GroupWithCount> {
|
||||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||||
@@ -494,26 +257,26 @@ pub async fn get_groups_with_profile_counts() -> Result<Vec<GroupWithCount>, Str
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn create_profile_group(name: String) -> Result<ProfileGroup, String> {
|
pub async fn create_profile_group(app_handle: tauri::AppHandle, name: String) -> Result<ProfileGroup, String> {
|
||||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||||
group_manager
|
group_manager
|
||||||
.create_group(name)
|
.create_group(&app_handle, name)
|
||||||
.map_err(|e| format!("Failed to create group: {e}"))
|
.map_err(|e| format!("Failed to create group: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn update_profile_group(group_id: String, name: String) -> Result<ProfileGroup, String> {
|
pub async fn update_profile_group(app_handle: tauri::AppHandle, group_id: String, name: String) -> Result<ProfileGroup, String> {
|
||||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||||
group_manager
|
group_manager
|
||||||
.update_group(group_id, name)
|
.update_group(&app_handle, group_id, name)
|
||||||
.map_err(|e| format!("Failed to update group: {e}"))
|
.map_err(|e| format!("Failed to update group: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn delete_profile_group(group_id: String) -> Result<(), String> {
|
pub async fn delete_profile_group(app_handle: tauri::AppHandle, group_id: String) -> Result<(), String> {
|
||||||
let group_manager = GROUP_MANAGER.lock().unwrap();
|
let group_manager = GROUP_MANAGER.lock().unwrap();
|
||||||
group_manager
|
group_manager
|
||||||
.delete_group(group_id)
|
.delete_group(&app_handle, group_id)
|
||||||
.map_err(|e| format!("Failed to delete group: {e}"))
|
.map_err(|e| format!("Failed to delete group: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-9
@@ -18,9 +18,11 @@ import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
|||||||
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
|
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
|
||||||
import { SettingsDialog } from "@/components/settings-dialog";
|
import { SettingsDialog } from "@/components/settings-dialog";
|
||||||
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
||||||
|
import { useGroupEvents } from "@/hooks/use-group-events";
|
||||||
import type { PermissionType } from "@/hooks/use-permissions";
|
import type { PermissionType } from "@/hooks/use-permissions";
|
||||||
import { usePermissions } from "@/hooks/use-permissions";
|
import { usePermissions } from "@/hooks/use-permissions";
|
||||||
import { useProfileEvents } from "@/hooks/use-profile-events";
|
import { useProfileEvents } from "@/hooks/use-profile-events";
|
||||||
|
import { useProxyEvents } from "@/hooks/use-proxy-events";
|
||||||
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
||||||
import { useVersionUpdater } from "@/hooks/use-version-updater";
|
import { useVersionUpdater } from "@/hooks/use-version-updater";
|
||||||
import { showErrorToast, showToast } from "@/lib/toast-utils";
|
import { showErrorToast, showToast } from "@/lib/toast-utils";
|
||||||
@@ -49,14 +51,23 @@ export default function Home() {
|
|||||||
// Use the new profile events hook for centralized profile management
|
// Use the new profile events hook for centralized profile management
|
||||||
const {
|
const {
|
||||||
profiles,
|
profiles,
|
||||||
groups,
|
|
||||||
runningProfiles,
|
runningProfiles,
|
||||||
isLoading: profilesLoading,
|
isLoading: profilesLoading,
|
||||||
error: profilesError,
|
error: profilesError,
|
||||||
loadProfiles,
|
|
||||||
clearError: clearProfilesError,
|
|
||||||
} = useProfileEvents();
|
} = useProfileEvents();
|
||||||
|
|
||||||
|
const {
|
||||||
|
groups: groupsData,
|
||||||
|
isLoading: groupsLoading,
|
||||||
|
error: groupsError,
|
||||||
|
} = useGroupEvents();
|
||||||
|
|
||||||
|
const {
|
||||||
|
storedProxies,
|
||||||
|
isLoading: proxiesLoading,
|
||||||
|
error: proxiesError,
|
||||||
|
} = useProxyEvents();
|
||||||
|
|
||||||
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
|
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
|
||||||
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
||||||
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
|
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
|
||||||
@@ -194,7 +205,7 @@ export default function Home() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Auto-update functionality - use the existing hook for compatibility
|
// Auto-update functionality - use the existing hook for compatibility
|
||||||
const updateNotifications = useUpdateNotifications(loadProfiles);
|
const updateNotifications = useUpdateNotifications();
|
||||||
const { checkForUpdates, isUpdating } = updateNotifications;
|
const { checkForUpdates, isUpdating } = updateNotifications;
|
||||||
|
|
||||||
useAppUpdateNotifications();
|
useAppUpdateNotifications();
|
||||||
@@ -260,9 +271,8 @@ export default function Home() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profilesError) {
|
if (profilesError) {
|
||||||
showErrorToast(profilesError);
|
showErrorToast(profilesError);
|
||||||
clearProfilesError();
|
|
||||||
}
|
}
|
||||||
}, [profilesError, clearProfilesError]);
|
}, [profilesError]);
|
||||||
|
|
||||||
const checkAllPermissions = useCallback(async () => {
|
const checkAllPermissions = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -644,6 +654,9 @@ export default function Home() {
|
|||||||
return profiles.filter((profile) => profile.group_id === selectedGroupId);
|
return profiles.filter((profile) => profile.group_id === selectedGroupId);
|
||||||
}, [profiles, selectedGroupId]);
|
}, [profiles, selectedGroupId]);
|
||||||
|
|
||||||
|
// Update loading states
|
||||||
|
const isLoading = profilesLoading || groupsLoading || proxiesLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen gap-8 font-[family-name:var(--font-geist-sans)] bg-background">
|
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen gap-8 font-[family-name:var(--font-geist-sans)] bg-background">
|
||||||
<main className="flex flex-col row-start-2 gap-6 items-center w-full max-w-3xl">
|
<main className="flex flex-col row-start-2 gap-6 items-center w-full max-w-3xl">
|
||||||
@@ -663,8 +676,8 @@ export default function Home() {
|
|||||||
<GroupBadges
|
<GroupBadges
|
||||||
selectedGroupId={selectedGroupId}
|
selectedGroupId={selectedGroupId}
|
||||||
onGroupSelect={handleSelectGroup}
|
onGroupSelect={handleSelectGroup}
|
||||||
groups={groups}
|
groups={groupsData}
|
||||||
isLoading={profilesLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
<ProfilesDataTable
|
<ProfilesDataTable
|
||||||
profiles={filteredProfiles}
|
profiles={filteredProfiles}
|
||||||
@@ -717,7 +730,6 @@ export default function Home() {
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setImportProfileDialogOpen(false);
|
setImportProfileDialogOpen(false);
|
||||||
}}
|
}}
|
||||||
onImportComplete={() => void loadProfiles()}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProxyManagementDialog
|
<ProxyManagementDialog
|
||||||
|
|||||||
@@ -31,13 +31,11 @@ import { RippleButton } from "./ui/ripple";
|
|||||||
interface ImportProfileDialogProps {
|
interface ImportProfileDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onImportComplete?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImportProfileDialog({
|
export function ImportProfileDialog({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onImportComplete,
|
|
||||||
}: ImportProfileDialogProps) {
|
}: ImportProfileDialogProps) {
|
||||||
const [detectedProfiles, setDetectedProfiles] = useState<DetectedProfile[]>(
|
const [detectedProfiles, setDetectedProfiles] = useState<DetectedProfile[]>(
|
||||||
[],
|
[],
|
||||||
@@ -140,9 +138,6 @@ export function ImportProfileDialog({
|
|||||||
toast.success(
|
toast.success(
|
||||||
`Successfully imported profile "${autoDetectProfileName.trim()}"`,
|
`Successfully imported profile "${autoDetectProfileName.trim()}"`,
|
||||||
);
|
);
|
||||||
if (onImportComplete) {
|
|
||||||
onImportComplete();
|
|
||||||
}
|
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to import profile:", error);
|
console.error("Failed to import profile:", error);
|
||||||
@@ -168,7 +163,6 @@ export function ImportProfileDialog({
|
|||||||
selectedDetectedProfile,
|
selectedDetectedProfile,
|
||||||
autoDetectProfileName,
|
autoDetectProfileName,
|
||||||
detectedProfiles,
|
detectedProfiles,
|
||||||
onImportComplete,
|
|
||||||
onClose,
|
onClose,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -193,9 +187,6 @@ export function ImportProfileDialog({
|
|||||||
toast.success(
|
toast.success(
|
||||||
`Successfully imported profile "${manualProfileName.trim()}"`,
|
`Successfully imported profile "${manualProfileName.trim()}"`,
|
||||||
);
|
);
|
||||||
if (onImportComplete) {
|
|
||||||
onImportComplete();
|
|
||||||
}
|
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to import profile:", error);
|
console.error("Failed to import profile:", error);
|
||||||
@@ -217,13 +208,7 @@ export function ImportProfileDialog({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsImporting(false);
|
setIsImporting(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [manualBrowserType, manualProfilePath, manualProfileName, onClose]);
|
||||||
manualBrowserType,
|
|
||||||
manualProfilePath,
|
|
||||||
manualProfileName,
|
|
||||||
onImportComplete,
|
|
||||||
onClose,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setSelectedDetectedProfile(null);
|
setSelectedDetectedProfile(null);
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import type { GroupWithCount } from "@/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to manage group-related state and listen for backend events.
|
||||||
|
* This hook eliminates the need for manual UI refreshes by automatically
|
||||||
|
* updating state when the backend emits group change events.
|
||||||
|
*/
|
||||||
|
export function useGroupEvents() {
|
||||||
|
const [groups, setGroups] = useState<GroupWithCount[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Load groups from backend
|
||||||
|
const loadGroups = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const groupsWithCounts = await invoke<GroupWithCount[]>(
|
||||||
|
"get_groups_with_profile_counts",
|
||||||
|
);
|
||||||
|
setGroups(groupsWithCounts);
|
||||||
|
setError(null);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error("Failed to load groups:", err);
|
||||||
|
setError(`Failed to load groups: ${JSON.stringify(err)}`);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Clear error state
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Initial load and event listeners setup
|
||||||
|
useEffect(() => {
|
||||||
|
let groupsUnlisten: (() => void) | undefined;
|
||||||
|
|
||||||
|
const setupListeners = async () => {
|
||||||
|
try {
|
||||||
|
// Initial load
|
||||||
|
await loadGroups();
|
||||||
|
|
||||||
|
// Listen for group changes (create, delete, rename, update, etc.)
|
||||||
|
groupsUnlisten = await listen("groups-changed", () => {
|
||||||
|
console.log("Received groups-changed event, reloading groups");
|
||||||
|
void loadGroups();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also listen for profile changes since groups show profile counts
|
||||||
|
const profilesUnlisten = await listen("profiles-changed", () => {
|
||||||
|
console.log(
|
||||||
|
"Received profiles-changed event, reloading groups for updated counts",
|
||||||
|
);
|
||||||
|
void loadGroups();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store both listeners for cleanup
|
||||||
|
groupsUnlisten = () => {
|
||||||
|
groupsUnlisten?.();
|
||||||
|
profilesUnlisten();
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Group event listeners set up successfully");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to setup group event listeners:", err);
|
||||||
|
setError(
|
||||||
|
`Failed to setup group event listeners: ${JSON.stringify(err)}`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void setupListeners();
|
||||||
|
|
||||||
|
// Cleanup listeners on unmount
|
||||||
|
return () => {
|
||||||
|
if (groupsUnlisten) groupsUnlisten();
|
||||||
|
};
|
||||||
|
}, [loadGroups]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
groups,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
loadGroups,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user