mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-30 07:37:51 +02:00
refactor: dynamic proxy
This commit is contained in:
@@ -516,6 +516,7 @@ export default function Home() {
|
||||
extensionGroupId?: string;
|
||||
ephemeral?: boolean;
|
||||
dnsBlocklist?: string;
|
||||
launchHook?: string;
|
||||
}) => {
|
||||
try {
|
||||
const profile = await invoke<BrowserProfile>(
|
||||
@@ -534,6 +535,7 @@ export default function Home() {
|
||||
(selectedGroupId !== "default" ? selectedGroupId : undefined),
|
||||
ephemeral: profileData.ephemeral,
|
||||
dnsBlocklist: profileData.dnsBlocklist,
|
||||
launchHook: profileData.launchHook,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ interface CreateProfileDialogProps {
|
||||
extensionGroupId?: string;
|
||||
ephemeral?: boolean;
|
||||
dnsBlocklist?: string;
|
||||
launchHook?: string;
|
||||
}) => Promise<void>;
|
||||
selectedGroupId?: string;
|
||||
crossOsUnlocked?: boolean;
|
||||
@@ -126,6 +127,7 @@ export function CreateProfileDialog({
|
||||
const [selectedProxyId, setSelectedProxyId] = useState<string>();
|
||||
const [proxyPopoverOpen, setProxyPopoverOpen] = useState(false);
|
||||
const [dnsBlocklist, setDnsBlocklist] = useState<string>("");
|
||||
const [launchHook, setLaunchHook] = useState("");
|
||||
|
||||
// Camoufox anti-detect states
|
||||
const [camoufoxConfig, setCamoufoxConfig] = useState<CamoufoxConfig>(() => ({
|
||||
@@ -150,6 +152,7 @@ export function CreateProfileDialog({
|
||||
setSelectedBrowser(null);
|
||||
setProfileName("");
|
||||
setSelectedProxyId(undefined);
|
||||
setLaunchHook("");
|
||||
};
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
@@ -158,6 +161,7 @@ export function CreateProfileDialog({
|
||||
setSelectedBrowser(null);
|
||||
setProfileName("");
|
||||
setSelectedProxyId(undefined);
|
||||
setLaunchHook("");
|
||||
};
|
||||
|
||||
const [supportedBrowsers, setSupportedBrowsers] = useState<string[]>([]);
|
||||
@@ -398,6 +402,7 @@ export function CreateProfileDialog({
|
||||
extensionGroupId: selectedExtensionGroupId,
|
||||
ephemeral,
|
||||
dnsBlocklist: dnsBlocklist || undefined,
|
||||
launchHook: launchHook.trim() || undefined,
|
||||
});
|
||||
} else {
|
||||
// Default to Camoufox
|
||||
@@ -424,6 +429,7 @@ export function CreateProfileDialog({
|
||||
extensionGroupId: selectedExtensionGroupId,
|
||||
ephemeral,
|
||||
dnsBlocklist: dnsBlocklist || undefined,
|
||||
launchHook: launchHook.trim() || undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -448,6 +454,7 @@ export function CreateProfileDialog({
|
||||
proxyId: selectedProxyId,
|
||||
groupId: selectedGroupId !== "default" ? selectedGroupId : undefined,
|
||||
dnsBlocklist: dnsBlocklist || undefined,
|
||||
launchHook: launchHook.trim() || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -469,6 +476,7 @@ export function CreateProfileDialog({
|
||||
setActiveTab("anti-detect");
|
||||
setSelectedBrowser(null);
|
||||
setSelectedProxyId(undefined);
|
||||
setLaunchHook("");
|
||||
setReleaseTypes({});
|
||||
setIsLoadingReleaseTypes(false);
|
||||
setReleaseTypesError(null);
|
||||
@@ -1167,6 +1175,23 @@ export function CreateProfileDialog({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="launch-hook-url">
|
||||
{t("createProfile.launchHook.label")}
|
||||
</Label>
|
||||
<Input
|
||||
id="launch-hook-url"
|
||||
value={launchHook}
|
||||
onChange={(e) => {
|
||||
setLaunchHook(e.target.value);
|
||||
}}
|
||||
placeholder={t(
|
||||
"createProfile.launchHook.placeholder",
|
||||
)}
|
||||
disabled={isCreating}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* DNS Blocklist */}
|
||||
<div className="space-y-2">
|
||||
<Label>{t("dnsBlocklist.title")}</Label>
|
||||
@@ -1498,6 +1523,23 @@ export function CreateProfileDialog({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="launch-hook-url-regular">
|
||||
{t("createProfile.launchHook.label")}
|
||||
</Label>
|
||||
<Input
|
||||
id="launch-hook-url-regular"
|
||||
value={launchHook}
|
||||
onChange={(e) => {
|
||||
setLaunchHook(e.target.value);
|
||||
}}
|
||||
placeholder={t(
|
||||
"createProfile.launchHook.placeholder",
|
||||
)}
|
||||
disabled={isCreating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</>
|
||||
|
||||
@@ -128,6 +128,8 @@ export function ProfileInfoDialog({
|
||||
const [extensionGroupName, setExtensionGroupName] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [launchHookValue, setLaunchHookValue] = React.useState("");
|
||||
const [isSavingLaunchHook, setIsSavingLaunchHook] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isOpen || !profile?.group_id) {
|
||||
@@ -169,6 +171,12 @@ export function ProfileInfoDialog({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
setLaunchHookValue(profile?.launch_hook ?? "");
|
||||
}
|
||||
}, [isOpen, profile?.launch_hook]);
|
||||
|
||||
if (!profile) return null;
|
||||
|
||||
const ProfileIcon = getProfileIcon(profile);
|
||||
@@ -217,6 +225,22 @@ export function ProfileInfoDialog({
|
||||
const hasTags = profile.tags && profile.tags.length > 0;
|
||||
const hasNote = !!profile.note;
|
||||
const showCrossOs = isCrossOsProfile(profile);
|
||||
const trimmedLaunchHook = launchHookValue.trim();
|
||||
const savedLaunchHook = profile.launch_hook ?? "";
|
||||
|
||||
const handleSaveLaunchHook = async () => {
|
||||
setIsSavingLaunchHook(true);
|
||||
try {
|
||||
await invoke("update_profile_launch_hook", {
|
||||
profileId: profile.id,
|
||||
launchHook: trimmedLaunchHook || null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to update launch hook:", error);
|
||||
} finally {
|
||||
setIsSavingLaunchHook(false);
|
||||
}
|
||||
};
|
||||
|
||||
interface ActionItem {
|
||||
icon: React.ReactNode;
|
||||
@@ -474,6 +498,10 @@ export function ProfileInfoDialog({
|
||||
: t("dnsBlocklist.none")
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
label={t("profileInfo.fields.launchHook")}
|
||||
value={profile.launch_hook || t("profileInfo.values.none")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sync */}
|
||||
@@ -546,33 +574,62 @@ export function ProfileInfoDialog({
|
||||
</TabsContent>
|
||||
<TabsContent value="settings">
|
||||
<div className="overflow-y-auto max-h-[calc(80vh-12rem)]">
|
||||
<div className="flex flex-col py-1">
|
||||
{visibleActions.map((action) => (
|
||||
<button
|
||||
key={action.label}
|
||||
type="button"
|
||||
disabled={action.disabled}
|
||||
onClick={action.onClick}
|
||||
className={cn(
|
||||
"flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors text-left w-full",
|
||||
"hover:bg-accent disabled:opacity-50 disabled:pointer-events-none",
|
||||
action.destructive &&
|
||||
"text-destructive hover:bg-destructive/10",
|
||||
)}
|
||||
>
|
||||
{action.icon}
|
||||
<span className="flex-1 flex items-center gap-2">
|
||||
{action.label}
|
||||
{action.runningBadge && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-semibold rounded bg-primary/15 text-primary uppercase">
|
||||
{t("common.status.running")}
|
||||
</span>
|
||||
<div className="flex flex-col gap-3 py-1">
|
||||
<div className="rounded-md bg-muted/50 border px-3 py-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("profileInfo.launchHook.label")}
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Input
|
||||
value={launchHookValue}
|
||||
onChange={(e) => {
|
||||
setLaunchHookValue(e.target.value);
|
||||
}}
|
||||
placeholder={t("profileInfo.launchHook.placeholder")}
|
||||
disabled={isSavingLaunchHook}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => void handleSaveLaunchHook()}
|
||||
disabled={
|
||||
isSavingLaunchHook ||
|
||||
trimmedLaunchHook === savedLaunchHook
|
||||
}
|
||||
>
|
||||
{t("common.buttons.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{visibleActions.map((action) => (
|
||||
<button
|
||||
key={action.label}
|
||||
type="button"
|
||||
disabled={action.disabled}
|
||||
onClick={action.onClick}
|
||||
className={cn(
|
||||
"flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors text-left w-full",
|
||||
"hover:bg-accent disabled:opacity-50 disabled:pointer-events-none",
|
||||
action.destructive &&
|
||||
"text-destructive hover:bg-destructive/10",
|
||||
)}
|
||||
{action.proBadge && !action.runningBadge && <ProBadge />}
|
||||
</span>
|
||||
<LuChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
))}
|
||||
>
|
||||
{action.icon}
|
||||
<span className="flex-1 flex items-center gap-2">
|
||||
{action.label}
|
||||
{action.runningBadge && (
|
||||
<span className="px-1.5 py-0.5 text-[10px] font-semibold rounded bg-primary/15 text-primary uppercase">
|
||||
{t("common.status.running")}
|
||||
</span>
|
||||
)}
|
||||
{action.proBadge && !action.runningBadge && (
|
||||
<ProBadge />
|
||||
)}
|
||||
</span>
|
||||
<LuChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -50,9 +50,7 @@ export function ProxyCheckButton({
|
||||
try {
|
||||
const result = await invoke<ProxyCheckResult>("check_proxy_validity", {
|
||||
proxyId: proxy.id,
|
||||
proxySettings: proxy.dynamic_proxy_url
|
||||
? undefined
|
||||
: proxy.proxy_settings,
|
||||
proxySettings: proxy.proxy_settings,
|
||||
});
|
||||
setLocalResult(result);
|
||||
onCheckComplete?.(result);
|
||||
|
||||
@@ -21,11 +21,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import type { ProxySettings, StoredProxy } from "@/types";
|
||||
import type { StoredProxy } from "@/types";
|
||||
import { RippleButton } from "./ui/ripple";
|
||||
|
||||
interface RegularFormData {
|
||||
interface ProxyFormData {
|
||||
name: string;
|
||||
proxy_type: string;
|
||||
host: string;
|
||||
@@ -34,20 +33,21 @@ interface RegularFormData {
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface DynamicFormData {
|
||||
name: string;
|
||||
url: string;
|
||||
format: string;
|
||||
}
|
||||
|
||||
type ProxyMode = "regular" | "dynamic";
|
||||
|
||||
interface ProxyFormDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
editingProxy?: StoredProxy | null;
|
||||
}
|
||||
|
||||
const DEFAULT_FORM: ProxyFormData = {
|
||||
name: "",
|
||||
proxy_type: "http",
|
||||
host: "",
|
||||
port: 8080,
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
export function ProxyFormDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -55,158 +55,66 @@ export function ProxyFormDialog({
|
||||
}: ProxyFormDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const [mode, setMode] = useState<ProxyMode>("regular");
|
||||
const [regularForm, setRegularForm] = useState<RegularFormData>({
|
||||
name: "",
|
||||
proxy_type: "http",
|
||||
host: "",
|
||||
port: 8080,
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const [dynamicForm, setDynamicForm] = useState<DynamicFormData>({
|
||||
name: "",
|
||||
url: "",
|
||||
format: "json",
|
||||
});
|
||||
const [form, setForm] = useState<ProxyFormData>(DEFAULT_FORM);
|
||||
|
||||
const resetForm = useCallback(() => {
|
||||
setRegularForm({
|
||||
name: "",
|
||||
proxy_type: "http",
|
||||
host: "",
|
||||
port: 8080,
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
setDynamicForm({
|
||||
name: "",
|
||||
url: "",
|
||||
format: "json",
|
||||
});
|
||||
setMode("regular");
|
||||
setForm(DEFAULT_FORM);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (editingProxy) {
|
||||
if (editingProxy.dynamic_proxy_url) {
|
||||
setMode("dynamic");
|
||||
setDynamicForm({
|
||||
name: editingProxy.name,
|
||||
url: editingProxy.dynamic_proxy_url,
|
||||
format: editingProxy.dynamic_proxy_format || "json",
|
||||
});
|
||||
} else {
|
||||
setMode("regular");
|
||||
setRegularForm({
|
||||
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 handleTestDynamic = useCallback(async () => {
|
||||
if (!dynamicForm.url.trim()) {
|
||||
toast.error(t("proxies.dynamic.urlRequired"));
|
||||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
setIsTesting(true);
|
||||
try {
|
||||
const settings = await invoke<ProxySettings>("fetch_dynamic_proxy", {
|
||||
url: dynamicForm.url.trim(),
|
||||
format: dynamicForm.format,
|
||||
});
|
||||
toast.success(
|
||||
t("proxies.dynamic.testSuccess", {
|
||||
host: settings.host,
|
||||
port: settings.port,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
toast.error(t("proxies.dynamic.testFailed", { error: errorMessage }));
|
||||
} finally {
|
||||
setIsTesting(false);
|
||||
|
||||
if (!editingProxy) {
|
||||
resetForm();
|
||||
return;
|
||||
}
|
||||
}, [dynamicForm, t]);
|
||||
|
||||
setForm({
|
||||
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 ?? "",
|
||||
});
|
||||
}, [editingProxy, isOpen, resetForm]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (mode === "regular") {
|
||||
if (!regularForm.name.trim()) {
|
||||
toast.error(t("proxies.form.nameRequired", "Proxy name is required"));
|
||||
return;
|
||||
}
|
||||
if (!regularForm.host.trim() || !regularForm.port) {
|
||||
toast.error(
|
||||
t("proxies.form.hostPortRequired", "Host and port are required"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!dynamicForm.name.trim()) {
|
||||
toast.error(t("proxies.form.nameRequired", "Proxy name is required"));
|
||||
return;
|
||||
}
|
||||
if (!dynamicForm.url.trim()) {
|
||||
toast.error(t("proxies.dynamic.urlRequired"));
|
||||
return;
|
||||
}
|
||||
if (!form.name.trim()) {
|
||||
toast.error(t("proxies.form.nameRequired", "Proxy name is required"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!form.host.trim() || !form.port) {
|
||||
toast.error(
|
||||
t("proxies.form.hostPortRequired", "Host and port are required"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const payload = {
|
||||
name: form.name.trim(),
|
||||
proxySettings: {
|
||||
proxy_type: form.proxy_type,
|
||||
host: form.host.trim(),
|
||||
port: form.port,
|
||||
username: form.username.trim() || undefined,
|
||||
password: form.password.trim() || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (editingProxy) {
|
||||
if (mode === "dynamic") {
|
||||
await invoke("update_stored_proxy", {
|
||||
proxyId: editingProxy.id,
|
||||
name: dynamicForm.name.trim(),
|
||||
dynamicProxyUrl: dynamicForm.url.trim(),
|
||||
dynamicProxyFormat: dynamicForm.format,
|
||||
});
|
||||
} else {
|
||||
await invoke("update_stored_proxy", {
|
||||
proxyId: editingProxy.id,
|
||||
name: regularForm.name.trim(),
|
||||
proxySettings: {
|
||||
proxy_type: regularForm.proxy_type,
|
||||
host: regularForm.host.trim(),
|
||||
port: regularForm.port,
|
||||
username: regularForm.username.trim() || undefined,
|
||||
password: regularForm.password.trim() || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
await invoke("update_stored_proxy", {
|
||||
proxyId: editingProxy.id,
|
||||
...payload,
|
||||
});
|
||||
toast.success(t("toasts.success.proxyUpdated"));
|
||||
} else {
|
||||
if (mode === "dynamic") {
|
||||
await invoke("create_stored_proxy", {
|
||||
name: dynamicForm.name.trim(),
|
||||
dynamicProxyUrl: dynamicForm.url.trim(),
|
||||
dynamicProxyFormat: dynamicForm.format,
|
||||
});
|
||||
} else {
|
||||
await invoke("create_stored_proxy", {
|
||||
name: regularForm.name.trim(),
|
||||
proxySettings: {
|
||||
proxy_type: regularForm.proxy_type,
|
||||
host: regularForm.host.trim(),
|
||||
port: regularForm.port,
|
||||
username: regularForm.username.trim() || undefined,
|
||||
password: regularForm.password.trim() || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
await invoke("create_stored_proxy", payload);
|
||||
toast.success(t("toasts.success.proxyCreated"));
|
||||
}
|
||||
|
||||
@@ -219,7 +127,7 @@ export function ProxyFormDialog({
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [mode, regularForm, dynamicForm, editingProxy, onClose, t]);
|
||||
}, [editingProxy, form, onClose, t]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (!isSubmitting) {
|
||||
@@ -227,17 +135,8 @@ export function ProxyFormDialog({
|
||||
}
|
||||
}, [isSubmitting, onClose]);
|
||||
|
||||
const isRegularValid =
|
||||
regularForm.name.trim() &&
|
||||
regularForm.host.trim() &&
|
||||
regularForm.port > 0 &&
|
||||
regularForm.port <= 65535;
|
||||
|
||||
const isDynamicValid = dynamicForm.name.trim() && dynamicForm.url.trim();
|
||||
|
||||
const isFormValid = mode === "regular" ? isRegularValid : isDynamicValid;
|
||||
|
||||
const isEditingDynamic = editingProxy?.dynamic_proxy_url != null;
|
||||
const isFormValid =
|
||||
form.name.trim() && form.host.trim() && form.port > 0 && form.port <= 65535;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
@@ -249,210 +148,109 @@ export function ProxyFormDialog({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
{!editingProxy && (
|
||||
<Tabs
|
||||
value={mode}
|
||||
onValueChange={(v) => {
|
||||
setMode(v as ProxyMode);
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-name">{t("proxies.form.name")}</Label>
|
||||
<Input
|
||||
id="proxy-name"
|
||||
value={form.name}
|
||||
onChange={(e) => {
|
||||
setForm({ ...form, name: e.target.value });
|
||||
}}
|
||||
placeholder={t("proxies.form.namePlaceholder")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("proxies.form.type")}</Label>
|
||||
<Select
|
||||
value={form.proxy_type}
|
||||
onValueChange={(value) => {
|
||||
setForm({ ...form, proxy_type: value });
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="regular" className="flex-1">
|
||||
{t("proxies.tabs.regular")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="dynamic" className="flex-1">
|
||||
{t("proxies.tabs.dynamic")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)}
|
||||
<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>
|
||||
|
||||
{editingProxy && isEditingDynamic && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxies.dynamic.description")}
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-host">{t("proxies.form.host")}</Label>
|
||||
<Input
|
||||
id="proxy-host"
|
||||
value={form.host}
|
||||
onChange={(e) => {
|
||||
setForm({ ...form, host: e.target.value });
|
||||
}}
|
||||
placeholder={t("proxies.form.hostPlaceholder")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{mode === "regular" ? (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-name">{t("proxies.form.name")}</Label>
|
||||
<Input
|
||||
id="proxy-name"
|
||||
value={regularForm.name}
|
||||
onChange={(e) => {
|
||||
setRegularForm({ ...regularForm, name: e.target.value });
|
||||
}}
|
||||
placeholder="e.g. Office Proxy, Home VPN, etc."
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-port">{t("proxies.form.port")}</Label>
|
||||
<Input
|
||||
id="proxy-port"
|
||||
type="number"
|
||||
value={form.port}
|
||||
onChange={(e) => {
|
||||
setForm({
|
||||
...form,
|
||||
port: Number.parseInt(e.target.value, 10) || 0,
|
||||
});
|
||||
}}
|
||||
placeholder={t("proxies.form.portPlaceholder")}
|
||||
min="1"
|
||||
max="65535"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("proxies.form.type")}</Label>
|
||||
<Select
|
||||
value={regularForm.proxy_type}
|
||||
onValueChange={(value) => {
|
||||
setRegularForm({ ...regularForm, 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-username">
|
||||
{t("proxies.form.username")} (
|
||||
{t("proxies.form.usernamePlaceholder")})
|
||||
</Label>
|
||||
<Input
|
||||
id="proxy-username"
|
||||
value={form.username}
|
||||
onChange={(e) => {
|
||||
setForm({ ...form, username: e.target.value });
|
||||
}}
|
||||
placeholder={t("proxies.form.usernamePlaceholder")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-host">{t("proxies.form.host")}</Label>
|
||||
<Input
|
||||
id="proxy-host"
|
||||
value={regularForm.host}
|
||||
onChange={(e) => {
|
||||
setRegularForm({ ...regularForm, host: e.target.value });
|
||||
}}
|
||||
placeholder={t("proxies.form.hostPlaceholder")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-port">{t("proxies.form.port")}</Label>
|
||||
<Input
|
||||
id="proxy-port"
|
||||
type="number"
|
||||
value={regularForm.port}
|
||||
onChange={(e) => {
|
||||
setRegularForm({
|
||||
...regularForm,
|
||||
port: parseInt(e.target.value, 10) || 0,
|
||||
});
|
||||
}}
|
||||
placeholder={t("proxies.form.portPlaceholder")}
|
||||
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">
|
||||
{t("proxies.form.username")} (
|
||||
{t("proxies.form.usernamePlaceholder")})
|
||||
</Label>
|
||||
<Input
|
||||
id="proxy-username"
|
||||
value={regularForm.username}
|
||||
onChange={(e) => {
|
||||
setRegularForm({
|
||||
...regularForm,
|
||||
username: e.target.value,
|
||||
});
|
||||
}}
|
||||
placeholder={t("proxies.form.usernamePlaceholder")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-password">
|
||||
{t("proxies.form.password")} (
|
||||
{t("proxies.form.passwordPlaceholder")})
|
||||
</Label>
|
||||
<Input
|
||||
id="proxy-password"
|
||||
type="password"
|
||||
value={regularForm.password}
|
||||
onChange={(e) => {
|
||||
setRegularForm({
|
||||
...regularForm,
|
||||
password: e.target.value,
|
||||
});
|
||||
}}
|
||||
placeholder={t("proxies.form.passwordPlaceholder")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dynamic-name">{t("proxies.form.name")}</Label>
|
||||
<Input
|
||||
id="dynamic-name"
|
||||
value={dynamicForm.name}
|
||||
onChange={(e) => {
|
||||
setDynamicForm({ ...dynamicForm, name: e.target.value });
|
||||
}}
|
||||
placeholder="e.g. My Tunnel"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="dynamic-url">{t("proxies.dynamic.url")}</Label>
|
||||
<Input
|
||||
id="dynamic-url"
|
||||
value={dynamicForm.url}
|
||||
onChange={(e) => {
|
||||
setDynamicForm({ ...dynamicForm, url: e.target.value });
|
||||
}}
|
||||
placeholder={t("proxies.dynamic.urlPlaceholder")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("proxies.dynamic.format")}</Label>
|
||||
<Select
|
||||
value={dynamicForm.format}
|
||||
onValueChange={(value) => {
|
||||
setDynamicForm({ ...dynamicForm, format: value });
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="json">
|
||||
{t("proxies.dynamic.formatJson")}
|
||||
</SelectItem>
|
||||
<SelectItem value="text">
|
||||
{t("proxies.dynamic.formatText")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{dynamicForm.format === "json"
|
||||
? t("proxies.dynamic.formatJsonHint")
|
||||
: t("proxies.dynamic.formatTextHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RippleButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleTestDynamic}
|
||||
disabled={isSubmitting || isTesting || !dynamicForm.url.trim()}
|
||||
>
|
||||
{isTesting
|
||||
? t("proxies.dynamic.testing")
|
||||
: t("proxies.dynamic.testUrl")}
|
||||
</RippleButton>
|
||||
</>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="proxy-password">
|
||||
{t("proxies.form.password")} (
|
||||
{t("proxies.form.passwordPlaceholder")})
|
||||
</Label>
|
||||
<Input
|
||||
id="proxy-password"
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) => {
|
||||
setForm({ ...form, password: e.target.value });
|
||||
}}
|
||||
placeholder={t("proxies.form.passwordPlaceholder")}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
@@ -461,7 +259,7 @@ export function ProxyFormDialog({
|
||||
onClick={handleClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t("common.cancel", "Cancel")}
|
||||
{t("common.buttons.cancel")}
|
||||
</RippleButton>
|
||||
<LoadingButton
|
||||
isLoading={isSubmitting}
|
||||
|
||||
@@ -469,14 +469,6 @@ export function ProxyManagementDialog({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{proxy.name}
|
||||
{proxy.dynamic_proxy_url && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] px-1 py-0"
|
||||
>
|
||||
Dynamic
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -229,6 +229,10 @@
|
||||
"noProxy": "No proxy / VPN",
|
||||
"noProxiesAvailable": "No proxies or VPNs available. Add one to route this profile's traffic."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "Launch Hook URL",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"version": {
|
||||
"fetching": "Fetching available versions...",
|
||||
"fetchError": "Failed to fetch browser versions. Please check your internet connection and try again.",
|
||||
@@ -759,6 +763,7 @@
|
||||
"browser": "Browser",
|
||||
"releaseType": "Release Type",
|
||||
"proxyVpn": "Proxy / VPN",
|
||||
"launchHook": "Launch Hook",
|
||||
"group": "Group",
|
||||
"tags": "Tags",
|
||||
"note": "Note",
|
||||
@@ -783,6 +788,10 @@
|
||||
"noRules": "No bypass rules configured.",
|
||||
"ruleTypes": "Supports hostnames, IP addresses, and regex patterns."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "Launch Hook URL",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"actions": {
|
||||
"manageCookies": "Manage Cookies",
|
||||
"assignExtensionGroup": "Assign Extension Group"
|
||||
|
||||
@@ -229,6 +229,10 @@
|
||||
"noProxy": "Sin proxy / VPN",
|
||||
"noProxiesAvailable": "No hay proxies o VPNs disponibles. Agrega uno para enrutar el tráfico de este perfil."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "URL del hook de inicio",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"version": {
|
||||
"fetching": "Obteniendo versiones disponibles...",
|
||||
"fetchError": "Error al obtener versiones del navegador. Por favor verifica tu conexión a internet e intenta de nuevo.",
|
||||
@@ -759,6 +763,7 @@
|
||||
"browser": "Navegador",
|
||||
"releaseType": "Tipo de Versión",
|
||||
"proxyVpn": "Proxy / VPN",
|
||||
"launchHook": "Hook de inicio",
|
||||
"group": "Grupo",
|
||||
"tags": "Etiquetas",
|
||||
"note": "Nota",
|
||||
@@ -783,6 +788,10 @@
|
||||
"noRules": "No hay reglas de omisión configuradas.",
|
||||
"ruleTypes": "Soporta nombres de host, direcciones IP y patrones regex."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "URL del hook de inicio",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"actions": {
|
||||
"manageCookies": "Administrar Cookies",
|
||||
"assignExtensionGroup": "Asignar Grupo de Extensiones"
|
||||
|
||||
@@ -229,6 +229,10 @@
|
||||
"noProxy": "Pas de proxy / VPN",
|
||||
"noProxiesAvailable": "Aucun proxy ou VPN disponible. Ajoutez-en un pour router le trafic de ce profil."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "URL du hook de lancement",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"version": {
|
||||
"fetching": "Récupération des versions disponibles...",
|
||||
"fetchError": "Échec de la récupération des versions du navigateur. Veuillez vérifier votre connexion Internet et réessayer.",
|
||||
@@ -759,6 +763,7 @@
|
||||
"browser": "Navigateur",
|
||||
"releaseType": "Type de Version",
|
||||
"proxyVpn": "Proxy / VPN",
|
||||
"launchHook": "Hook de lancement",
|
||||
"group": "Groupe",
|
||||
"tags": "Tags",
|
||||
"note": "Note",
|
||||
@@ -783,6 +788,10 @@
|
||||
"noRules": "Aucune règle de contournement configurée.",
|
||||
"ruleTypes": "Prend en charge les noms d'hôte, les adresses IP et les expressions régulières."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "URL du hook de lancement",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"actions": {
|
||||
"manageCookies": "Gérer les Cookies",
|
||||
"assignExtensionGroup": "Assigner un Groupe d'Extensions"
|
||||
|
||||
@@ -229,6 +229,10 @@
|
||||
"noProxy": "プロキシ / VPNなし",
|
||||
"noProxiesAvailable": "利用可能なプロキシまたはVPNがありません。このプロファイルのトラフィックをルーティングするために追加してください。"
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "起動フックURL",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"version": {
|
||||
"fetching": "利用可能なバージョンを取得中...",
|
||||
"fetchError": "ブラウザバージョンの取得に失敗しました。インターネット接続を確認して再試行してください。",
|
||||
@@ -759,6 +763,7 @@
|
||||
"browser": "ブラウザ",
|
||||
"releaseType": "リリースタイプ",
|
||||
"proxyVpn": "プロキシ / VPN",
|
||||
"launchHook": "起動フック",
|
||||
"group": "グループ",
|
||||
"tags": "タグ",
|
||||
"note": "メモ",
|
||||
@@ -783,6 +788,10 @@
|
||||
"noRules": "バイパスルールは設定されていません。",
|
||||
"ruleTypes": "ホスト名、IPアドレス、正規表現パターンをサポートしています。"
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "起動フックURL",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"actions": {
|
||||
"manageCookies": "Cookieを管理",
|
||||
"assignExtensionGroup": "拡張機能グループを割り当て"
|
||||
|
||||
@@ -229,6 +229,10 @@
|
||||
"noProxy": "Sem proxy / VPN",
|
||||
"noProxiesAvailable": "Nenhum proxy ou VPN disponível. Adicione um para rotear o tráfego deste perfil."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "URL do hook de inicialização",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"version": {
|
||||
"fetching": "Buscando versões disponíveis...",
|
||||
"fetchError": "Falha ao buscar versões do navegador. Por favor, verifique sua conexão com a internet e tente novamente.",
|
||||
@@ -759,6 +763,7 @@
|
||||
"browser": "Navegador",
|
||||
"releaseType": "Tipo de Versão",
|
||||
"proxyVpn": "Proxy / VPN",
|
||||
"launchHook": "Hook de inicialização",
|
||||
"group": "Grupo",
|
||||
"tags": "Tags",
|
||||
"note": "Nota",
|
||||
@@ -783,6 +788,10 @@
|
||||
"noRules": "Nenhuma regra de bypass configurada.",
|
||||
"ruleTypes": "Suporta nomes de host, endereços IP e padrões regex."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "URL do hook de inicialização",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"actions": {
|
||||
"manageCookies": "Gerenciar Cookies",
|
||||
"assignExtensionGroup": "Atribuir Grupo de Extensões"
|
||||
|
||||
@@ -229,6 +229,10 @@
|
||||
"noProxy": "Без прокси / VPN",
|
||||
"noProxiesAvailable": "Нет доступных прокси или VPN. Добавьте один для маршрутизации трафика этого профиля."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "URL хука запуска",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"version": {
|
||||
"fetching": "Получение доступных версий...",
|
||||
"fetchError": "Не удалось получить версии браузера. Проверьте интернет-соединение и попробуйте снова.",
|
||||
@@ -759,6 +763,7 @@
|
||||
"browser": "Браузер",
|
||||
"releaseType": "Тип релиза",
|
||||
"proxyVpn": "Прокси / VPN",
|
||||
"launchHook": "Хук запуска",
|
||||
"group": "Группа",
|
||||
"tags": "Теги",
|
||||
"note": "Заметка",
|
||||
@@ -783,6 +788,10 @@
|
||||
"noRules": "Правила обхода не настроены.",
|
||||
"ruleTypes": "Поддерживает имена хостов, IP-адреса и шаблоны регулярных выражений."
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "URL хука запуска",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"actions": {
|
||||
"manageCookies": "Управление Cookie",
|
||||
"assignExtensionGroup": "Назначить группу расширений"
|
||||
|
||||
@@ -229,6 +229,10 @@
|
||||
"noProxy": "无代理 / VPN",
|
||||
"noProxiesAvailable": "没有可用的代理或VPN。添加一个来路由此配置文件的流量。"
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "启动钩子 URL",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"version": {
|
||||
"fetching": "正在获取可用版本...",
|
||||
"fetchError": "获取浏览器版本失败。请检查您的网络连接并重试。",
|
||||
@@ -759,6 +763,7 @@
|
||||
"browser": "浏览器",
|
||||
"releaseType": "发布类型",
|
||||
"proxyVpn": "代理 / VPN",
|
||||
"launchHook": "启动钩子",
|
||||
"group": "分组",
|
||||
"tags": "标签",
|
||||
"note": "备注",
|
||||
@@ -783,6 +788,10 @@
|
||||
"noRules": "未配置绕过规则。",
|
||||
"ruleTypes": "支持主机名、IP地址和正则表达式模式。"
|
||||
},
|
||||
"launchHook": {
|
||||
"label": "启动钩子 URL",
|
||||
"placeholder": "https://example.com/hooks/profile-launch"
|
||||
},
|
||||
"actions": {
|
||||
"manageCookies": "管理 Cookie",
|
||||
"assignExtensionGroup": "分配扩展程序组"
|
||||
|
||||
+1
-2
@@ -18,6 +18,7 @@ export interface BrowserProfile {
|
||||
version: string;
|
||||
proxy_id?: string; // Reference to stored proxy
|
||||
vpn_id?: string; // Reference to stored VPN config
|
||||
launch_hook?: string;
|
||||
process_id?: number;
|
||||
last_launch?: number;
|
||||
release_type: string; // "stable" or "nightly"
|
||||
@@ -135,8 +136,6 @@ export interface StoredProxy {
|
||||
geo_region?: string;
|
||||
geo_city?: string;
|
||||
geo_isp?: string;
|
||||
dynamic_proxy_url?: string;
|
||||
dynamic_proxy_format?: string;
|
||||
}
|
||||
|
||||
export interface LocationItem {
|
||||
|
||||
Reference in New Issue
Block a user