mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-03 17:15:12 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 269db678b7 | |||
| f809b975f3 | |||
| e369214715 | |||
| 5f93841bb7 |
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "donutbrowser",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
|
||||
Generated
+1
-1
@@ -1021,7 +1021,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "donutbrowser"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
description = "Simple Yet Powerful Anti-Detect Browser"
|
||||
authors = ["zhom@github"]
|
||||
edition = "2021"
|
||||
|
||||
+106
-117
@@ -497,19 +497,6 @@ impl ProfileManager {
|
||||
format!("Profile {profile_name} not found").into()
|
||||
})?;
|
||||
|
||||
// Check if browser is running to manage proxy accordingly
|
||||
let browser_is_running = profile.process_id.is_some()
|
||||
&& self
|
||||
.check_browser_status(app_handle.clone(), &profile)
|
||||
.await?;
|
||||
|
||||
// If browser is running, stop existing proxy
|
||||
if browser_is_running && profile.proxy_id.is_some() {
|
||||
if let Some(pid) = profile.process_id {
|
||||
let _ = PROXY_MANAGER.stop_proxy(app_handle.clone(), pid).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Update proxy settings
|
||||
profile.proxy_id = proxy_id.clone();
|
||||
|
||||
@@ -520,68 +507,16 @@ impl ProfileManager {
|
||||
format!("Failed to save profile: {e}").into()
|
||||
})?;
|
||||
|
||||
// Handle proxy startup/configuration
|
||||
// Update on-disk browser profile config immediately
|
||||
if let Some(proxy_id_ref) = &proxy_id {
|
||||
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) {
|
||||
if browser_is_running {
|
||||
// Browser is running and proxy is enabled, start new proxy
|
||||
if let Some(pid) = profile.process_id {
|
||||
match PROXY_MANAGER
|
||||
.start_proxy(
|
||||
app_handle.clone(),
|
||||
Some(&proxy_settings),
|
||||
pid,
|
||||
Some(profile_name),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(internal_proxy_settings) => {
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
|
||||
// Apply the proxy settings with the internal proxy to the profile directory
|
||||
self
|
||||
.apply_proxy_settings_to_profile(
|
||||
&profile_path,
|
||||
&proxy_settings,
|
||||
Some(&internal_proxy_settings),
|
||||
)
|
||||
.map_err(|e| format!("Failed to update profile proxy: {e}"))?;
|
||||
|
||||
println!("Successfully started proxy for profile: {}", profile.name);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to start proxy: {e}");
|
||||
// Apply proxy settings without internal proxy
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
self
|
||||
.apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to apply proxy settings: {e}").into()
|
||||
})?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No PID available, apply proxy settings without internal proxy
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
self
|
||||
.apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to apply proxy settings: {e}").into()
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
// Proxy disabled or browser not running, just apply settings
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
self
|
||||
.apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to apply proxy settings: {e}").into()
|
||||
})?;
|
||||
}
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_path = profiles_dir.join(profile.id.to_string()).join("profile");
|
||||
self
|
||||
.apply_proxy_settings_to_profile(&profile_path, &proxy_settings, None)
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to apply proxy settings: {e}").into()
|
||||
})?;
|
||||
} else {
|
||||
// Proxy ID provided but proxy not found, disable proxy
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
@@ -624,7 +559,7 @@ impl ProfileManager {
|
||||
}
|
||||
|
||||
// For non-camoufox browsers, use the existing PID-based logic
|
||||
let mut inner_profile = profile.clone();
|
||||
let inner_profile = profile.clone();
|
||||
let system = System::new_all();
|
||||
let mut is_running = false;
|
||||
let mut found_pid: Option<u32> = None;
|
||||
@@ -748,24 +683,42 @@ impl ProfileManager {
|
||||
let metadata_exists = metadata_file.exists();
|
||||
|
||||
if metadata_exists {
|
||||
// Update the process ID if we found a different one
|
||||
// Load the latest profile from disk to avoid overwriting fields like proxy_id
|
||||
let latest_profile: BrowserProfile = match std::fs::read_to_string(&metadata_file)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
{
|
||||
Some(p) => p,
|
||||
None => inner_profile.clone(),
|
||||
};
|
||||
|
||||
let previous_pid = latest_profile.process_id;
|
||||
let mut merged = latest_profile.clone();
|
||||
|
||||
if let Some(pid) = found_pid {
|
||||
if inner_profile.process_id != Some(pid) {
|
||||
inner_profile.process_id = Some(pid);
|
||||
if let Err(e) = self.save_profile(&inner_profile) {
|
||||
if merged.process_id != Some(pid) {
|
||||
merged.process_id = Some(pid);
|
||||
if let Err(e) = self.save_profile(&merged) {
|
||||
println!("Warning: Failed to update profile with new PID: {e}");
|
||||
}
|
||||
}
|
||||
} else if inner_profile.process_id.is_some() {
|
||||
} else if merged.process_id.is_some() {
|
||||
// Clear the PID if no process found
|
||||
inner_profile.process_id = None;
|
||||
if let Err(e) = self.save_profile(&inner_profile) {
|
||||
merged.process_id = None;
|
||||
if let Err(e) = self.save_profile(&merged) {
|
||||
println!("Warning: Failed to clear profile PID: {e}");
|
||||
}
|
||||
|
||||
// Stop any associated proxy immediately when the browser stops
|
||||
if let Some(old_pid) = previous_pid {
|
||||
let _ = crate::proxy_manager::PROXY_MANAGER
|
||||
.stop_proxy(app_handle.clone(), old_pid)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &inner_profile) {
|
||||
if let Err(e) = app_handle.emit("profile-updated", &merged) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
}
|
||||
@@ -790,49 +743,71 @@ impl ProfileManager {
|
||||
match launcher.find_camoufox_by_profile(&profile_path_str).await {
|
||||
Ok(Some(camoufox_process)) => {
|
||||
// Found a running instance, update profile with process info if changed
|
||||
let process_id_changed = profile.process_id != camoufox_process.processId;
|
||||
// Only write status changes if metadata still exists
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
|
||||
let metadata_file = profile_uuid_dir.join("metadata.json");
|
||||
let metadata_exists = metadata_file.exists();
|
||||
|
||||
if process_id_changed && metadata_exists {
|
||||
let mut updated_profile = profile.clone();
|
||||
updated_profile.process_id = camoufox_process.processId;
|
||||
if let Err(e) = self.save_profile(&updated_profile) {
|
||||
println!("Warning: Failed to update Camoufox profile with process info: {e}");
|
||||
}
|
||||
if metadata_exists {
|
||||
// Load latest to avoid overwriting other fields
|
||||
let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
{
|
||||
Some(p) => p,
|
||||
None => profile.clone(),
|
||||
};
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
if latest.process_id != camoufox_process.processId {
|
||||
latest.process_id = camoufox_process.processId;
|
||||
if let Err(e) = self.save_profile(&latest) {
|
||||
println!("Warning: Failed to update Camoufox profile with process info: {e}");
|
||||
}
|
||||
|
||||
println!(
|
||||
"Camoufox process has started for profile '{}' with PID: {:?}",
|
||||
profile.name, camoufox_process.processId
|
||||
);
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &latest) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
|
||||
println!(
|
||||
"Camoufox process has started for profile '{}' with PID: {:?}",
|
||||
profile.name, camoufox_process.processId
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
Ok(None) => {
|
||||
// No running instance found, clear process ID if set
|
||||
// No running instance found, clear process ID if set and stop proxy
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
|
||||
let metadata_file = profile_uuid_dir.join("metadata.json");
|
||||
let metadata_exists = metadata_file.exists();
|
||||
|
||||
if profile.process_id.is_some() && metadata_exists {
|
||||
let mut updated_profile = profile.clone();
|
||||
updated_profile.process_id = None;
|
||||
if let Err(e) = self.save_profile(&updated_profile) {
|
||||
println!("Warning: Failed to clear Camoufox profile process info: {e}");
|
||||
}
|
||||
if metadata_exists {
|
||||
let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
{
|
||||
Some(p) => p,
|
||||
None => profile.clone(),
|
||||
};
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
if let Some(old_pid) = latest.process_id {
|
||||
latest.process_id = None;
|
||||
if let Err(e) = self.save_profile(&latest) {
|
||||
println!("Warning: Failed to clear Camoufox profile process info: {e}");
|
||||
}
|
||||
|
||||
// Stop any proxy tied to this old PID immediately
|
||||
let _ = crate::proxy_manager::PROXY_MANAGER
|
||||
.stop_proxy(app_handle.clone(), old_pid)
|
||||
.await;
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &latest) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
@@ -845,16 +820,30 @@ impl ProfileManager {
|
||||
let metadata_file = profile_uuid_dir.join("metadata.json");
|
||||
let metadata_exists = metadata_file.exists();
|
||||
|
||||
if profile.process_id.is_some() && metadata_exists {
|
||||
let mut updated_profile = profile.clone();
|
||||
updated_profile.process_id = None;
|
||||
if let Err(e) = self.save_profile(&updated_profile) {
|
||||
println!("Warning: Failed to clear Camoufox profile process info after error: {e}");
|
||||
}
|
||||
if metadata_exists {
|
||||
let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
{
|
||||
Some(p) => p,
|
||||
None => profile.clone(),
|
||||
};
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
|
||||
println!("Warning: Failed to emit profile update event: {e}");
|
||||
if let Some(old_pid) = latest.process_id {
|
||||
latest.process_id = None;
|
||||
if let Err(e2) = self.save_profile(&latest) {
|
||||
println!("Warning: Failed to clear Camoufox profile process info after error: {e2}");
|
||||
}
|
||||
|
||||
// Best-effort stop of proxy tied to old PID
|
||||
let _ = crate::proxy_manager::PROXY_MANAGER
|
||||
.stop_proxy(app_handle.clone(), old_pid)
|
||||
.await;
|
||||
|
||||
// Emit profile update event to frontend
|
||||
if let Err(e3) = app_handle.emit("profile-updated", &latest) {
|
||||
println!("Warning: Failed to emit profile update event: {e3}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
|
||||
@@ -350,29 +350,6 @@ impl ProxyManager {
|
||||
let _ = self.stop_proxy(app_handle.clone(), browser_pid).await;
|
||||
}
|
||||
|
||||
// Check if we have a preferred port for this profile
|
||||
let preferred_port = if let Some(name) = profile_name {
|
||||
let profile_proxies = self.profile_proxies.lock().unwrap();
|
||||
profile_proxies.get(name).and_then(|_settings| {
|
||||
// Find existing proxy with same settings to reuse port
|
||||
let active_proxies = self.active_proxies.lock().unwrap();
|
||||
active_proxies
|
||||
.values()
|
||||
.find(|p| {
|
||||
if let Some(proxy_settings) = proxy_settings {
|
||||
p.upstream_host == proxy_settings.host
|
||||
&& p.upstream_port == proxy_settings.port
|
||||
&& p.upstream_type == proxy_settings.proxy_type
|
||||
} else {
|
||||
p.upstream_type == "DIRECT"
|
||||
}
|
||||
})
|
||||
.map(|p| p.local_port)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Start a new proxy using the nodecar binary with the correct CLI interface
|
||||
let mut nodecar = app_handle
|
||||
.shell()
|
||||
@@ -400,11 +377,6 @@ impl ProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a preferred port, use it
|
||||
if let Some(port) = preferred_port {
|
||||
nodecar = nodecar.arg("--port").arg(port.to_string());
|
||||
}
|
||||
|
||||
// Execute the command and wait for it to complete
|
||||
// The nodecar binary should start the worker and then exit
|
||||
let output = nodecar
|
||||
@@ -449,6 +421,30 @@ impl ProxyManager {
|
||||
profile_name: profile_name.map(|s| s.to_string()),
|
||||
};
|
||||
|
||||
// Wait for the local proxy port to be ready to accept connections
|
||||
{
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{sleep, Duration};
|
||||
let mut ready = false;
|
||||
for _ in 0..50 {
|
||||
match TcpStream::connect((std::net::Ipv4Addr::LOCALHOST, proxy_info.local_port)).await {
|
||||
Ok(_stream) => {
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !ready {
|
||||
return Err(format!(
|
||||
"Local proxy on 127.0.0.1:{} did not become ready in time",
|
||||
proxy_info.local_port
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Store the proxy info
|
||||
{
|
||||
let mut proxies = self.active_proxies.lock().unwrap();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Donut Browser",
|
||||
"version": "0.9.2",
|
||||
"version": "0.9.3",
|
||||
"identifier": "com.donutbrowser",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { GoPlus } from "react-icons/go";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
@@ -634,6 +635,7 @@ export function CreateProfileDialog({
|
||||
onSave={(proxy) => {
|
||||
setStoredProxies((prev) => [...prev, proxy]);
|
||||
setSelectedProxyId(proxy.id);
|
||||
void emit("stored-proxies-changed");
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { emit, listen } from "@tauri-apps/api/event";
|
||||
import * as React from "react";
|
||||
import { CiCircleCheck } from "react-icons/ci";
|
||||
import { IoEllipsisHorizontal } from "react-icons/io5";
|
||||
@@ -132,10 +133,12 @@ export function ProfilesDataTable({
|
||||
async (profileName: string, proxyId: string | null) => {
|
||||
try {
|
||||
await invoke("update_profile_proxy", {
|
||||
profileName: profileName,
|
||||
proxy_id: proxyId,
|
||||
profileName,
|
||||
proxyId,
|
||||
});
|
||||
setProxyOverrides((prev) => ({ ...prev, [profileName]: proxyId }));
|
||||
// Notify other parts of the app so usage counts and lists refresh
|
||||
await emit("profile-updated");
|
||||
} catch (error) {
|
||||
console.error("Failed to update proxy settings:", error);
|
||||
} finally {
|
||||
@@ -179,6 +182,24 @@ export function ProfilesDataTable({
|
||||
}
|
||||
}, [browserState.isClient, loadStoredProxies]);
|
||||
|
||||
// Keep stored proxies up-to-date by listening for changes emitted elsewhere in the app
|
||||
React.useEffect(() => {
|
||||
if (!browserState.isClient) return;
|
||||
let unlisten: (() => void) | undefined;
|
||||
(async () => {
|
||||
try {
|
||||
unlisten = await listen("stored-proxies-changed", () => {
|
||||
void loadStoredProxies();
|
||||
});
|
||||
} catch (_err) {
|
||||
// Best-effort only
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
if (unlisten) unlisten();
|
||||
};
|
||||
}, [browserState.isClient, loadStoredProxies]);
|
||||
|
||||
// Automatically deselect profiles that become running, updating, launching, or stopping
|
||||
React.useEffect(() => {
|
||||
setSelectedProfiles((prev) => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { emit, listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FiEdit2, FiPlus, FiTrash2, FiWifi } from "react-icons/fi";
|
||||
import { toast } from "sonner";
|
||||
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
|
||||
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -38,6 +39,8 @@ export function ProxyManagementDialog({
|
||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||
const [editingProxy, setEditingProxy] = useState<StoredProxy | null>(null);
|
||||
const [proxyUsage, setProxyUsage] = useState<Record<string, number>>({});
|
||||
const [proxyToDelete, setProxyToDelete] = useState<StoredProxy | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const loadStoredProxies = useCallback(async () => {
|
||||
try {
|
||||
@@ -91,22 +94,46 @@ export function ProxyManagementDialog({
|
||||
};
|
||||
}, [isOpen, loadProxyUsage]);
|
||||
|
||||
const handleDeleteProxy = useCallback(async (proxy: StoredProxy) => {
|
||||
if (
|
||||
!confirm(`Are you sure you want to delete the proxy "${proxy.name}"?`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// 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) => {
|
||||
// Open in-app confirmation dialog
|
||||
setProxyToDelete(proxy);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (!proxyToDelete) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await invoke("delete_stored_proxy", { proxyId: proxy.id });
|
||||
setStoredProxies((prev) => prev.filter((p) => p.id !== proxy.id));
|
||||
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) {
|
||||
console.error("Failed to delete proxy:", error);
|
||||
toast.error("Failed to delete proxy");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setProxyToDelete(null);
|
||||
}
|
||||
}, []);
|
||||
}, [proxyToDelete]);
|
||||
|
||||
const handleCreateProxy = useCallback(() => {
|
||||
setEditingProxy(null);
|
||||
@@ -133,6 +160,7 @@ export function ProxyManagementDialog({
|
||||
});
|
||||
setShowProxyForm(false);
|
||||
setEditingProxy(null);
|
||||
void emit("stored-proxies-changed");
|
||||
}, []);
|
||||
|
||||
const handleProxyFormClose = useCallback(() => {
|
||||
@@ -241,17 +269,28 @@ export function ProxyManagementDialog({
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteProxy(proxy)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<FiTrash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteProxy(proxy)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
disabled={(proxyUsage[proxy.id] ?? 0) > 0}
|
||||
>
|
||||
<FiTrash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete proxy</p>
|
||||
{(proxyUsage[proxy.id] ?? 0) > 0 ? (
|
||||
<p>
|
||||
Cannot delete: in use by {proxyUsage[proxy.id]}{" "}
|
||||
profile
|
||||
{proxyUsage[proxy.id] > 1 ? "s" : ""}
|
||||
</p>
|
||||
) : (
|
||||
<p>Delete proxy</p>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -274,6 +313,15 @@ export function ProxyManagementDialog({
|
||||
onSave={handleProxySaved}
|
||||
editingProxy={editingProxy}
|
||||
/>
|
||||
<DeleteConfirmationDialog
|
||||
isOpen={proxyToDelete !== null}
|
||||
onClose={() => setProxyToDelete(null)}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="Delete Proxy"
|
||||
description={`This action cannot be undone. This will permanently delete the proxy "${proxyToDelete?.name ?? ""}".`}
|
||||
confirmButtonText="Delete"
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export function SharedCamoufoxConfigForm({
|
||||
forceAdvanced = false,
|
||||
}: SharedCamoufoxConfigFormProps) {
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
forceAdvanced ? "advanced" : "normal",
|
||||
forceAdvanced ? "manual" : "automatic",
|
||||
);
|
||||
const [fingerprintConfig, setFingerprintConfig] =
|
||||
useState<CamoufoxFingerprintConfig>({});
|
||||
@@ -817,14 +817,13 @@ export function SharedCamoufoxConfigForm({
|
||||
// Advanced mode only (for editing)
|
||||
renderAdvancedForm()
|
||||
) : (
|
||||
// Normal/Advanced tabs for creation
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid grid-cols-2 w-full">
|
||||
<TabsTrigger value="normal">Normal</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
<TabsTrigger value="automatic">Automatic</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="normal" className="space-y-6">
|
||||
<TabsContent value="automatic" className="space-y-6">
|
||||
{/* Automatic Location Configuration */}
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -908,7 +907,7 @@ export function SharedCamoufoxConfigForm({
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="advanced" className="space-y-6">
|
||||
<TabsContent value="manual" className="space-y-6">
|
||||
{renderAdvancedForm()}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
Reference in New Issue
Block a user