From 59f430ec43a75a85f2c68bcee0fcf76901d0628b Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:37:42 +0400 Subject: [PATCH] refactor: make ui reactive for proxy changes --- src-tauri/src/api_server.rs | 23 +++-- src-tauri/src/lib.rs | 10 +- src-tauri/src/proxy_manager.rs | 30 +++++- src/components/profile-data-table.tsx | 33 ++---- src/components/profile-selector-dialog.tsx | 23 +---- src/components/proxy-form-dialog.tsx | 11 +- src/components/proxy-management-dialog.tsx | 106 ++----------------- src/hooks/use-proxy-events.ts | 112 +++++++++++++++++++++ 8 files changed, 181 insertions(+), 167 deletions(-) create mode 100644 src/hooks/use-proxy-events.ts diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index 29d099f..d93b6b5 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -537,11 +537,11 @@ async fn get_group( } async fn create_group( - State(_state): State, + State(state): State, Json(request): Json, ) -> Result, StatusCode> { match GROUP_MANAGER.lock() { - Ok(manager) => match manager.create_group(request.name) { + Ok(manager) => match manager.create_group(&state.app_handle, request.name) { Ok(group) => Ok(Json(ApiGroupResponse { id: group.id, name: group.name, @@ -555,11 +555,11 @@ async fn create_group( async fn update_group( Path(id): Path, - State(_state): State, + State(state): State, Json(request): Json, ) -> Result, StatusCode> { match GROUP_MANAGER.lock() { - Ok(manager) => match manager.update_group(id.clone(), request.name) { + Ok(manager) => match manager.update_group(&state.app_handle, id.clone(), request.name) { Ok(group) => Ok(Json(ApiGroupResponse { id: group.id, name: group.name, @@ -573,10 +573,10 @@ async fn update_group( async fn delete_group( Path(id): Path, - State(_state): State, + State(state): State, ) -> Result { match GROUP_MANAGER.lock() { - Ok(manager) => match manager.delete_group(id.clone()) { + Ok(manager) => match manager.delete_group(&state.app_handle, id.clone()) { Ok(_) => Ok(StatusCode::NO_CONTENT), Err(_) => Err(StatusCode::BAD_REQUEST), }, @@ -629,13 +629,13 @@ async fn get_proxy( } async fn create_proxy( - State(_state): State, + State(state): State, Json(request): Json, ) -> Result, StatusCode> { // Convert JSON value to ProxySettings match serde_json::from_value(request.proxy_settings.clone()) { Ok(proxy_settings) => { - match PROXY_MANAGER.create_stored_proxy(request.name.clone(), proxy_settings) { + match PROXY_MANAGER.create_stored_proxy(&state.app_handle, request.name.clone(), proxy_settings) { Ok(_) => { // Find the created proxy to return it let proxies = PROXY_MANAGER.get_stored_proxies(); @@ -658,7 +658,7 @@ async fn create_proxy( async fn update_proxy( Path(id): Path, - State(_state): State, + State(state): State, Json(request): Json, ) -> Result, StatusCode> { let proxies = PROXY_MANAGER.get_stored_proxies(); @@ -674,6 +674,7 @@ async fn update_proxy( }; match PROXY_MANAGER.update_stored_proxy( + &state.app_handle, &id, Some(new_name.clone()), Some(new_proxy_settings.clone()), @@ -692,9 +693,9 @@ async fn update_proxy( async fn delete_proxy( Path(id): Path, - State(_state): State, + State(state): State, ) -> Result { - match PROXY_MANAGER.delete_stored_proxy(&id) { + match PROXY_MANAGER.delete_stored_proxy(&state.app_handle, &id) { Ok(_) => Ok(StatusCode::NO_CONTENT), Err(_) => Err(StatusCode::BAD_REQUEST), } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 74d6ae2..eea359f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -174,11 +174,12 @@ async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), Strin #[tauri::command] async fn create_stored_proxy( + app_handle: tauri::AppHandle, name: String, proxy_settings: crate::browser::ProxySettings, ) -> Result { crate::proxy_manager::PROXY_MANAGER - .create_stored_proxy(name, proxy_settings) + .create_stored_proxy(&app_handle, name, proxy_settings) .map_err(|e| format!("Failed to create stored proxy: {e}")) } @@ -189,19 +190,20 @@ async fn get_stored_proxies() -> Result, #[tauri::command] async fn update_stored_proxy( + app_handle: tauri::AppHandle, proxy_id: String, name: Option, proxy_settings: Option, ) -> Result { crate::proxy_manager::PROXY_MANAGER - .update_stored_proxy(&proxy_id, name, proxy_settings) + .update_stored_proxy(&app_handle, &proxy_id, name, proxy_settings) .map_err(|e| format!("Failed to update stored proxy: {e}")) } #[tauri::command] -async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> { +async fn delete_stored_proxy(app_handle: tauri::AppHandle, proxy_id: String) -> Result<(), String> { crate::proxy_manager::PROXY_MANAGER - .delete_stored_proxy(&proxy_id) + .delete_stored_proxy(&app_handle, &proxy_id) .map_err(|e| format!("Failed to delete stored proxy: {e}")) } diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index c8e28d3..40857f2 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -6,6 +6,7 @@ use std::fs; use std::path::PathBuf; use std::sync::Mutex; use tauri_plugin_shell::ShellExt; +use tauri::Emitter; use crate::browser::ProxySettings; @@ -146,6 +147,7 @@ impl ProxyManager { // Create a new stored proxy pub fn create_stored_proxy( &self, + app_handle: &tauri::AppHandle, name: String, proxy_settings: ProxySettings, ) -> Result { @@ -168,6 +170,11 @@ impl ProxyManager { eprintln!("Warning: Failed to save proxy: {e}"); } + // Emit event for reactive UI updates + if let Err(e) = app_handle.emit("proxies-changed", ()) { + eprintln!("Failed to emit proxies-changed event: {e}"); + } + Ok(stored_proxy) } @@ -182,6 +189,7 @@ impl ProxyManager { // Update a stored proxy pub fn update_stored_proxy( &self, + app_handle: &tauri::AppHandle, proxy_id: &str, name: Option, proxy_settings: Option, @@ -226,11 +234,16 @@ impl ProxyManager { eprintln!("Warning: Failed to save proxy: {e}"); } + // Emit event for reactive UI updates + if let Err(e) = app_handle.emit("proxies-changed", ()) { + eprintln!("Failed to emit proxies-changed event: {e}"); + } + Ok(updated_proxy) } // Delete a stored proxy - pub fn delete_stored_proxy(&self, proxy_id: &str) -> Result<(), String> { + pub fn delete_stored_proxy(&self, app_handle: &tauri::AppHandle, proxy_id: &str) -> Result<(), String> { { let mut stored_proxies = self.stored_proxies.lock().unwrap(); if stored_proxies.remove(proxy_id).is_none() { @@ -242,6 +255,11 @@ impl ProxyManager { eprintln!("Warning: Failed to delete proxy file: {e}"); } + // Emit event for reactive UI updates + if let Err(e) = app_handle.emit("proxies-changed", ()) { + eprintln!("Failed to emit proxies-changed event: {e}"); + } + Ok(()) } @@ -514,6 +532,11 @@ impl ProxyManager { } } + // Emit event for reactive UI updates + if let Err(e) = app_handle.emit("proxies-changed", ()) { + eprintln!("Failed to emit proxies-changed event: {e}"); + } + Ok(()) } @@ -554,6 +577,11 @@ impl ProxyManager { let _ = self.stop_proxy(app_handle.clone(), *dead_pid).await; } + // Emit event for reactive UI updates + if let Err(e) = app_handle.emit("proxies-changed", ()) { + eprintln!("Failed to emit proxies-changed event: {e}"); + } + Ok(dead_pids) } } diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index d5e0110..643a04b 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -52,6 +52,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useBrowserState } from "@/hooks/use-browser-state"; +import { useProxyEvents } from "@/hooks/use-proxy-events"; import { useTableSorting } from "@/hooks/use-table-sorting"; import { getBrowserDisplayName, @@ -413,10 +414,8 @@ export function ProfilesDataTable({ new Set(), ); - const [storedProxies, setStoredProxies] = React.useState([]); - const [openProxySelectorFor, setOpenProxySelectorFor] = React.useState< - string | null - >(null); + const { storedProxies } = useProxyEvents(); + const [proxyOverrides, setProxyOverrides] = React.useState< Record >({}); @@ -428,6 +427,9 @@ export function ProfilesDataTable({ const [openTagsEditorFor, setOpenTagsEditorFor] = React.useState< string | null >(null); + const [openProxySelectorFor, setOpenProxySelectorFor] = React.useState< + string | null + >(null); const loadAllTags = React.useCallback(async () => { try { @@ -466,16 +468,6 @@ export function ProfilesDataTable({ stoppingProfiles, ); - // Load stored proxies - const loadStoredProxies = React.useCallback(async () => { - try { - const proxiesList = await invoke("get_stored_proxies"); - setStoredProxies(proxiesList); - } catch (error) { - console.error("Failed to load stored proxies:", error); - } - }, []); - // Clear launching/stopping spinners when backend reports running status changes React.useEffect(() => { if (!browserState.isClient) return; @@ -511,12 +503,6 @@ export function ProfilesDataTable({ }; }, [browserState.isClient]); - React.useEffect(() => { - if (browserState.isClient) { - void loadStoredProxies(); - } - }, [browserState.isClient, loadStoredProxies]); - // Keep stored proxies up-to-date by listening for changes emitted elsewhere in the app React.useEffect(() => { if (!browserState.isClient) return; @@ -524,10 +510,7 @@ export function ProfilesDataTable({ (async () => { try { unlisten = await listen("stored-proxies-changed", () => { - void loadStoredProxies(); - }); - // Also refresh tags on profile updates - await listen("profile-updated", () => { + // Also refresh tags on profile updates void loadAllTags(); }); } catch (_err) { @@ -537,7 +520,7 @@ export function ProfilesDataTable({ return () => { if (unlisten) unlisten(); }; - }, [browserState.isClient, loadStoredProxies, loadAllTags]); + }, [browserState.isClient, loadAllTags]); // Automatically deselect profiles that become running, updating, launching, or stopping React.useEffect(() => { diff --git a/src/components/profile-selector-dialog.tsx b/src/components/profile-selector-dialog.tsx index fa370eb..418ae7a 100644 --- a/src/components/profile-selector-dialog.tsx +++ b/src/components/profile-selector-dialog.tsx @@ -28,8 +28,9 @@ import { } from "@/components/ui/tooltip"; import { useBrowserState } from "@/hooks/use-browser-state"; import { useProfileEvents } from "@/hooks/use-profile-events"; +import { useProxyEvents } from "@/hooks/use-proxy-events"; import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; -import type { BrowserProfile, StoredProxy } from "@/types"; +import type { BrowserProfile } from "@/types"; import { RippleButton } from "./ui/ripple"; interface ProfileSelectorDialogProps { @@ -53,9 +54,10 @@ export function ProfileSelectorDialog({ // Use external runningProfiles if provided, otherwise use hook's runningProfiles const runningProfiles = externalRunningProfiles || hookRunningProfiles; + const { storedProxies } = useProxyEvents(); + const [selectedProfile, setSelectedProfile] = useState(null); const [isLaunching, setIsLaunching] = useState(false); - const [storedProxies, setStoredProxies] = useState([]); const [launchingProfiles, setLaunchingProfiles] = useState>( new Set(), ); @@ -82,16 +84,6 @@ export function ProfileSelectorDialog({ [storedProxies], ); - // Load stored proxies - const loadStoredProxies = useCallback(async () => { - try { - const proxiesList = await invoke("get_stored_proxies"); - setStoredProxies(proxiesList); - } catch (err) { - console.error("Failed to load stored proxies:", err); - } - }, []); - // Helper function to get tooltip content for profiles - now uses shared hook const getProfileTooltipContent = (profile: BrowserProfile): string | null => { return browserState.getProfileTooltipContent(profile); @@ -182,13 +174,6 @@ export function ProfileSelectorDialog({ } }, [isOpen, profiles, selectedProfile, runningProfiles]); - // Load stored proxies when dialog opens - useEffect(() => { - if (isOpen) { - void loadStoredProxies(); - } - }, [isOpen, loadStoredProxies]); - return ( diff --git a/src/components/proxy-form-dialog.tsx b/src/components/proxy-form-dialog.tsx index 584145d..4f6993e 100644 --- a/src/components/proxy-form-dialog.tsx +++ b/src/components/proxy-form-dialog.tsx @@ -35,14 +35,12 @@ interface ProxyFormData { interface ProxyFormDialogProps { isOpen: boolean; onClose: () => void; - onSave: (proxy: StoredProxy) => void; editingProxy?: StoredProxy | null; } export function ProxyFormDialog({ isOpen, onClose, - onSave, editingProxy, }: ProxyFormDialogProps) { const [isSubmitting, setIsSubmitting] = useState(false); @@ -105,11 +103,9 @@ export function ProxyFormDialog({ password: formData.password.trim() || undefined, }; - let savedProxy: StoredProxy; - if (editingProxy) { // Update existing proxy - savedProxy = await invoke("update_stored_proxy", { + await invoke("update_stored_proxy", { proxyId: editingProxy.id, name: formData.name.trim(), proxySettings, @@ -117,14 +113,13 @@ export function ProxyFormDialog({ toast.success("Proxy updated successfully"); } else { // Create new proxy - savedProxy = await invoke("create_stored_proxy", { + await invoke("create_stored_proxy", { name: formData.name.trim(), proxySettings, }); toast.success("Proxy created successfully"); } - onSave(savedProxy); onClose(); } catch (error) { console.error("Failed to save proxy:", error); @@ -134,7 +129,7 @@ export function ProxyFormDialog({ } finally { setIsSubmitting(false); } - }, [formData, editingProxy, onSave, onClose]); + }, [formData, editingProxy, onClose]); const handleClose = useCallback(() => { if (!isSubmitting) { diff --git a/src/components/proxy-management-dialog.tsx b/src/components/proxy-management-dialog.tsx index 2dafd33..dc420aa 100644 --- a/src/components/proxy-management-dialog.tsx +++ b/src/components/proxy-management-dialog.tsx @@ -21,6 +21,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useProxyEvents } from "@/hooks/use-proxy-events"; import { trimName } from "@/lib/name-utils"; import type { StoredProxy } from "@/types"; import { RippleButton } from "./ui/ripple"; @@ -34,84 +35,12 @@ export function ProxyManagementDialog({ isOpen, onClose, }: ProxyManagementDialogProps) { - const [storedProxies, setStoredProxies] = useState([]); - const [loading, setLoading] = useState(false); const [showProxyForm, setShowProxyForm] = useState(false); const [editingProxy, setEditingProxy] = useState(null); - const [proxyUsage, setProxyUsage] = useState>({}); const [proxyToDelete, setProxyToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); - const loadStoredProxies = useCallback(async () => { - try { - setLoading(true); - const proxies = await invoke("get_stored_proxies"); - setStoredProxies(proxies); - } catch (error) { - console.error("Failed to load stored proxies:", error); - toast.error("Failed to load proxies"); - } finally { - setLoading(false); - } - }, []); - - const loadProxyUsage = useCallback(async () => { - try { - const profiles = await invoke>( - "list_browser_profiles", - ); - const counts: Record = {}; - for (const p of profiles) { - if (p.proxy_id) counts[p.proxy_id] = (counts[p.proxy_id] ?? 0) + 1; - } - setProxyUsage(counts); - } catch (_err) { - // ignore non-critical errors - } - }, []); - - useEffect(() => { - if (isOpen) { - loadStoredProxies(); - void loadProxyUsage(); - } - }, [isOpen, loadStoredProxies, loadProxyUsage]); - - useEffect(() => { - let unlisten: (() => void) | undefined; - const setup = async () => { - try { - unlisten = await listen("profile-updated", () => { - void loadProxyUsage(); - }); - } catch (_err) { - // ignore non-critical errors - } - }; - if (isOpen) void setup(); - return () => { - if (unlisten) unlisten(); - }; - }, [isOpen, loadProxyUsage]); - - // Keep list in sync with external changes (e.g., created from CreateProfileDialog) - useEffect(() => { - let unlisten: (() => void) | undefined; - const setup = async () => { - try { - unlisten = await listen("stored-proxies-changed", () => { - void loadStoredProxies(); - void loadProxyUsage(); - }); - } catch (_err) { - // ignore non-critical errors - } - }; - if (isOpen) void setup(); - return () => { - if (unlisten) unlisten(); - }; - }, [isOpen, loadStoredProxies, loadProxyUsage]); + const { storedProxies, proxyUsage, isLoading } = useProxyEvents(); const handleDeleteProxy = useCallback((proxy: StoredProxy) => { // Open in-app confirmation dialog @@ -123,7 +52,6 @@ export function ProxyManagementDialog({ setIsDeleting(true); try { await invoke("delete_stored_proxy", { proxyId: proxyToDelete.id }); - setStoredProxies((prev) => prev.filter((p) => p.id !== proxyToDelete.id)); toast.success("Proxy deleted successfully"); await emit("stored-proxies-changed"); } catch (error) { @@ -145,24 +73,6 @@ export function ProxyManagementDialog({ setShowProxyForm(true); }, []); - const handleProxySaved = useCallback((savedProxy: StoredProxy) => { - setStoredProxies((prev) => { - const existingIndex = prev.findIndex((p) => p.id === savedProxy.id); - if (existingIndex >= 0) { - // Update existing proxy - const updated = [...prev]; - updated[existingIndex] = savedProxy; - return updated; - } else { - // Add new proxy - return [...prev, savedProxy]; - } - }); - setShowProxyForm(false); - setEditingProxy(null); - void emit("stored-proxies-changed"); - }, []); - const handleProxyFormClose = useCallback(() => { setShowProxyForm(false); setEditingProxy(null); @@ -200,13 +110,12 @@ export function ProxyManagementDialog({ {/* Proxy List - Scrollable */}
- {loading ? ( -
-

- Loading proxies... -

+ {isLoading && ( +
+
- ) : storedProxies.length === 0 ? ( + )} + {storedProxies.length === 0 && !isLoading ? (

@@ -310,7 +219,6 @@ export function ProxyManagementDialog({ ([]); + const [proxyUsage, setProxyUsage] = useState>({}); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Load proxy usage (how many profiles are using each proxy) + const loadProxyUsage = useCallback(async () => { + try { + const profiles = await invoke>( + "list_browser_profiles", + ); + const counts: Record = {}; + for (const p of profiles) { + if (p.proxy_id) counts[p.proxy_id] = (counts[p.proxy_id] ?? 0) + 1; + } + setProxyUsage(counts); + } catch (err) { + console.error("Failed to load proxy usage:", err); + // Don't set error for non-critical proxy usage + } + }, []); + + // Load proxies from backend + const loadProxies = useCallback(async () => { + try { + const stored = await invoke("get_stored_proxies"); + setStoredProxies(stored); + await loadProxyUsage(); + setError(null); + } catch (err: unknown) { + console.error("Failed to load proxies:", err); + setError(`Failed to load proxies: ${JSON.stringify(err)}`); + } + }, [loadProxyUsage]); + + // Clear error state + const clearError = useCallback(() => { + setError(null); + }, []); + + // Initial load and event listeners setup + useEffect(() => { + let proxiesUnlisten: (() => void) | undefined; + let profilesUnlisten: (() => void) | undefined; + let storedProxiesUnlisten: (() => void) | undefined; + + const setupListeners = async () => { + try { + // Initial load + await loadProxies(); + + // Listen for proxy changes (create, delete, update, start, stop, etc.) + proxiesUnlisten = await listen("proxies-changed", () => { + console.log("Received proxies-changed event, reloading proxies"); + void loadProxies(); + }); + + // Listen for profile changes to update proxy usage counts + profilesUnlisten = await listen("profiles-changed", () => { + console.log("Received profiles-changed event, reloading proxy usage"); + void loadProxyUsage(); + }); + + // Listen for profile updates to update proxy usage counts + storedProxiesUnlisten = await listen("stored-proxies-changed", () => { + console.log( + "Received stored-proxies-changed event, reloading proxies", + ); + void loadProxies(); + }); + + console.log("Proxy event listeners set up successfully"); + } catch (err) { + console.error("Failed to setup proxy event listeners:", err); + setError( + `Failed to setup proxy event listeners: ${JSON.stringify(err)}`, + ); + } finally { + setIsLoading(false); + } + }; + + void setupListeners(); + + // Cleanup listeners on unmount + return () => { + if (proxiesUnlisten) proxiesUnlisten(); + if (profilesUnlisten) profilesUnlisten(); + if (storedProxiesUnlisten) storedProxiesUnlisten(); + }; + }, [loadProxies, loadProxyUsage]); + + return { + storedProxies, + proxyUsage, + isLoading, + error, + loadProxies, + clearError, + }; +}