mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-07 23:43:57 +02:00
feat: add proxy management
This commit is contained in:
+42
-17
@@ -5,6 +5,7 @@ import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrent } from "@tauri-apps/plugin-deep-link";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FaDownload } from "react-icons/fa";
|
||||
import { FiWifi } from "react-icons/fi";
|
||||
import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go";
|
||||
import { ChangeVersionDialog } from "@/components/change-version-dialog";
|
||||
import { CreateProfileDialog } from "@/components/create-profile-dialog";
|
||||
@@ -12,6 +13,7 @@ import { ImportProfileDialog } from "@/components/import-profile-dialog";
|
||||
import { PermissionDialog } from "@/components/permission-dialog";
|
||||
import { ProfilesDataTable } from "@/components/profile-data-table";
|
||||
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
||||
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
|
||||
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
|
||||
import { SettingsDialog } from "@/components/settings-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -34,7 +36,7 @@ import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
||||
import { useVersionUpdater } from "@/hooks/use-version-updater";
|
||||
import { showErrorToast } from "@/lib/toast-utils";
|
||||
import { sleep } from "@/lib/utils";
|
||||
import type { BrowserProfile, ProxySettings } from "@/types";
|
||||
import type { BrowserProfile } from "@/types";
|
||||
|
||||
type BrowserTypeString =
|
||||
| "mullvad-browser"
|
||||
@@ -58,6 +60,8 @@ export default function Home() {
|
||||
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false);
|
||||
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
||||
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
|
||||
const [proxyManagementDialogOpen, setProxyManagementDialogOpen] =
|
||||
useState(false);
|
||||
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
|
||||
const [currentProfileForProxy, setCurrentProfileForProxy] =
|
||||
useState<BrowserProfile | null>(null);
|
||||
@@ -67,6 +71,7 @@ export default function Home() {
|
||||
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
|
||||
const [currentPermissionType, setCurrentPermissionType] =
|
||||
useState<PermissionType>("microphone");
|
||||
const [proxyDataReloadTrigger, setProxyDataReloadTrigger] = useState(0);
|
||||
const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } =
|
||||
usePermissions();
|
||||
|
||||
@@ -140,6 +145,11 @@ export default function Home() {
|
||||
}
|
||||
}, [checkMissingBinaries]);
|
||||
|
||||
// Trigger proxy data reload in ProfilesDataTable
|
||||
const triggerProxyDataReload = useCallback(() => {
|
||||
setProxyDataReloadTrigger((prev) => prev + 1);
|
||||
}, []);
|
||||
|
||||
const handleUrlOpen = useCallback(async (url: string) => {
|
||||
try {
|
||||
// Use smart profile selection
|
||||
@@ -317,7 +327,7 @@ export default function Home() {
|
||||
}, []);
|
||||
|
||||
const handleSaveProxy = useCallback(
|
||||
async (proxySettings: ProxySettings) => {
|
||||
async (proxyId: string | null) => {
|
||||
setProxyDialogOpen(false);
|
||||
setError(null);
|
||||
|
||||
@@ -325,16 +335,18 @@ export default function Home() {
|
||||
if (currentProfileForProxy) {
|
||||
await invoke("update_profile_proxy", {
|
||||
profileName: currentProfileForProxy.name,
|
||||
proxy: proxySettings,
|
||||
proxyId: proxyId,
|
||||
});
|
||||
}
|
||||
await loadProfiles();
|
||||
// Trigger proxy data reload in the table
|
||||
triggerProxyDataReload();
|
||||
} catch (err: unknown) {
|
||||
console.error("Failed to update proxy settings:", err);
|
||||
setError(`Failed to update proxy settings: ${JSON.stringify(err)}`);
|
||||
}
|
||||
},
|
||||
[currentProfileForProxy, loadProfiles],
|
||||
[currentProfileForProxy, loadProfiles, triggerProxyDataReload],
|
||||
);
|
||||
|
||||
const handleCreateProfile = useCallback(
|
||||
@@ -343,30 +355,25 @@ export default function Home() {
|
||||
browserStr: BrowserTypeString;
|
||||
version: string;
|
||||
releaseType: string;
|
||||
proxy?: ProxySettings;
|
||||
proxyId?: string;
|
||||
}) => {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const profile = await invoke<BrowserProfile>(
|
||||
const _profile = await invoke<BrowserProfile>(
|
||||
"create_browser_profile_new",
|
||||
{
|
||||
name: profileData.name,
|
||||
browserStr: profileData.browserStr,
|
||||
version: profileData.version,
|
||||
releaseType: profileData.releaseType,
|
||||
proxyId: profileData.proxyId,
|
||||
},
|
||||
);
|
||||
|
||||
// Update proxy if provided
|
||||
if (profileData.proxy) {
|
||||
await invoke("update_profile_proxy", {
|
||||
profileName: profile.name,
|
||||
proxy: profileData.proxy,
|
||||
});
|
||||
}
|
||||
|
||||
await loadProfiles();
|
||||
// Trigger proxy data reload in the table
|
||||
triggerProxyDataReload();
|
||||
} catch (error) {
|
||||
setError(
|
||||
`Failed to create profile: ${
|
||||
@@ -376,7 +383,7 @@ export default function Home() {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[loadProfiles],
|
||||
[loadProfiles, triggerProxyDataReload],
|
||||
);
|
||||
|
||||
const [runningProfiles, setRunningProfiles] = useState<Set<string>>(
|
||||
@@ -607,6 +614,14 @@ export default function Home() {
|
||||
<GoGear className="mr-2 w-4 h-4" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setProxyManagementDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<FiWifi className="mr-2 w-4 h-4" />
|
||||
Proxies
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setImportProfileDialogOpen(true);
|
||||
@@ -645,6 +660,9 @@ export default function Home() {
|
||||
onChangeVersion={openChangeVersionDialog}
|
||||
runningProfiles={runningProfiles}
|
||||
isUpdating={isUpdating}
|
||||
onReloadProxyData={
|
||||
proxyDataReloadTrigger > 0 ? triggerProxyDataReload : undefined
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -655,8 +673,8 @@ export default function Home() {
|
||||
onClose={() => {
|
||||
setProxyDialogOpen(false);
|
||||
}}
|
||||
onSave={(proxy: ProxySettings) => void handleSaveProxy(proxy)}
|
||||
initialSettings={currentProfileForProxy?.proxy}
|
||||
onSave={handleSaveProxy}
|
||||
initialProxyId={currentProfileForProxy?.proxy_id}
|
||||
browserType={currentProfileForProxy?.browser}
|
||||
/>
|
||||
|
||||
@@ -692,6 +710,13 @@ export default function Home() {
|
||||
onImportComplete={() => void loadProfiles()}
|
||||
/>
|
||||
|
||||
<ProxyManagementDialog
|
||||
isOpen={proxyManagementDialogOpen}
|
||||
onClose={() => {
|
||||
setProxyManagementDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{pendingUrls.map((pendingUrl) => (
|
||||
<ProfileSelectorDialog
|
||||
key={pendingUrl.id}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FiPlus } from "react-icons/fi";
|
||||
import { toast } from "sonner";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
||||
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -31,11 +32,7 @@ import {
|
||||
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||
import { useBrowserSupport } from "@/hooks/use-browser-support";
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
import type {
|
||||
BrowserProfile,
|
||||
BrowserReleaseTypes,
|
||||
ProxySettings,
|
||||
} from "@/types";
|
||||
import type { BrowserProfile, BrowserReleaseTypes, StoredProxy } from "@/types";
|
||||
import { Alert, AlertDescription } from "./ui/alert";
|
||||
|
||||
type BrowserTypeString =
|
||||
@@ -55,7 +52,7 @@ interface CreateProfileDialogProps {
|
||||
browserStr: BrowserTypeString;
|
||||
version: string;
|
||||
releaseType: string;
|
||||
proxy?: ProxySettings;
|
||||
proxyId?: string;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -80,13 +77,11 @@ export function CreateProfileDialog({
|
||||
);
|
||||
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
|
||||
|
||||
// Proxy settings
|
||||
const [proxyEnabled, setProxyEnabled] = useState(false);
|
||||
const [proxyType, setProxyType] = useState("http");
|
||||
const [proxyHost, setProxyHost] = useState("");
|
||||
const [proxyPort, setProxyPort] = useState(8080);
|
||||
const [proxyUsername, setProxyUsername] = useState("");
|
||||
const [proxyPassword, setProxyPassword] = useState("");
|
||||
// Proxy settings - now using stored proxy selection
|
||||
const [selectedProxyId, setSelectedProxyId] = useState<string | null>(null);
|
||||
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
|
||||
const [isLoadingProxies, setIsLoadingProxies] = useState(false);
|
||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||
|
||||
const {
|
||||
downloadBrowser,
|
||||
@@ -136,6 +131,19 @@ export function CreateProfileDialog({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadStoredProxies = useCallback(async () => {
|
||||
try {
|
||||
setIsLoadingProxies(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 available proxies");
|
||||
} finally {
|
||||
setIsLoadingProxies(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadReleaseTypes = useCallback(async (browser: string) => {
|
||||
try {
|
||||
setIsLoadingReleaseTypes(true);
|
||||
@@ -191,12 +199,37 @@ export function CreateProfileDialog({
|
||||
// Helper to determine if proxy should be disabled for the selected browser
|
||||
const isProxyDisabled = selectedBrowser === "tor-browser";
|
||||
|
||||
// Update proxy enabled state when browser changes to tor-browser
|
||||
// Update proxy selection when browser changes to tor-browser
|
||||
useEffect(() => {
|
||||
if (selectedBrowser === "tor-browser" && proxyEnabled) {
|
||||
setProxyEnabled(false);
|
||||
if (selectedBrowser === "tor-browser" && selectedProxyId) {
|
||||
setSelectedProxyId(null);
|
||||
}
|
||||
}, [selectedBrowser, proxyEnabled]);
|
||||
}, [selectedBrowser, selectedProxyId]);
|
||||
|
||||
const handleCreateProxy = useCallback(() => {
|
||||
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];
|
||||
}
|
||||
});
|
||||
setSelectedProxyId(savedProxy.id);
|
||||
setShowProxyForm(false);
|
||||
}, []);
|
||||
|
||||
const handleProxyFormClose = useCallback(() => {
|
||||
setShowProxyForm(false);
|
||||
}, []);
|
||||
|
||||
const handleCreate = useCallback(async () => {
|
||||
if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return;
|
||||
@@ -219,34 +252,18 @@ export function CreateProfileDialog({
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const proxy =
|
||||
proxyEnabled && !isProxyDisabled
|
||||
? {
|
||||
enabled: true,
|
||||
proxy_type: proxyType,
|
||||
host: proxyHost,
|
||||
port: proxyPort,
|
||||
username: proxyUsername || undefined,
|
||||
password: proxyPassword || undefined,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await onCreateProfile({
|
||||
name: profileName.trim(),
|
||||
browserStr: selectedBrowser,
|
||||
version,
|
||||
releaseType: selectedReleaseType,
|
||||
proxy,
|
||||
proxyId: isProxyDisabled ? undefined : (selectedProxyId ?? undefined),
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setProfileName("");
|
||||
setSelectedReleaseType(null);
|
||||
setProxyEnabled(false);
|
||||
setProxyHost("");
|
||||
setProxyPort(8080);
|
||||
setProxyUsername("");
|
||||
setProxyPassword("");
|
||||
setSelectedProxyId(null);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Failed to create profile:", error);
|
||||
@@ -258,14 +275,9 @@ export function CreateProfileDialog({
|
||||
selectedBrowser,
|
||||
selectedReleaseType,
|
||||
onCreateProfile,
|
||||
proxyEnabled,
|
||||
isProxyDisabled,
|
||||
selectedProxyId,
|
||||
onClose,
|
||||
proxyHost,
|
||||
proxyPassword,
|
||||
proxyPort,
|
||||
proxyType,
|
||||
proxyUsername,
|
||||
releaseTypes.nightly,
|
||||
releaseTypes.stable,
|
||||
validateProfileName,
|
||||
@@ -286,14 +298,14 @@ export function CreateProfileDialog({
|
||||
selectedReleaseType &&
|
||||
selectedVersion &&
|
||||
isVersionDownloaded(selectedVersion) &&
|
||||
(!proxyEnabled || isProxyDisabled || (proxyHost && proxyPort)) &&
|
||||
!nameError;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
void loadExistingProfiles();
|
||||
void loadStoredProxies();
|
||||
}
|
||||
}, [isOpen, loadExistingProfiles]);
|
||||
}, [isOpen, loadExistingProfiles, loadStoredProxies]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && selectedBrowser) {
|
||||
@@ -305,260 +317,239 @@ export function CreateProfileDialog({
|
||||
}, [isOpen, selectedBrowser, loadDownloadedVersions, loadReleaseTypes]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>Create New Profile</DialogTitle>
|
||||
</DialogHeader>
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>Create New Profile</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid overflow-y-scroll flex-1 gap-6 py-4 min-h-0">
|
||||
{/* Profile Name */}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="profile-name">Profile Name</Label>
|
||||
<Input
|
||||
id="profile-name"
|
||||
value={profileName}
|
||||
onChange={(e) => {
|
||||
setProfileName(e.target.value);
|
||||
}}
|
||||
placeholder="Enter profile name"
|
||||
className={nameError ? "border-red-500" : ""}
|
||||
/>
|
||||
{nameError && <p className="text-sm text-red-600">{nameError}</p>}
|
||||
</div>
|
||||
|
||||
{/* Browser Selection */}
|
||||
<div className="grid gap-2">
|
||||
<Label>Browser</Label>
|
||||
<Select
|
||||
value={selectedBrowser ?? undefined}
|
||||
onValueChange={(value) => {
|
||||
setSelectedBrowser(value as BrowserTypeString);
|
||||
}}
|
||||
disabled={isLoadingSupport}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
isLoadingSupport ? "Loading browsers..." : "Select browser"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(
|
||||
[
|
||||
"mullvad-browser",
|
||||
"firefox",
|
||||
"firefox-developer",
|
||||
"chromium",
|
||||
"brave",
|
||||
"zen",
|
||||
"tor-browser",
|
||||
] as BrowserTypeString[]
|
||||
).map((browser) => {
|
||||
const isSupported = isBrowserSupported(browser);
|
||||
const displayName = getBrowserDisplayName(browser);
|
||||
|
||||
if (!isSupported) {
|
||||
return (
|
||||
<Tooltip key={browser}>
|
||||
<TooltipTrigger asChild>
|
||||
<SelectItem
|
||||
value={browser}
|
||||
disabled={true}
|
||||
className="opacity-50"
|
||||
>
|
||||
{displayName} (Not supported)
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{displayName} is not supported on your current
|
||||
platform or architecture.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectItem key={browser} value={browser}>
|
||||
{displayName}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedBrowser ? (
|
||||
<div className="grid overflow-y-scroll flex-1 gap-6 py-4 min-h-0">
|
||||
{/* Profile Name */}
|
||||
<div className="grid gap-2">
|
||||
<Label>Release Type</Label>
|
||||
{isLoadingReleaseTypes ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading release types...
|
||||
</div>
|
||||
) : Object.keys(releaseTypes).length === 0 ? (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
No releases are available for{" "}
|
||||
{getBrowserDisplayName(selectedBrowser)}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{(!releaseTypes.stable || !releaseTypes.nightly) && (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Only {(releaseTypes.stable && "Stable") ?? "Nightly"}{" "}
|
||||
releases are available for{" "}
|
||||
{getBrowserDisplayName(selectedBrowser)}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Label htmlFor="profile-name">Profile Name</Label>
|
||||
<Input
|
||||
id="profile-name"
|
||||
value={profileName}
|
||||
onChange={(e) => {
|
||||
setProfileName(e.target.value);
|
||||
}}
|
||||
placeholder="Enter profile name"
|
||||
className={nameError ? "border-red-500" : ""}
|
||||
/>
|
||||
{nameError && <p className="text-sm text-red-600">{nameError}</p>}
|
||||
</div>
|
||||
|
||||
{/* Browser Selection */}
|
||||
<div className="grid gap-2">
|
||||
<Label>Browser</Label>
|
||||
<Select
|
||||
value={selectedBrowser ?? undefined}
|
||||
onValueChange={(value) => {
|
||||
setSelectedBrowser(value as BrowserTypeString);
|
||||
}}
|
||||
disabled={isLoadingSupport}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
isLoadingSupport
|
||||
? "Loading browsers..."
|
||||
: "Select browser"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(
|
||||
[
|
||||
"mullvad-browser",
|
||||
"firefox",
|
||||
"firefox-developer",
|
||||
"chromium",
|
||||
"brave",
|
||||
"zen",
|
||||
"tor-browser",
|
||||
] as BrowserTypeString[]
|
||||
).map((browser) => {
|
||||
const isSupported = isBrowserSupported(browser);
|
||||
const displayName = getBrowserDisplayName(browser);
|
||||
|
||||
if (!isSupported) {
|
||||
return (
|
||||
<Tooltip key={browser}>
|
||||
<TooltipTrigger asChild>
|
||||
<SelectItem
|
||||
value={browser}
|
||||
disabled={true}
|
||||
className="opacity-50"
|
||||
>
|
||||
{displayName} (Not supported)
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{displayName} is not supported on your current
|
||||
platform or architecture.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectItem key={browser} value={browser}>
|
||||
{displayName}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedBrowser ? (
|
||||
<div className="grid gap-2">
|
||||
<Label>Release Type</Label>
|
||||
{isLoadingReleaseTypes ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading release types...
|
||||
</div>
|
||||
) : Object.keys(releaseTypes).length === 0 ? (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
No releases are available for{" "}
|
||||
{getBrowserDisplayName(selectedBrowser)}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{(!releaseTypes.stable || !releaseTypes.nightly) && (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
Only {(releaseTypes.stable && "Stable") ?? "Nightly"}{" "}
|
||||
releases are available for{" "}
|
||||
{getBrowserDisplayName(selectedBrowser)}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<ReleaseTypeSelector
|
||||
selectedReleaseType={selectedReleaseType}
|
||||
onReleaseTypeSelect={setSelectedReleaseType}
|
||||
availableReleaseTypes={releaseTypes}
|
||||
browser={selectedBrowser}
|
||||
isDownloading={isDownloading}
|
||||
onDownload={() => {
|
||||
void handleDownload();
|
||||
}}
|
||||
placeholder="Select release type..."
|
||||
downloadedVersions={downloadedVersions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Proxy Settings */}
|
||||
<div className="grid gap-4 pt-4 border-t">
|
||||
<div className="grid gap-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label>Proxy Settings</Label>
|
||||
{!isProxyDisabled && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateProxy}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<FiPlus className="w-4 h-4" />
|
||||
Create Proxy
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Create a new proxy configuration</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<ReleaseTypeSelector
|
||||
selectedReleaseType={selectedReleaseType}
|
||||
onReleaseTypeSelect={setSelectedReleaseType}
|
||||
availableReleaseTypes={releaseTypes}
|
||||
browser={selectedBrowser}
|
||||
isDownloading={isDownloading}
|
||||
onDownload={() => {
|
||||
void handleDownload();
|
||||
}}
|
||||
placeholder="Select release type..."
|
||||
downloadedVersions={downloadedVersions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Proxy Settings */}
|
||||
<div className="grid gap-4 pt-4 border-t">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isProxyDisabled ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center space-x-2 opacity-50">
|
||||
<Checkbox
|
||||
id="proxy-enabled"
|
||||
checked={false}
|
||||
disabled={true}
|
||||
/>
|
||||
<Label htmlFor="proxy-enabled" className="text-gray-500">
|
||||
Enable Proxy
|
||||
</Label>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Tor Browser has its own built-in proxy system and
|
||||
doesn't support additional proxy configuration
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<>
|
||||
<Checkbox
|
||||
id="proxy-enabled"
|
||||
checked={proxyEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setProxyEnabled(checked as boolean);
|
||||
{isProxyDisabled ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="p-3 bg-yellow-50 rounded-md border border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
Tor Browser has its own built-in proxy system and
|
||||
doesn't support additional proxy configuration.
|
||||
</p>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Tor Browser manages its own proxy routing automatically
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Select
|
||||
value={selectedProxyId ?? "none"}
|
||||
onValueChange={(value) => {
|
||||
setSelectedProxyId(value === "none" ? null : value);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="proxy-enabled">Enable Proxy</Label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{proxyEnabled && !isProxyDisabled && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label>Proxy Type</Label>
|
||||
<Select value={proxyType} onValueChange={setProxyType}>
|
||||
disabled={isLoadingProxies}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
<SelectValue
|
||||
placeholder={
|
||||
isLoadingProxies
|
||||
? "Loading proxies..."
|
||||
: "Select proxy (optional)"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{["http", "https", "socks4", "socks5"].map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type.toUpperCase()}
|
||||
<SelectItem value="none">No Proxy</SelectItem>
|
||||
{storedProxies.map((proxy) => (
|
||||
<SelectItem key={proxy.id} value={proxy.id}>
|
||||
{proxy.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-host">Host</Label>
|
||||
<Input
|
||||
id="proxy-host"
|
||||
value={proxyHost}
|
||||
onChange={(e) => {
|
||||
setProxyHost(e.target.value);
|
||||
}}
|
||||
placeholder="e.g. 127.0.0.1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-port">Port</Label>
|
||||
<Input
|
||||
id="proxy-port"
|
||||
type="number"
|
||||
value={proxyPort}
|
||||
onChange={(e) => {
|
||||
setProxyPort(Number.parseInt(e.target.value, 10) || 0);
|
||||
}}
|
||||
placeholder="e.g. 8080"
|
||||
min="1"
|
||||
max="65535"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-username">Username (optional)</Label>
|
||||
<Input
|
||||
id="proxy-username"
|
||||
value={proxyUsername}
|
||||
onChange={(e) => {
|
||||
setProxyUsername(e.target.value);
|
||||
}}
|
||||
placeholder="Proxy username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-password">Password (optional)</Label>
|
||||
<Input
|
||||
id="proxy-password"
|
||||
type="password"
|
||||
value={proxyPassword}
|
||||
onChange={(e) => {
|
||||
setProxyPassword(e.target.value);
|
||||
}}
|
||||
placeholder="Proxy password"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isProxyDisabled &&
|
||||
storedProxies.length === 0 &&
|
||||
!isLoadingProxies && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No saved proxies available. Use the "Create Proxy" button
|
||||
above to create proxy configurations.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
isLoading={isCreating}
|
||||
onClick={() => void handleCreate()}
|
||||
disabled={!canCreate}
|
||||
>
|
||||
Create Profile
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
isLoading={isCreating}
|
||||
onClick={() => void handleCreate()}
|
||||
disabled={!canCreate}
|
||||
>
|
||||
Create Profile
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<ProxyFormDialog
|
||||
isOpen={showProxyForm}
|
||||
onClose={handleProxyFormClose}
|
||||
onSave={handleProxySaved}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import * as React from "react";
|
||||
import { CiCircleCheck } from "react-icons/ci";
|
||||
import { IoEllipsisHorizontal } from "react-icons/io5";
|
||||
@@ -44,7 +45,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useTableSorting } from "@/hooks/use-table-sorting";
|
||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||
import type { BrowserProfile } from "@/types";
|
||||
import type { BrowserProfile, StoredProxy } from "@/types";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
|
||||
@@ -58,6 +59,7 @@ interface ProfilesDataTableProps {
|
||||
onChangeVersion: (profile: BrowserProfile) => void;
|
||||
runningProfiles: Set<string>;
|
||||
isUpdating?: (browser: string) => boolean;
|
||||
onReloadProxyData?: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
export function ProfilesDataTable({
|
||||
@@ -70,6 +72,7 @@ export function ProfilesDataTable({
|
||||
onChangeVersion,
|
||||
runningProfiles,
|
||||
isUpdating = () => false,
|
||||
onReloadProxyData,
|
||||
}: ProfilesDataTableProps) {
|
||||
const { getTableSorting, updateSorting, isLoaded } = useTableSorting();
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
@@ -83,12 +86,65 @@ export function ProfilesDataTable({
|
||||
React.useState("");
|
||||
const [deleteError, setDeleteError] = React.useState<string | null>(null);
|
||||
const [isClient, setIsClient] = React.useState(false);
|
||||
const [storedProxies, setStoredProxies] = React.useState<StoredProxy[]>([]);
|
||||
|
||||
// Helper function to check if a profile has a proxy
|
||||
const hasProxy = React.useCallback(
|
||||
(profile: BrowserProfile): boolean => {
|
||||
if (!profile.proxy_id) return false;
|
||||
const proxy = storedProxies.find((p) => p.id === profile.proxy_id);
|
||||
return proxy !== undefined;
|
||||
},
|
||||
[storedProxies],
|
||||
);
|
||||
|
||||
// Helper function to get proxy info for a profile
|
||||
const getProxyInfo = React.useCallback(
|
||||
(profile: BrowserProfile): StoredProxy | null => {
|
||||
if (!profile.proxy_id) return null;
|
||||
return storedProxies.find((p) => p.id === profile.proxy_id) ?? null;
|
||||
},
|
||||
[storedProxies],
|
||||
);
|
||||
|
||||
// Helper function to get proxy name for display
|
||||
const getProxyDisplayName = React.useCallback(
|
||||
(profile: BrowserProfile): string => {
|
||||
if (!profile.proxy_id) return "Disabled";
|
||||
const proxy = storedProxies.find((p) => p.id === profile.proxy_id);
|
||||
return proxy?.name ?? "Unknown Proxy";
|
||||
},
|
||||
[storedProxies],
|
||||
);
|
||||
|
||||
// Ensure we're on the client side to prevent hydration mismatches
|
||||
React.useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isClient) {
|
||||
void loadStoredProxies();
|
||||
}
|
||||
}, [isClient, loadStoredProxies]);
|
||||
|
||||
// Reload proxy data when requested from parent
|
||||
React.useEffect(() => {
|
||||
if (onReloadProxyData) {
|
||||
void loadStoredProxies();
|
||||
}
|
||||
}, [onReloadProxyData, loadStoredProxies]);
|
||||
|
||||
// Update local sorting state when settings are loaded
|
||||
React.useEffect(() => {
|
||||
if (isLoaded && isClient) {
|
||||
@@ -320,32 +376,41 @@ export function ProfilesDataTable({
|
||||
header: "Proxy",
|
||||
cell: ({ row }) => {
|
||||
const profile = row.original;
|
||||
const hasProxy = profile.proxy?.enabled;
|
||||
const regularText = hasProxy ? profile.proxy?.proxy_type : "Disabled";
|
||||
const regularTooltipText = hasProxy
|
||||
? `${profile.proxy?.proxy_type.toUpperCase()} proxy enabled (${
|
||||
profile.proxy?.host
|
||||
}:${profile.proxy?.port})`
|
||||
: "No proxy configured";
|
||||
const profileHasProxy = hasProxy(profile);
|
||||
const proxyDisplayName = getProxyDisplayName(profile);
|
||||
const proxyInfo = getProxyInfo(profile);
|
||||
|
||||
const tooltipText =
|
||||
profile.browser === "tor-browser"
|
||||
? "Proxies are not supported for TOR browser"
|
||||
: profileHasProxy && proxyInfo
|
||||
? `${proxyDisplayName}, ${proxyInfo.proxy_settings.proxy_type.toUpperCase()} (${
|
||||
proxyInfo.proxy_settings.host
|
||||
}:${proxyInfo.proxy_settings.port})`
|
||||
: "No proxy configured";
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex gap-2 items-center">
|
||||
{hasProxy && (
|
||||
{profileHasProxy && (
|
||||
<CiCircleCheck className="w-4 h-4 text-green-500" />
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{profile.browser === "tor-browser"
|
||||
? "Not supported"
|
||||
: regularText}
|
||||
</span>
|
||||
|
||||
{proxyDisplayName.length > 10 ? (
|
||||
<span className="text-sm truncate text-muted-foreground">
|
||||
{proxyDisplayName.slice(0, 10)}...
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{profile.browser === "tor-browser"
|
||||
? "Not supported"
|
||||
: proxyDisplayName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{profile.browser === "tor-browser"
|
||||
? "Proxies are not supported for TOR browser"
|
||||
: regularTooltipText}
|
||||
</TooltipContent>
|
||||
<TooltipContent>{tooltipText}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
@@ -426,6 +491,9 @@ export function ProfilesDataTable({
|
||||
onKillProfile,
|
||||
onProxySettings,
|
||||
onChangeVersion,
|
||||
getProxyInfo,
|
||||
hasProxy,
|
||||
getProxyDisplayName,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||
import type { BrowserProfile } from "@/types";
|
||||
import type { BrowserProfile, StoredProxy } from "@/types";
|
||||
|
||||
interface ProfileSelectorDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -47,6 +47,17 @@ export function ProfileSelectorDialog({
|
||||
const [selectedProfile, setSelectedProfile] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLaunching, setIsLaunching] = useState(false);
|
||||
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
|
||||
|
||||
// Helper function to check if a profile has a proxy
|
||||
const hasProxy = useCallback(
|
||||
(profile: BrowserProfile): boolean => {
|
||||
if (!profile.proxy_id) return false;
|
||||
const proxy = storedProxies.find((p) => p.id === profile.proxy_id);
|
||||
return proxy !== undefined;
|
||||
},
|
||||
[storedProxies],
|
||||
);
|
||||
|
||||
// Helper function to determine if a profile can be used for opening links
|
||||
const canUseProfileForLinks = useCallback(
|
||||
@@ -86,15 +97,18 @@ export function ProfileSelectorDialog({
|
||||
const loadProfiles = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const profileList = await invoke<BrowserProfile[]>(
|
||||
"list_browser_profiles",
|
||||
);
|
||||
// Load both profiles and stored proxies
|
||||
const [profileList, proxiesList] = await Promise.all([
|
||||
invoke<BrowserProfile[]>("list_browser_profiles"),
|
||||
invoke<StoredProxy[]>("get_stored_proxies"),
|
||||
]);
|
||||
|
||||
// Sort profiles by name
|
||||
profileList.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Don't filter any profiles, show all of them
|
||||
// Set both profiles and proxies
|
||||
setProfiles(profileList);
|
||||
setStoredProxies(proxiesList);
|
||||
|
||||
// Auto-select first available profile for link opening
|
||||
if (profileList.length > 0) {
|
||||
@@ -305,7 +319,7 @@ export function ProfileSelectorDialog({
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{getBrowserDisplayName(profile.browser)}
|
||||
</Badge>
|
||||
{profile.proxy?.enabled && (
|
||||
{hasProxy(profile) && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Proxy
|
||||
</Badge>
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { StoredProxy } from "@/types";
|
||||
|
||||
interface ProxyFormData {
|
||||
name: string;
|
||||
proxy_type: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
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);
|
||||
const [formData, setFormData] = useState<ProxyFormData>({
|
||||
name: "",
|
||||
proxy_type: "http",
|
||||
host: "",
|
||||
port: 8080,
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setFormData({
|
||||
name: "",
|
||||
proxy_type: "http",
|
||||
host: "",
|
||||
port: 8080,
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Load editing proxy data when dialog opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (editingProxy) {
|
||||
setFormData({
|
||||
name: editingProxy.name,
|
||||
proxy_type: editingProxy.proxy_settings.proxy_type,
|
||||
host: editingProxy.proxy_settings.host,
|
||||
port: editingProxy.proxy_settings.port,
|
||||
username: editingProxy.proxy_settings.username || "",
|
||||
password: editingProxy.proxy_settings.password || "",
|
||||
});
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
}, [isOpen, editingProxy, resetForm]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (!formData.name.trim()) {
|
||||
toast.error("Proxy name is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.host.trim() || !formData.port) {
|
||||
toast.error("Host and port are required");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const proxySettings = {
|
||||
proxy_type: formData.proxy_type,
|
||||
host: formData.host.trim(),
|
||||
port: formData.port,
|
||||
username: formData.username.trim() || undefined,
|
||||
password: formData.password.trim() || undefined,
|
||||
};
|
||||
|
||||
let savedProxy: StoredProxy;
|
||||
|
||||
if (editingProxy) {
|
||||
// Update existing proxy
|
||||
savedProxy = await invoke<StoredProxy>("update_stored_proxy", {
|
||||
proxyId: editingProxy.id,
|
||||
name: formData.name.trim(),
|
||||
proxySettings,
|
||||
});
|
||||
toast.success("Proxy updated successfully");
|
||||
} else {
|
||||
// Create new proxy
|
||||
savedProxy = await invoke<StoredProxy>("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);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(`Failed to save proxy: ${errorMessage}`);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [formData, editingProxy, onSave, onClose]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (!isSubmitting) {
|
||||
onClose();
|
||||
}
|
||||
}, [isSubmitting, onClose]);
|
||||
|
||||
const isFormValid =
|
||||
formData.name.trim() &&
|
||||
formData.host.trim() &&
|
||||
formData.port > 0 &&
|
||||
formData.port <= 65535;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingProxy ? "Edit Proxy" : "Create New Proxy"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-name">Proxy Name</Label>
|
||||
<Input
|
||||
id="proxy-name"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, name: e.target.value })
|
||||
}
|
||||
placeholder="e.g. Office Proxy, Home VPN, etc."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Proxy Type</Label>
|
||||
<Select
|
||||
value={formData.proxy_type}
|
||||
onValueChange={(value) =>
|
||||
setFormData({ ...formData, proxy_type: value })
|
||||
}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select proxy type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{["http", "https", "socks4", "socks5"].map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type.toUpperCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-host">Host</Label>
|
||||
<Input
|
||||
id="proxy-host"
|
||||
value={formData.host}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, host: e.target.value })
|
||||
}
|
||||
placeholder="e.g. 127.0.0.1"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-port">Port</Label>
|
||||
<Input
|
||||
id="proxy-port"
|
||||
type="number"
|
||||
value={formData.port}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
port: parseInt(e.target.value, 10) || 0,
|
||||
})
|
||||
}
|
||||
placeholder="e.g. 8080"
|
||||
min="1"
|
||||
max="65535"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-username">Username (optional)</Label>
|
||||
<Input
|
||||
id="proxy-username"
|
||||
value={formData.username}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
username: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Proxy username"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-password">Password (optional)</Label>
|
||||
<Input
|
||||
id="proxy-password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
password: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Proxy password"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
isLoading={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
disabled={!isFormValid}
|
||||
>
|
||||
{editingProxy ? "Update Proxy" : "Create Proxy"}
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
"use client";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FiEdit2, FiPlus, FiTrash2, FiWifi } from "react-icons/fi";
|
||||
import { toast } from "sonner";
|
||||
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { StoredProxy } from "@/types";
|
||||
|
||||
interface ProxyManagementDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function ProxyManagementDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: ProxyManagementDialogProps) {
|
||||
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||
const [editingProxy, setEditingProxy] = useState<StoredProxy | null>(null);
|
||||
|
||||
const loadStoredProxies = useCallback(async () => {
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadStoredProxies();
|
||||
}
|
||||
}, [isOpen, loadStoredProxies]);
|
||||
|
||||
const handleDeleteProxy = useCallback(async (proxy: StoredProxy) => {
|
||||
if (
|
||||
!confirm(`Are you sure you want to delete the proxy "${proxy.name}"?`)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await invoke("delete_stored_proxy", { proxyId: proxy.id });
|
||||
setStoredProxies((prev) => prev.filter((p) => p.id !== proxy.id));
|
||||
toast.success("Proxy deleted successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to delete proxy:", error);
|
||||
toast.error("Failed to delete proxy");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCreateProxy = useCallback(() => {
|
||||
setEditingProxy(null);
|
||||
setShowProxyForm(true);
|
||||
}, []);
|
||||
|
||||
const handleEditProxy = useCallback((proxy: StoredProxy) => {
|
||||
setEditingProxy(proxy);
|
||||
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);
|
||||
}, []);
|
||||
|
||||
const handleProxyFormClose = useCallback(() => {
|
||||
setShowProxyForm(false);
|
||||
setEditingProxy(null);
|
||||
}, []);
|
||||
|
||||
const trimName = useCallback((name: string) => {
|
||||
return name.length > 30 ? `${name.substring(0, 30)}...` : name;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<div className="flex gap-2 items-center">
|
||||
<FiWifi className="w-5 h-5" />
|
||||
<DialogTitle>Proxy Management</DialogTitle>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col flex-1 gap-4 py-4 min-h-0">
|
||||
{/* Header with Create Button */}
|
||||
<div className="flex flex-shrink-0 justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Stored Proxies</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage your saved proxy configurations for reuse across
|
||||
profiles
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleCreateProxy}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<FiPlus className="w-4 h-4" />
|
||||
Create Proxy
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Proxy List - Scrollable */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-32">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Loading proxies...
|
||||
</p>
|
||||
</div>
|
||||
) : storedProxies.length === 0 ? (
|
||||
<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" />
|
||||
<p className="mb-2 text-muted-foreground">
|
||||
No proxies configured
|
||||
</p>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Create your first proxy configuration to get started
|
||||
</p>
|
||||
<Button variant="outline" onClick={handleCreateProxy}>
|
||||
<FiPlus className="mr-2 w-4 h-4" />
|
||||
Create First Proxy
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-y-auto pr-2 space-y-2 h-full">
|
||||
{storedProxies.map((proxy) => (
|
||||
<div
|
||||
key={proxy.id}
|
||||
className="flex justify-between items-center p-1 rounded border bg-card"
|
||||
>
|
||||
<div className="flex-1 ml-2 min-w-0">
|
||||
{proxy.name.length > 30 ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="block font-medium truncate text-card-foreground">
|
||||
{trimName(proxy.name)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<span className="text-sm font-medium text-card-foreground">
|
||||
{proxy.name}
|
||||
</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="text-sm font-medium text-card-foreground">
|
||||
{proxy.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 gap-1 items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditProxy(proxy)}
|
||||
>
|
||||
<FiEdit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Edit proxy</p>
|
||||
</TooltipContent>
|
||||
</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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete proxy</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-shrink-0">
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<ProxyFormDialog
|
||||
isOpen={showProxyForm}
|
||||
onClose={handleProxyFormClose}
|
||||
onSave={handleProxySaved}
|
||||
editingProxy={editingProxy}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FiPlus } from "react-icons/fi";
|
||||
import { toast } from "sonner";
|
||||
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -10,35 +15,20 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
interface ProxySettings {
|
||||
enabled: boolean;
|
||||
proxy_type: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { StoredProxy } from "@/types";
|
||||
|
||||
interface ProxySettingsDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (proxySettings: ProxySettings) => void;
|
||||
initialSettings?: ProxySettings;
|
||||
onSave: (proxyId: string | null) => void;
|
||||
initialProxyId?: string | null;
|
||||
browserType?: string;
|
||||
}
|
||||
|
||||
@@ -46,232 +36,245 @@ export function ProxySettingsDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
initialSettings,
|
||||
initialProxyId,
|
||||
browserType,
|
||||
}: ProxySettingsDialogProps) {
|
||||
const [settings, setSettings] = useState<ProxySettings>({
|
||||
enabled: initialSettings?.enabled ?? false,
|
||||
proxy_type: initialSettings?.proxy_type ?? "http",
|
||||
host: initialSettings?.host ?? "",
|
||||
port: initialSettings?.port ?? 8080,
|
||||
username: initialSettings?.username ?? "",
|
||||
password: initialSettings?.password ?? "",
|
||||
});
|
||||
|
||||
const [initialSettingsState, setInitialSettingsState] =
|
||||
useState<ProxySettings>({
|
||||
enabled: false,
|
||||
proxy_type: "http",
|
||||
host: "",
|
||||
port: 8080,
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && initialSettings) {
|
||||
const newSettings = {
|
||||
enabled: initialSettings.enabled,
|
||||
proxy_type: initialSettings.proxy_type,
|
||||
host: initialSettings.host,
|
||||
port: initialSettings.port,
|
||||
username: initialSettings.username ?? "",
|
||||
password: initialSettings.password ?? "",
|
||||
};
|
||||
setSettings(newSettings);
|
||||
setInitialSettingsState(newSettings);
|
||||
} else if (isOpen) {
|
||||
const defaultSettings = {
|
||||
enabled: false,
|
||||
proxy_type: "http",
|
||||
host: "",
|
||||
port: 80,
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
setSettings(defaultSettings);
|
||||
setInitialSettingsState(defaultSettings);
|
||||
}
|
||||
}, [isOpen, initialSettings]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSave(settings);
|
||||
};
|
||||
|
||||
// Check if settings have changed
|
||||
const hasChanged = () => {
|
||||
return (
|
||||
settings.enabled !== initialSettingsState.enabled ||
|
||||
settings.proxy_type !== initialSettingsState.proxy_type ||
|
||||
settings.host !== initialSettingsState.host ||
|
||||
settings.port !== initialSettingsState.port ||
|
||||
settings.username !== initialSettingsState.username ||
|
||||
settings.password !== initialSettingsState.password
|
||||
);
|
||||
};
|
||||
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
|
||||
const [selectedProxyId, setSelectedProxyId] = useState<string | null>(
|
||||
initialProxyId || null,
|
||||
);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||
|
||||
// Helper to determine if proxy should be disabled for the selected browser
|
||||
const isProxyDisabled = browserType === "tor-browser";
|
||||
|
||||
// Update proxy enabled state when browser is tor-browser
|
||||
useEffect(() => {
|
||||
if (browserType === "tor-browser" && settings.enabled) {
|
||||
setSettings((prev) => ({ ...prev, enabled: false }));
|
||||
const loadStoredProxies = useCallback(async () => {
|
||||
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);
|
||||
}
|
||||
}, [browserType, settings.enabled]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadStoredProxies();
|
||||
if (isProxyDisabled) {
|
||||
setSelectedProxyId(null);
|
||||
}
|
||||
}
|
||||
}, [isOpen, isProxyDisabled, loadStoredProxies]);
|
||||
|
||||
const handleCreateProxy = useCallback(() => {
|
||||
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];
|
||||
}
|
||||
});
|
||||
setSelectedProxyId(savedProxy.id);
|
||||
setShowProxyForm(false);
|
||||
}, []);
|
||||
|
||||
const handleProxyFormClose = useCallback(() => {
|
||||
setShowProxyForm(false);
|
||||
}, []);
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(selectedProxyId);
|
||||
};
|
||||
|
||||
const hasChanged = () => {
|
||||
return selectedProxyId !== initialProxyId;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Proxy Settings</DialogTitle>
|
||||
</DialogHeader>
|
||||
<>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>Proxy Settings</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isProxyDisabled ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center space-x-2 opacity-50">
|
||||
<Checkbox
|
||||
id="proxy-enabled"
|
||||
checked={false}
|
||||
disabled={true}
|
||||
/>
|
||||
<Label htmlFor="proxy-enabled" className="text-gray-500">
|
||||
Enable Proxy
|
||||
</Label>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Tor Browser has its own built-in proxy system and
|
||||
doesn't support additional proxy configuration
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="grid gap-6 py-4">
|
||||
{isProxyDisabled && (
|
||||
<div className="p-4 bg-yellow-50 rounded-md border border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
Tor Browser has its own built-in proxy system and doesn't
|
||||
support additional proxy configuration.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isProxyDisabled && (
|
||||
<>
|
||||
<Checkbox
|
||||
id="proxy-enabled"
|
||||
checked={settings.enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setSettings({ ...settings, enabled: checked as boolean });
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="proxy-enabled">Enable Proxy</Label>
|
||||
{/* Proxy Selection */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label className="text-base font-medium">
|
||||
Select Proxy
|
||||
</Label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateProxy}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
<FiPlus className="w-4 h-4" />
|
||||
Create New
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Create a new proxy configuration</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto p-2 space-y-2 h-full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setSelectedProxyId(null)}
|
||||
asChild
|
||||
>
|
||||
<Card
|
||||
className={cn(
|
||||
"w-full bg-card cursor-pointer transition-colors",
|
||||
selectedProxyId === null
|
||||
? "ring-2 ring-blue-500"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4 w-full">
|
||||
<div className="flex items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
id="no-proxy"
|
||||
name="proxy-selection"
|
||||
checked={selectedProxyId === null}
|
||||
onChange={() => setSelectedProxyId(null)}
|
||||
/>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Label
|
||||
htmlFor="no-proxy"
|
||||
className="font-medium cursor-pointer"
|
||||
>
|
||||
No Proxy
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Button>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Loading proxies...
|
||||
</p>
|
||||
) : (
|
||||
storedProxies.map((proxy) => (
|
||||
<Button
|
||||
key={proxy.id}
|
||||
variant="ghost"
|
||||
onClick={() => setSelectedProxyId(proxy.id)}
|
||||
asChild
|
||||
>
|
||||
<Card
|
||||
className={cn(
|
||||
"w-full bg-card cursor-pointer transition-colors",
|
||||
selectedProxyId === proxy.id
|
||||
? "ring-2 ring-blue-500"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-4 w-full">
|
||||
<div className="flex items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
id={`proxy-${proxy.id}`}
|
||||
name="proxy-selection"
|
||||
checked={selectedProxyId === proxy.id}
|
||||
onChange={() => setSelectedProxyId(proxy.id)}
|
||||
/>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Label
|
||||
htmlFor={`proxy-${proxy.id}`}
|
||||
className="font-medium cursor-pointer"
|
||||
>
|
||||
{proxy.name}
|
||||
</Label>
|
||||
<Badge variant="outline">
|
||||
{proxy.proxy_settings.proxy_type.toUpperCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
|
||||
{!loading && storedProxies.length === 0 && (
|
||||
<div className="py-4 text-center">
|
||||
<p className="mb-2 text-sm text-muted-foreground">
|
||||
No saved proxies available.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateProxy}
|
||||
>
|
||||
<FiPlus className="mr-2 w-4 h-4" />
|
||||
Create First Proxy
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{settings.enabled && !isProxyDisabled && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label>Proxy Type</Label>
|
||||
<Select
|
||||
value={settings.proxy_type}
|
||||
onValueChange={(value) => {
|
||||
setSettings({
|
||||
...settings,
|
||||
proxy_type: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select proxy type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{["http", "https", "socks4", "socks5"].map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type.toUpperCase()}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!hasChanged()}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="host">Host</Label>
|
||||
<Input
|
||||
id="host"
|
||||
value={settings.host}
|
||||
onChange={(e) => {
|
||||
setSettings({ ...settings, host: e.target.value });
|
||||
}}
|
||||
placeholder="e.g. 127.0.0.1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="port">Port</Label>
|
||||
<Input
|
||||
id="port"
|
||||
type="number"
|
||||
value={settings.port}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
...settings,
|
||||
port: Number.parseInt(e.target.value, 10) || 0,
|
||||
});
|
||||
}}
|
||||
placeholder="e.g. 8080"
|
||||
min="1"
|
||||
max="65535"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="username">Username (optional)</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={settings.username}
|
||||
onChange={(e) => {
|
||||
setSettings({ ...settings, username: e.target.value });
|
||||
}}
|
||||
placeholder="Proxy username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password (optional)</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={settings.password}
|
||||
onChange={(e) => {
|
||||
setSettings({ ...settings, password: e.target.value });
|
||||
}}
|
||||
placeholder="Proxy password"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={
|
||||
!hasChanged() ||
|
||||
(!isProxyDisabled &&
|
||||
settings.enabled &&
|
||||
(!settings.host || !settings.port))
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ProxyFormDialog
|
||||
isOpen={showProxyForm}
|
||||
onClose={handleProxyFormClose}
|
||||
onSave={handleProxySaved}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-3
@@ -1,5 +1,4 @@
|
||||
export interface ProxySettings {
|
||||
enabled: boolean;
|
||||
proxy_type: string; // "http", "https", "socks4", or "socks5"
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -13,16 +12,22 @@ export interface TableSortingSettings {
|
||||
}
|
||||
|
||||
export interface BrowserProfile {
|
||||
id: string; // UUID of the profile
|
||||
name: string;
|
||||
browser: string;
|
||||
version: string;
|
||||
profile_path: string;
|
||||
proxy?: ProxySettings;
|
||||
proxy_id?: string; // Reference to stored proxy
|
||||
process_id?: number;
|
||||
last_launch?: number;
|
||||
release_type: string; // "stable" or "nightly"
|
||||
}
|
||||
|
||||
export interface StoredProxy {
|
||||
id: string;
|
||||
name: string;
|
||||
proxy_settings: ProxySettings;
|
||||
}
|
||||
|
||||
export interface DetectedProfile {
|
||||
browser: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user