feat: proxy change-IP link

This commit is contained in:
zhom committed 2026-10-12 00:32:08 +04:00
1 parent 69d3c90a7c
commit 3f07a41fac
25 files changed
+925 -51

No files matched your search

+19
View File
@@ -29,6 +29,7 @@ import {
LuLockOpen,
LuPlus,
LuPuzzle,
LuRefreshCcwDot,
LuRefreshCw,
LuSettings,
LuShield,
@@ -91,6 +92,7 @@ import { getProfileIcon, getProfileNativeOs } from "@/lib/browser-utils";
import { DNS_BLOCKLIST_LEVELS } from "@/lib/dns-blocklist-levels";
import { formatRelativeTime } from "@/lib/flag-utils";
import { MOTION_EASE_OUT } from "@/lib/motion";
import { rotateProxyIps } from "@/lib/proxy-rotation";
import type { RemoteHandoffState } from "@/lib/remote-sessions";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { cn } from "@/lib/utils";
@@ -1751,6 +1753,9 @@ function NetworkSectionInline({
const [vpnId, setVpnId] = React.useState<string | null>(
profile.vpn_id ?? null,
);
const rotatableProxy = storedProxies.find(
(p) => p.id === proxyId && p.rotation_url,
);
const dnsServerId = React.useId();
const [savedDnsServer, setSavedDnsServer] = React.useState(
profile.dns_server ?? "",
@@ -1863,6 +1868,20 @@ function NetworkSectionInline({
))}
</SelectContent>
</Select>
{rotatableProxy && (
<Button
type="button"
variant="soft"
className="shrink-0"
data-slot="profile-proxy-rotate"
title={t("proxies.rotation.actionTooltip")}
disabled={isSaving}
onClick={() => void rotateProxyIps([rotatableProxy], t as never)}
>
<LuRefreshCcwDot className="size-3.5" />
{t("proxies.rotation.action")}
</Button>
)}
</div>
<div className="flex items-center gap-2">
+46 -1
View File
@@ -25,6 +25,7 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { translateBackendError } from "@/lib/backend-errors";
import { looksLikeRotationUrl } from "@/lib/proxy-rotation";
import {
isFirstHopEncrypted,
isXrayProxyType,
@@ -42,6 +43,7 @@ interface ProxyFormData {
username: string;
password: string;
vless_uri: string;
rotation_url: string;
}
interface ProxyFormDialogProps {
@@ -58,6 +60,7 @@ const DEFAULT_FORM: ProxyFormData = {
username: "",
password: "",
vless_uri: "",
rotation_url: "",
};
/**
@@ -175,6 +178,7 @@ export function ProxyFormDialog({
username: editingProxy.proxy_settings.username ?? "",
password: editingProxy.proxy_settings.password ?? "",
vless_uri: editingProxy.proxy_settings.vless_uri ?? "",
rotation_url: editingProxy.rotation_url ?? "",
});
}, [editingProxy, isOpen, resetForm]);
@@ -215,6 +219,12 @@ export function ProxyFormDialog({
return;
}
const rotationUrl = form.rotation_url.trim();
if (rotationUrl && !looksLikeRotationUrl(rotationUrl)) {
toast.error(t("backendErrors.proxyRotationUrlInvalid"));
return;
}
setIsSubmitting(true);
try {
const payload = {
@@ -234,9 +244,24 @@ export function ProxyFormDialog({
proxyId: editingProxy.id,
...payload,
});
if (rotationUrl !== (editingProxy.rotation_url ?? "")) {
await invoke("set_proxy_rotation_url", {
proxyId: editingProxy.id,
rotationUrl: rotationUrl || null,
});
}
toast.success(t("toasts.success.proxyUpdated"));
} else {
await invoke("create_stored_proxy", payload);
const created = await invoke<StoredProxy>(
"create_stored_proxy",
payload,
);
if (rotationUrl) {
await invoke("set_proxy_rotation_url", {
proxyId: created.id,
rotationUrl,
});
}
toast.success(t("toasts.success.proxyCreated"));
}
@@ -602,6 +627,26 @@ export function ProxyFormDialog({
)}
</>
)}
<div className="grid gap-2">
<Label htmlFor="proxy-rotation-url">
{t("proxies.rotation.label")}
</Label>
<Input
id="proxy-rotation-url"
value={form.rotation_url}
onChange={(e) => {
setForm({ ...form, rotation_url: e.target.value });
}}
placeholder={t("proxies.rotation.placeholder")}
spellCheck={false}
autoComplete="off"
disabled={isSubmitting}
/>
<p className="text-xs text-muted-foreground">
{t("proxies.rotation.hint")}
</p>
</div>
</div>
<DialogFooter>
@@ -15,6 +15,7 @@ import { GoPlus } from "react-icons/go";
import {
LuDownload,
LuPencil,
LuRefreshCcwDot,
LuRefreshCw,
LuTrash2,
LuUpload,
@@ -63,6 +64,7 @@ import { useProfileReferences } from "@/hooks/use-profile-references";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { useVpnEvents } from "@/hooks/use-vpn-events";
import { parseBackendError, translateBackendError } from "@/lib/backend-errors";
import { rotateProxyIps } from "@/lib/proxy-rotation";
import { isFirstHopEncrypted, proxyProtocolToken } from "@/lib/proxy-string";
import { canonicalProxyType } from "@/lib/proxy-type";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
@@ -655,6 +657,26 @@ export function ProxyManagementDialog({
return (
<div className="flex gap-1">
<ProxyCheckButton proxy={proxy} />
{proxy.rotation_url && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
data-slot="proxy-rotate"
aria-label={t("proxies.rotation.action")}
onClick={() => {
void rotateProxyIps([proxy], t);
}}
>
<LuRefreshCcwDot className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("proxies.rotation.actionTooltip")}</p>
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -1386,6 +1408,19 @@ export function ProxyManagementDialog({
>
<LuRefreshCw />
</DataTableActionBarAction>
<DataTableActionBarAction
tooltip={t("proxies.rotation.action")}
onClick={() =>
void rotateProxyIps(
proxiesTable
.getSelectedRowModel()
.rows.map((row) => row.original),
t,
)
}
>
<LuRefreshCcwDot />
</DataTableActionBarAction>
<DataTableActionBarAction
tooltip={t("common.buttons.delete")}
onClick={() => {