refactor: better proxy clipboard autofill

This commit is contained in:
zhom
2026-08-16 19:49:54 +04:00
parent a0175eab0d
commit 2be0d4df0b
7 changed files with 501 additions and 74 deletions
+48 -1
View File
@@ -23,7 +23,8 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { translateBackendError } from "@/lib/backend-errors";
import type { StoredProxy } from "@/types";
import { pickParsedProxy } from "@/lib/proxy-string";
import type { ProxyParseResult, StoredProxy } from "@/types";
import { RippleButton } from "./ui/ripple";
interface ProxyFormData {
@@ -202,6 +203,48 @@ export function ProxyFormDialog({
}
}, [isSubmitting, onClose]);
// Proxies are copied around as one string — `socks5://user:pass@host:1080`,
// `host:1080:user:pass`, and a dozen variants of both — so a paste into any
// one field is almost never meant for that field alone. Hand the clipboard to
// the same Rust parser the import dialog uses and spread the result across
// the form. The default paste is left alone until the answer comes back, so a
// string that isn't a proxy (a hostname, a port) lands where it was dropped.
const handleProxyPaste = useCallback(
(event: React.ClipboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const content = event.clipboardData.getData("text").trim();
if (!content) {
return;
}
// Captured before the browser applies the paste, so a proxy string
// dropped into the empty name field names the proxy after its endpoint
// instead of keeping the raw line.
const nameBeforePaste = form.name.trim();
void invoke<ProxyParseResult[]>("parse_txt_proxies", { content })
.then((results) => {
const parsed = pickParsedProxy(results);
if (!parsed) {
return;
}
setForm((previous) => ({
...previous,
name: nameBeforePaste || `${parsed.host}:${parsed.port}`,
proxy_type: parsed.proxy_type,
host: parsed.host,
port: parsed.port,
username: parsed.username ?? "",
password: parsed.password ?? "",
vless_uri: parsed.vless_uri ?? "",
}));
})
.catch((error: unknown) => {
console.error("Failed to parse pasted proxy:", error);
});
},
[form.name],
);
const isVless = form.proxy_type === "vless";
const vlessEndpoint = isVless ? parseVlessEndpoint(form.vless_uri) : null;
@@ -259,6 +302,7 @@ export function ProxyFormDialog({
onChange={(e) => {
setForm({ ...form, name: e.target.value });
}}
onPaste={handleProxyPaste}
placeholder={t("proxies.form.namePlaceholder")}
disabled={isSubmitting}
/>
@@ -303,6 +347,7 @@ export function ProxyFormDialog({
onChange={(e) => {
setForm({ ...form, vless_uri: e.target.value });
}}
onPaste={handleProxyPaste}
placeholder={t("proxies.form.vlessUriPlaceholder")}
disabled={isSubmitting}
aria-invalid={hasInvalidVlessUri}
@@ -337,6 +382,7 @@ export function ProxyFormDialog({
onChange={(e) => {
setForm({ ...form, host: e.target.value });
}}
onPaste={handleProxyPaste}
placeholder={t("proxies.form.hostPlaceholder")}
disabled={isSubmitting}
/>
@@ -354,6 +400,7 @@ export function ProxyFormDialog({
port: Number.parseInt(e.target.value, 10) || 0,
});
}}
onPaste={handleProxyPaste}
placeholder={t("proxies.form.portPlaceholder")}
min="1"
max="65535"
+7 -25
View File
@@ -20,6 +20,7 @@ import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import { StepTransition } from "@/components/ui/step-transition";
import { getCurrentOS } from "@/lib/browser-utils";
import { resolveAmbiguousProxyLine } from "@/lib/proxy-string";
import type {
ParsedProxyLine,
ProxyImportResult,
@@ -265,31 +266,12 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
);
const handleResolveAmbiguous = useCallback(() => {
// Convert ambiguous proxies to parsed based on selected format
const resolved: ParsedProxyLine[] = ambiguousProxies
.filter((p) => p.selectedFormat)
.map((p) => {
const parts = p.line.split(":");
if (p.selectedFormat === "host:port:username:password") {
return {
proxy_type: "http",
host: parts[0],
port: Number.parseInt(parts[1], 10),
username: parts[2],
password: parts[3],
original_line: p.line,
};
}
// username:password:host:port
return {
proxy_type: "http",
host: parts[2],
port: Number.parseInt(parts[3], 10),
username: parts[0],
password: parts[1],
original_line: p.line,
};
});
const resolved = ambiguousProxies.flatMap((p) => {
const parsed = p.selectedFormat
? resolveAmbiguousProxyLine(p.line, p.selectedFormat)
: null;
return parsed ? [parsed] : [];
});
setParsedProxies((prev) => [...prev, ...resolved]);
setStep("preview");