refactor: make ui reactive for proxy changes

This commit is contained in:
zhom committed 2025-08-18 17:37:42 +04:00
1 parent 9f68a21824
commit 59f430ec43
8 files changed
+181 -167

No files matched your search

+12 -11
View File
@@ -537,11 +537,11 @@ async fn get_group(
} }
async fn create_group( async fn create_group(
State(_state): State<ApiServerState>, State(state): State<ApiServerState>,
Json(request): Json<CreateGroupRequest>, Json(request): Json<CreateGroupRequest>,
) -> Result<Json<ApiGroupResponse>, StatusCode> { ) -> Result<Json<ApiGroupResponse>, StatusCode> {
match GROUP_MANAGER.lock() { 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 { Ok(group) => Ok(Json(ApiGroupResponse {
id: group.id, id: group.id,
name: group.name, name: group.name,
@@ -555,11 +555,11 @@ async fn create_group(
async fn update_group( async fn update_group(
Path(id): Path<String>, Path(id): Path<String>,
State(_state): State<ApiServerState>, State(state): State<ApiServerState>,
Json(request): Json<UpdateGroupRequest>, Json(request): Json<UpdateGroupRequest>,
) -> Result<Json<ApiGroupResponse>, StatusCode> { ) -> Result<Json<ApiGroupResponse>, StatusCode> {
match GROUP_MANAGER.lock() { 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 { Ok(group) => Ok(Json(ApiGroupResponse {
id: group.id, id: group.id,
name: group.name, name: group.name,
@@ -573,10 +573,10 @@ async fn update_group(
async fn delete_group( async fn delete_group(
Path(id): Path<String>, Path(id): Path<String>,
State(_state): State<ApiServerState>, State(state): State<ApiServerState>,
) -> Result<StatusCode, StatusCode> { ) -> Result<StatusCode, StatusCode> {
match GROUP_MANAGER.lock() { 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), Ok(_) => Ok(StatusCode::NO_CONTENT),
Err(_) => Err(StatusCode::BAD_REQUEST), Err(_) => Err(StatusCode::BAD_REQUEST),
}, },
@@ -629,13 +629,13 @@ async fn get_proxy(
} }
async fn create_proxy( async fn create_proxy(
State(_state): State<ApiServerState>, State(state): State<ApiServerState>,
Json(request): Json<CreateProxyRequest>, Json(request): Json<CreateProxyRequest>,
) -> Result<Json<ApiProxyResponse>, StatusCode> { ) -> Result<Json<ApiProxyResponse>, StatusCode> {
// Convert JSON value to ProxySettings // Convert JSON value to ProxySettings
match serde_json::from_value(request.proxy_settings.clone()) { match serde_json::from_value(request.proxy_settings.clone()) {
Ok(proxy_settings) => { 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(_) => { Ok(_) => {
// Find the created proxy to return it // Find the created proxy to return it
let proxies = PROXY_MANAGER.get_stored_proxies(); let proxies = PROXY_MANAGER.get_stored_proxies();
@@ -658,7 +658,7 @@ async fn create_proxy(
async fn update_proxy( async fn update_proxy(
Path(id): Path<String>, Path(id): Path<String>,
State(_state): State<ApiServerState>, State(state): State<ApiServerState>,
Json(request): Json<UpdateProxyRequest>, Json(request): Json<UpdateProxyRequest>,
) -> Result<Json<ApiProxyResponse>, StatusCode> { ) -> Result<Json<ApiProxyResponse>, StatusCode> {
let proxies = PROXY_MANAGER.get_stored_proxies(); let proxies = PROXY_MANAGER.get_stored_proxies();
@@ -674,6 +674,7 @@ async fn update_proxy(
}; };
match PROXY_MANAGER.update_stored_proxy( match PROXY_MANAGER.update_stored_proxy(
&state.app_handle,
&id, &id,
Some(new_name.clone()), Some(new_name.clone()),
Some(new_proxy_settings.clone()), Some(new_proxy_settings.clone()),
@@ -692,9 +693,9 @@ async fn update_proxy(
async fn delete_proxy( async fn delete_proxy(
Path(id): Path<String>, Path(id): Path<String>,
State(_state): State<ApiServerState>, State(state): State<ApiServerState>,
) -> Result<StatusCode, StatusCode> { ) -> Result<StatusCode, StatusCode> {
match PROXY_MANAGER.delete_stored_proxy(&id) { match PROXY_MANAGER.delete_stored_proxy(&state.app_handle, &id) {
Ok(_) => Ok(StatusCode::NO_CONTENT), Ok(_) => Ok(StatusCode::NO_CONTENT),
Err(_) => Err(StatusCode::BAD_REQUEST), Err(_) => Err(StatusCode::BAD_REQUEST),
} }
+6 -4
View File
@@ -174,11 +174,12 @@ async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), Strin
#[tauri::command] #[tauri::command]
async fn create_stored_proxy( async fn create_stored_proxy(
app_handle: tauri::AppHandle,
name: String, name: String,
proxy_settings: crate::browser::ProxySettings, proxy_settings: crate::browser::ProxySettings,
) -> Result<crate::proxy_manager::StoredProxy, String> { ) -> Result<crate::proxy_manager::StoredProxy, String> {
crate::proxy_manager::PROXY_MANAGER 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}")) .map_err(|e| format!("Failed to create stored proxy: {e}"))
} }
@@ -189,19 +190,20 @@ async fn get_stored_proxies() -> Result<Vec<crate::proxy_manager::StoredProxy>,
#[tauri::command] #[tauri::command]
async fn update_stored_proxy( async fn update_stored_proxy(
app_handle: tauri::AppHandle,
proxy_id: String, proxy_id: String,
name: Option<String>, name: Option<String>,
proxy_settings: Option<crate::browser::ProxySettings>, proxy_settings: Option<crate::browser::ProxySettings>,
) -> Result<crate::proxy_manager::StoredProxy, String> { ) -> Result<crate::proxy_manager::StoredProxy, String> {
crate::proxy_manager::PROXY_MANAGER 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}")) .map_err(|e| format!("Failed to update stored proxy: {e}"))
} }
#[tauri::command] #[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 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}")) .map_err(|e| format!("Failed to delete stored proxy: {e}"))
} }
+29 -1
View File
@@ -6,6 +6,7 @@ use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Mutex; use std::sync::Mutex;
use tauri_plugin_shell::ShellExt; use tauri_plugin_shell::ShellExt;
use tauri::Emitter;
use crate::browser::ProxySettings; use crate::browser::ProxySettings;
@@ -146,6 +147,7 @@ impl ProxyManager {
// Create a new stored proxy // Create a new stored proxy
pub fn create_stored_proxy( pub fn create_stored_proxy(
&self, &self,
app_handle: &tauri::AppHandle,
name: String, name: String,
proxy_settings: ProxySettings, proxy_settings: ProxySettings,
) -> Result<StoredProxy, String> { ) -> Result<StoredProxy, String> {
@@ -168,6 +170,11 @@ impl ProxyManager {
eprintln!("Warning: Failed to save proxy: {e}"); 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) Ok(stored_proxy)
} }
@@ -182,6 +189,7 @@ impl ProxyManager {
// Update a stored proxy // Update a stored proxy
pub fn update_stored_proxy( pub fn update_stored_proxy(
&self, &self,
app_handle: &tauri::AppHandle,
proxy_id: &str, proxy_id: &str,
name: Option<String>, name: Option<String>,
proxy_settings: Option<ProxySettings>, proxy_settings: Option<ProxySettings>,
@@ -226,11 +234,16 @@ impl ProxyManager {
eprintln!("Warning: Failed to save proxy: {e}"); 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) Ok(updated_proxy)
} }
// Delete a stored 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(); let mut stored_proxies = self.stored_proxies.lock().unwrap();
if stored_proxies.remove(proxy_id).is_none() { if stored_proxies.remove(proxy_id).is_none() {
@@ -242,6 +255,11 @@ impl ProxyManager {
eprintln!("Warning: Failed to delete proxy file: {e}"); 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(()) 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(()) Ok(())
} }
@@ -554,6 +577,11 @@ impl ProxyManager {
let _ = self.stop_proxy(app_handle.clone(), *dead_pid).await; 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) Ok(dead_pids)
} }
} }
+8 -25
View File
@@ -52,6 +52,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useBrowserState } from "@/hooks/use-browser-state"; import { useBrowserState } from "@/hooks/use-browser-state";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { useTableSorting } from "@/hooks/use-table-sorting"; import { useTableSorting } from "@/hooks/use-table-sorting";
import { import {
getBrowserDisplayName, getBrowserDisplayName,
@@ -413,10 +414,8 @@ export function ProfilesDataTable({
new Set(), new Set(),
); );
const [storedProxies, setStoredProxies] = React.useState<StoredProxy[]>([]); const { storedProxies } = useProxyEvents();
const [openProxySelectorFor, setOpenProxySelectorFor] = React.useState<
string | null
>(null);
const [proxyOverrides, setProxyOverrides] = React.useState< const [proxyOverrides, setProxyOverrides] = React.useState<
Record<string, string | null> Record<string, string | null>
>({}); >({});
@@ -428,6 +427,9 @@ export function ProfilesDataTable({
const [openTagsEditorFor, setOpenTagsEditorFor] = React.useState< const [openTagsEditorFor, setOpenTagsEditorFor] = React.useState<
string | null string | null
>(null); >(null);
const [openProxySelectorFor, setOpenProxySelectorFor] = React.useState<
string | null
>(null);
const loadAllTags = React.useCallback(async () => { const loadAllTags = React.useCallback(async () => {
try { try {
@@ -466,16 +468,6 @@ export function ProfilesDataTable({
stoppingProfiles, stoppingProfiles,
); );
// Load stored proxies
const loadStoredProxies = React.useCallback(async () => {
try {
const proxiesList = await invoke<StoredProxy[]>("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 // Clear launching/stopping spinners when backend reports running status changes
React.useEffect(() => { React.useEffect(() => {
if (!browserState.isClient) return; if (!browserState.isClient) return;
@@ -511,12 +503,6 @@ export function ProfilesDataTable({
}; };
}, [browserState.isClient]); }, [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 // Keep stored proxies up-to-date by listening for changes emitted elsewhere in the app
React.useEffect(() => { React.useEffect(() => {
if (!browserState.isClient) return; if (!browserState.isClient) return;
@@ -524,10 +510,7 @@ export function ProfilesDataTable({
(async () => { (async () => {
try { try {
unlisten = await listen("stored-proxies-changed", () => { unlisten = await listen("stored-proxies-changed", () => {
void loadStoredProxies(); // Also refresh tags on profile updates
});
// Also refresh tags on profile updates
await listen("profile-updated", () => {
void loadAllTags(); void loadAllTags();
}); });
} catch (_err) { } catch (_err) {
@@ -537,7 +520,7 @@ export function ProfilesDataTable({
return () => { return () => {
if (unlisten) unlisten(); if (unlisten) unlisten();
}; };
}, [browserState.isClient, loadStoredProxies, loadAllTags]); }, [browserState.isClient, loadAllTags]);
// Automatically deselect profiles that become running, updating, launching, or stopping // Automatically deselect profiles that become running, updating, launching, or stopping
React.useEffect(() => { React.useEffect(() => {
+4 -19
View File
@@ -28,8 +28,9 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useBrowserState } from "@/hooks/use-browser-state"; import { useBrowserState } from "@/hooks/use-browser-state";
import { useProfileEvents } from "@/hooks/use-profile-events"; import { useProfileEvents } from "@/hooks/use-profile-events";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils"; import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
import type { BrowserProfile, StoredProxy } from "@/types"; import type { BrowserProfile } from "@/types";
import { RippleButton } from "./ui/ripple"; import { RippleButton } from "./ui/ripple";
interface ProfileSelectorDialogProps { interface ProfileSelectorDialogProps {
@@ -53,9 +54,10 @@ export function ProfileSelectorDialog({
// Use external runningProfiles if provided, otherwise use hook's runningProfiles // Use external runningProfiles if provided, otherwise use hook's runningProfiles
const runningProfiles = externalRunningProfiles || hookRunningProfiles; const runningProfiles = externalRunningProfiles || hookRunningProfiles;
const { storedProxies } = useProxyEvents();
const [selectedProfile, setSelectedProfile] = useState<string | null>(null); const [selectedProfile, setSelectedProfile] = useState<string | null>(null);
const [isLaunching, setIsLaunching] = useState(false); const [isLaunching, setIsLaunching] = useState(false);
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
const [launchingProfiles, setLaunchingProfiles] = useState<Set<string>>( const [launchingProfiles, setLaunchingProfiles] = useState<Set<string>>(
new Set(), new Set(),
); );
@@ -82,16 +84,6 @@ export function ProfileSelectorDialog({
[storedProxies], [storedProxies],
); );
// Load stored proxies
const loadStoredProxies = useCallback(async () => {
try {
const proxiesList = await invoke<StoredProxy[]>("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 // Helper function to get tooltip content for profiles - now uses shared hook
const getProfileTooltipContent = (profile: BrowserProfile): string | null => { const getProfileTooltipContent = (profile: BrowserProfile): string | null => {
return browserState.getProfileTooltipContent(profile); return browserState.getProfileTooltipContent(profile);
@@ -182,13 +174,6 @@ export function ProfileSelectorDialog({
} }
}, [isOpen, profiles, selectedProfile, runningProfiles]); }, [isOpen, profiles, selectedProfile, runningProfiles]);
// Load stored proxies when dialog opens
useEffect(() => {
if (isOpen) {
void loadStoredProxies();
}
}, [isOpen, loadStoredProxies]);
return ( return (
<Dialog open={isOpen} onOpenChange={onClose}> <Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md"> <DialogContent className="max-w-md">
+3 -8
View File
@@ -35,14 +35,12 @@ interface ProxyFormData {
interface ProxyFormDialogProps { interface ProxyFormDialogProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onSave: (proxy: StoredProxy) => void;
editingProxy?: StoredProxy | null; editingProxy?: StoredProxy | null;
} }
export function ProxyFormDialog({ export function ProxyFormDialog({
isOpen, isOpen,
onClose, onClose,
onSave,
editingProxy, editingProxy,
}: ProxyFormDialogProps) { }: ProxyFormDialogProps) {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@@ -105,11 +103,9 @@ export function ProxyFormDialog({
password: formData.password.trim() || undefined, password: formData.password.trim() || undefined,
}; };
let savedProxy: StoredProxy;
if (editingProxy) { if (editingProxy) {
// Update existing proxy // Update existing proxy
savedProxy = await invoke<StoredProxy>("update_stored_proxy", { await invoke("update_stored_proxy", {
proxyId: editingProxy.id, proxyId: editingProxy.id,
name: formData.name.trim(), name: formData.name.trim(),
proxySettings, proxySettings,
@@ -117,14 +113,13 @@ export function ProxyFormDialog({
toast.success("Proxy updated successfully"); toast.success("Proxy updated successfully");
} else { } else {
// Create new proxy // Create new proxy
savedProxy = await invoke<StoredProxy>("create_stored_proxy", { await invoke("create_stored_proxy", {
name: formData.name.trim(), name: formData.name.trim(),
proxySettings, proxySettings,
}); });
toast.success("Proxy created successfully"); toast.success("Proxy created successfully");
} }
onSave(savedProxy);
onClose(); onClose();
} catch (error) { } catch (error) {
console.error("Failed to save proxy:", error); console.error("Failed to save proxy:", error);
@@ -134,7 +129,7 @@ export function ProxyFormDialog({
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
} }
}, [formData, editingProxy, onSave, onClose]); }, [formData, editingProxy, onClose]);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
if (!isSubmitting) { if (!isSubmitting) {
+7 -99
View File
@@ -21,6 +21,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { trimName } from "@/lib/name-utils"; import { trimName } from "@/lib/name-utils";
import type { StoredProxy } from "@/types"; import type { StoredProxy } from "@/types";
import { RippleButton } from "./ui/ripple"; import { RippleButton } from "./ui/ripple";
@@ -34,84 +35,12 @@ export function ProxyManagementDialog({
isOpen, isOpen,
onClose, onClose,
}: ProxyManagementDialogProps) { }: ProxyManagementDialogProps) {
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
const [loading, setLoading] = useState(false);
const [showProxyForm, setShowProxyForm] = useState(false); const [showProxyForm, setShowProxyForm] = useState(false);
const [editingProxy, setEditingProxy] = useState<StoredProxy | null>(null); const [editingProxy, setEditingProxy] = useState<StoredProxy | null>(null);
const [proxyUsage, setProxyUsage] = useState<Record<string, number>>({});
const [proxyToDelete, setProxyToDelete] = useState<StoredProxy | null>(null); const [proxyToDelete, setProxyToDelete] = useState<StoredProxy | null>(null);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const loadStoredProxies = useCallback(async () => { const { storedProxies, proxyUsage, isLoading } = useProxyEvents();
try {
setLoading(true);
const proxies = await invoke<StoredProxy[]>("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<Array<{ proxy_id?: string }>>(
"list_browser_profiles",
);
const counts: Record<string, number> = {};
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 handleDeleteProxy = useCallback((proxy: StoredProxy) => { const handleDeleteProxy = useCallback((proxy: StoredProxy) => {
// Open in-app confirmation dialog // Open in-app confirmation dialog
@@ -123,7 +52,6 @@ export function ProxyManagementDialog({
setIsDeleting(true); setIsDeleting(true);
try { try {
await invoke("delete_stored_proxy", { proxyId: proxyToDelete.id }); await invoke("delete_stored_proxy", { proxyId: proxyToDelete.id });
setStoredProxies((prev) => prev.filter((p) => p.id !== proxyToDelete.id));
toast.success("Proxy deleted successfully"); toast.success("Proxy deleted successfully");
await emit("stored-proxies-changed"); await emit("stored-proxies-changed");
} catch (error) { } catch (error) {
@@ -145,24 +73,6 @@ export function ProxyManagementDialog({
setShowProxyForm(true); 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(() => { const handleProxyFormClose = useCallback(() => {
setShowProxyForm(false); setShowProxyForm(false);
setEditingProxy(null); setEditingProxy(null);
@@ -200,13 +110,12 @@ export function ProxyManagementDialog({
{/* Proxy List - Scrollable */} {/* Proxy List - Scrollable */}
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
{loading ? ( {isLoading && (
<div className="flex justify-center items-center h-32"> <div className="flex justify-center items-center py-6">
<p className="text-sm text-muted-foreground"> <div className="w-8 h-8 rounded-full border-b-2 animate-spin border-primary"></div>
Loading proxies...
</p>
</div> </div>
) : storedProxies.length === 0 ? ( )}
{storedProxies.length === 0 && !isLoading ? (
<div className="flex flex-col justify-center items-center h-32 text-center"> <div className="flex flex-col justify-center items-center h-32 text-center">
<FiWifi className="mx-auto mb-4 w-12 h-12 text-muted-foreground" /> <FiWifi className="mx-auto mb-4 w-12 h-12 text-muted-foreground" />
<p className="mb-2 text-muted-foreground"> <p className="mb-2 text-muted-foreground">
@@ -310,7 +219,6 @@ export function ProxyManagementDialog({
<ProxyFormDialog <ProxyFormDialog
isOpen={showProxyForm} isOpen={showProxyForm}
onClose={handleProxyFormClose} onClose={handleProxyFormClose}
onSave={handleProxySaved}
editingProxy={editingProxy} editingProxy={editingProxy}
/> />
<DeleteConfirmationDialog <DeleteConfirmationDialog
+112
View File
@@ -0,0 +1,112 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import type { StoredProxy } from "@/types";
/**
* Custom hook to manage proxy-related state and listen for backend events.
* This hook eliminates the need for manual UI refreshes by automatically
* updating state when the backend emits proxy change events.
*/
export function useProxyEvents() {
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
const [proxyUsage, setProxyUsage] = useState<Record<string, number>>({});
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Load proxy usage (how many profiles are using each proxy)
const loadProxyUsage = useCallback(async () => {
try {
const profiles = await invoke<Array<{ proxy_id?: string }>>(
"list_browser_profiles",
);
const counts: Record<string, number> = {};
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<StoredProxy[]>("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,
};
}