New Interface (initial commit)

This commit is contained in:
coolcoala
2025-07-04 02:28:27 +03:00
parent 4435a5aee4
commit 686490ded1
121 changed files with 12852 additions and 13274 deletions

View File

@@ -1,6 +1,12 @@
import { BaseDialog, DialogRef, Switch } from "@/components/base";
import { forwardRef, useImperativeHandle, useState, useMemo, useEffect, ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import useSWR, { mutate } from "swr";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
// Новые импорты
import { DialogRef, Switch } from "@/components/base";
import { BaseFieldset } from "@/components/base/base-fieldset";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { useVerge } from "@/hooks/use-verge";
import { useAppData } from "@/providers/app-data-provider";
@@ -14,66 +20,71 @@ import {
} from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import getSystem from "@/utils/get-system";
import { EditRounded } from "@mui/icons-material";
import {
Autocomplete,
Button,
InputAdornment,
List,
ListItem,
ListItemText,
styled,
TextField,
Typography,
} from "@mui/material";
import { useLockFn } from "ahooks";
import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import useSWR, { mutate } from "swr";
const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;";
}`;
/** NO_PROXY validation */
// *., cdn*., *, etc.
const domain_subdomain_part = String.raw`(?:[a-z0-9\-\*]+\.|\*)*`;
// .*, .cn, .moe, .co*, *
const domain_tld_part = String.raw`(?:\w{2,64}\*?|\*)`;
// *epicgames*, *skk.moe, *.skk.moe, skk.*, sponsor.cdn.skk.moe, *.*, etc.
// also matches 192.168.*, 10.*, 127.0.0.*, etc. (partial ipv4)
const rDomainSimple = domain_subdomain_part + domain_tld_part;
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Check, ChevronsUpDown, Edit, Loader2 } from "lucide-react";
import { cn } from "@root/lib/utils";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
// --- Вся ваша оригинальная логика, константы и хелперы ---
const DEFAULT_PAC = `function FindProxyForURL(url, host) { return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;"; }`;
const ipv4_part = String.raw`\d{1,3}`;
const rDomainSimple = String.raw`(?:[a-z0-9\-\*]+\.|\*)*` + String.raw`(?:\w{2,64}\*?|\*)`;
const ipv6_part = "(?:[a-fA-F0-9:])+";
const rLocal = `localhost|<local>|localdomain`;
const getValidReg = (isWindows: boolean) => {
// 127.0.0.1 (full ipv4)
const rIPv4Unix = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}(?:\/\d{1,2})?`;
const rIPv4Windows = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}`;
const rIPv6Unix = String.raw`(?:${ipv6_part}:+)+${ipv6_part}(?:\/\d{1,3})?`;
const rIPv6Windows = String.raw`(?:${ipv6_part}:+)+${ipv6_part}`;
const rValidPart = `${rDomainSimple}|${
isWindows ? rIPv4Windows : rIPv4Unix
}|${isWindows ? rIPv6Windows : rIPv6Unix}|${rLocal}`;
const rValidPart = `${rDomainSimple}|${isWindows ? rIPv4Windows : rIPv4Unix}|${isWindows ? rIPv6Windows : rIPv6Unix}|${rLocal}`;
const separator = isWindows ? ";" : ",";
const rValid = String.raw`^(${rValidPart})(?:${separator}\s?(${rValidPart}))*${separator}?$`;
return new RegExp(rValid);
};
// --- Компонент Combobox для замены Autocomplete ---
const Combobox = ({ options, value, onValueChange, placeholder }: { options: string[], value: string, onValueChange: (value: string) => void, placeholder?: string }) => {
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen} modal={true}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open} className="w-48 h-8 justify-between font-normal">
{value || placeholder || "Select..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command onValueChange={onValueChange}>
<CommandInput placeholder="Search or type..." />
<CommandEmpty>No results found.</CommandEmpty>
<CommandList>
{options.map((option) => (
<CommandItem key={option} value={option} onSelect={(currentValue) => { onValueChange(options.find(opt => opt.toLowerCase() === currentValue) || ''); setOpen(false); }}>
<Check className={cn("mr-2 h-4 w-4", value === option ? "opacity-100" : "opacity-0")} />
{option}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
// --- Наш переиспользуемый компонент для строки настроек ---
const SettingRow = ({ label, children }: { label: React.ReactNode; children?: React.ReactNode; }) => (
<div className="flex items-center justify-between py-2">
<Label className="text-sm text-muted-foreground flex items-center gap-2">{label}</Label>
<div>{children}</div>
</div>
);
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const isWindows = getSystem() === "windows";
@@ -91,57 +102,25 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
type AutoProxy = Awaited<ReturnType<typeof getAutotemProxy>>;
const [autoproxy, setAutoproxy] = useState<AutoProxy>();
const {
enable_system_proxy: enabled,
proxy_auto_config,
pac_file_content,
enable_proxy_guard,
use_default_bypass,
system_proxy_bypass,
proxy_guard_duration,
proxy_host,
} = verge ?? {};
const { enable_system_proxy: enabled, proxy_auto_config, pac_file_content, enable_proxy_guard, use_default_bypass, system_proxy_bypass, proxy_guard_duration, proxy_host } = verge ?? {};
const [value, setValue] = useState({
guard: enable_proxy_guard,
bypass: system_proxy_bypass,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
guard: enable_proxy_guard, bypass: system_proxy_bypass, duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true, pac: proxy_auto_config, pac_content: pac_file_content ?? DEFAULT_PAC,
proxy_host: proxy_host ?? "127.0.0.1",
});
const defaultBypass = () => {
if (isWindows) {
return "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
}
if (getSystem() === "linux") {
return "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
}
if (isWindows) return "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
if (getSystem() === "linux") return "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
};
const { data: clashConfig, mutate: mutateClash } = useSWR(
"getClashConfig",
getClashConfig,
{
revalidateOnFocus: false,
revalidateIfStale: true,
dedupingInterval: 1000,
errorRetryInterval: 5000,
},
);
const [prevMixedPort, setPrevMixedPort] = useState(
clashConfig?.["mixed-port"],
);
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig, { revalidateOnFocus: false, revalidateIfStale: true, dedupingInterval: 1000, errorRetryInterval: 5000 });
const [prevMixedPort, setPrevMixedPort] = useState(clashConfig?.["mixed-port"]);
useEffect(() => {
if (
clashConfig?.["mixed-port"] &&
clashConfig?.["mixed-port"] !== prevMixedPort
) {
if (clashConfig?.["mixed-port"] && clashConfig?.["mixed-port"] !== prevMixedPort) {
setPrevMixedPort(clashConfig?.["mixed-port"]);
resetSystemProxy();
}
@@ -151,36 +130,20 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
try {
const currentSysProxy = await getSystemProxy();
const currentAutoProxy = await getAutotemProxy();
if (value.pac ? currentAutoProxy?.enable : currentSysProxy?.enable) {
// 临时关闭系统代理
await patchVergeConfig({ enable_system_proxy: false });
// 减少等待时间
await new Promise((resolve) => setTimeout(resolve, 200));
// 重新开启系统代理
await patchVergeConfig({ enable_system_proxy: true });
// 更新UI状态
await Promise.all([
mutate("getSystemProxy"),
mutate("getAutotemProxy"),
]);
await Promise.all([ mutate("getSystemProxy"), mutate("getAutotemProxy") ]);
}
} catch (err: any) {
showNotice("error", err.toString());
}
} catch (err: any) { showNotice("error", err.toString()); }
};
const { systemProxyAddress } = useAppData();
// 为当前状态计算系统代理地址
const getSystemProxyAddress = useMemo(() => {
if (!clashConfig) return "-";
const isPacMode = value.pac ?? false;
if (isPacMode) {
const host = value.proxy_host || "127.0.0.1";
const port = verge?.verge_mixed_port || clashConfig["mixed-port"] || 7897;
@@ -188,448 +151,160 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
} else {
return systemProxyAddress;
}
}, [
value.pac,
value.proxy_host,
verge?.verge_mixed_port,
clashConfig,
systemProxyAddress,
]);
}, [value.pac, value.proxy_host, verge?.verge_mixed_port, clashConfig, systemProxyAddress]);
const getCurrentPacUrl = useMemo(() => {
const host = value.proxy_host || "127.0.0.1";
// 根据环境判断PAC端口
const port = import.meta.env.DEV ? 11233 : 33331;
return `http://${host}:${port}/commands/pac`;
}, [value.proxy_host]);
const fetchNetworkInterfaces = async () => {
try {
const interfaces = await getNetworkInterfacesInfo();
const ipAddresses: string[] = [];
interfaces.forEach((iface) => {
iface.addr.forEach((address) => {
if (address.V4 && address.V4.ip) ipAddresses.push(address.V4.ip);
if (address.V6 && address.V6.ip) ipAddresses.push(address.V6.ip);
});
});
let hostname = "";
try {
hostname = await getSystemHostname();
if (hostname && hostname !== "localhost" && hostname !== "127.0.0.1") {
hostname = hostname + ".local";
}
} catch (err) { console.error("Failed to get hostname:", err); }
const options = ["127.0.0.1", "localhost"];
if (hostname) options.push(hostname);
options.push(...ipAddresses);
setHostOptions(Array.from(new Set(options)));
} catch (error) {
console.error("Failed to get network interfaces:", error);
setHostOptions(["127.0.0.1", "localhost"]);
}
};
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
setValue({
guard: enable_proxy_guard,
bypass: system_proxy_bypass,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
guard: enable_proxy_guard, bypass: system_proxy_bypass, duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true, pac: proxy_auto_config, pac_content: pac_file_content ?? DEFAULT_PAC,
proxy_host: proxy_host ?? "127.0.0.1",
});
getSystemProxy().then((p) => setSysproxy(p));
getAutotemProxy().then((p) => setAutoproxy(p));
getSystemProxy().then(setSysproxy);
getAutotemProxy().then(setAutoproxy);
fetchNetworkInterfaces();
},
close: () => setOpen(false),
}));
// 获取网络接口和主机名
const fetchNetworkInterfaces = async () => {
try {
// 获取系统网络接口信息
const interfaces = await getNetworkInterfacesInfo();
const ipAddresses: string[] = [];
// 从interfaces中提取IPv4和IPv6地址
interfaces.forEach((iface) => {
iface.addr.forEach((address) => {
if (address.V4 && address.V4.ip) {
ipAddresses.push(address.V4.ip);
}
if (address.V6 && address.V6.ip) {
ipAddresses.push(address.V6.ip);
}
});
});
// 获取当前系统的主机名
let hostname = "";
try {
hostname = await getSystemHostname();
console.log("获取到主机名:", hostname);
} catch (err) {
console.error("获取主机名失败:", err);
}
// 构建选项列表
const options = ["127.0.0.1", "localhost"];
// 确保主机名添加到列表,即使它是空字符串也记录下来
if (hostname) {
// 如果主机名不是localhost或127.0.0.1,则添加它
if (hostname !== "localhost" && hostname !== "127.0.0.1") {
hostname = hostname + ".local";
options.push(hostname);
console.log("主机名已添加到选项中:", hostname);
} else {
console.log("主机名与已有选项重复:", hostname);
}
} else {
console.log("主机名为空");
}
// 添加IP地址
options.push(...ipAddresses);
// 去重
const uniqueOptions = Array.from(new Set(options));
console.log("最终选项列表:", uniqueOptions);
setHostOptions(uniqueOptions);
} catch (error) {
console.error("获取网络接口失败:", error);
// 失败时至少提供基本选项
setHostOptions(["127.0.0.1", "localhost"]);
}
};
const onSave = useLockFn(async () => {
if (value.duration < 1) {
showNotice(
"error",
t("Proxy Daemon Duration Cannot be Less than 1 Second"),
);
return;
}
if (value.bypass && !validReg.test(value.bypass)) {
showNotice("error", t("Invalid Bypass Format"));
return;
}
// 修改验证规则允许IP和主机名
const ipv4Regex =
/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv6Regex =
/^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
const hostnameRegex =
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
if (
!ipv4Regex.test(value.proxy_host) &&
!ipv6Regex.test(value.proxy_host) &&
!hostnameRegex.test(value.proxy_host)
) {
showNotice("error", t("Invalid Proxy Host Format"));
return;
}
if (value.duration < 1) { showNotice("error", t("Proxy Daemon Duration Cannot be Less than 1 Second")); return; }
if (value.bypass && !validReg.test(value.bypass)) { showNotice("error", t("Invalid Bypass Format")); return; }
const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
const hostnameRegex = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][a-zA-Z0-9\-]*[A-Za-z0-9])$/;
if (!ipv4Regex.test(value.proxy_host) && !ipv6Regex.test(value.proxy_host) && !hostnameRegex.test(value.proxy_host)) { showNotice("error", t("Invalid Proxy Host Format")); return; }
setSaving(true);
setOpen(false);
setSaving(false);
const patch: Partial<IVergeConfig> = {};
if (value.guard !== enable_proxy_guard) {
patch.enable_proxy_guard = value.guard;
}
if (value.duration !== proxy_guard_duration) {
patch.proxy_guard_duration = value.duration;
}
if (value.bypass !== system_proxy_bypass) {
patch.system_proxy_bypass = value.bypass;
}
if (value.pac !== proxy_auto_config) {
patch.proxy_auto_config = value.pac;
}
if (value.use_default !== use_default_bypass) {
patch.use_default_bypass = value.use_default;
}
let proxyHost = value.proxy_host;
if (ipv6Regex.test(proxyHost) && !proxyHost.startsWith("[") && !proxyHost.endsWith("]")) { proxyHost = `[${proxyHost}]`; }
const patch: Partial<IVergeConfig> = {
enable_proxy_guard: value.guard,
proxy_guard_duration: value.duration,
system_proxy_bypass: value.bypass,
proxy_auto_config: value.pac,
use_default_bypass: value.use_default,
proxy_host: proxyHost,
};
let pacContent = value.pac_content;
if (pacContent) {
pacContent = pacContent.replace(/%proxy_host%/g, value.proxy_host);
// 将 mixed-port 转换为字符串
const mixedPortStr = (clashConfig?.["mixed-port"] || "").toString();
pacContent = pacContent.replace(/%mixed-port%/g, mixedPortStr);
}
patch.pac_file_content = pacContent;
if (pacContent !== pac_file_content) {
patch.pac_file_content = pacContent;
try {
await patchVerge(patch);
setTimeout(() => {
if (enabled) resetSystemProxy();
}, 50);
} catch (err: any) {
showNotice("error", err.toString());
} finally {
setSaving(false);
setOpen(false);
}
// 处理IPv6地址如果是IPv6地址但没有被方括号包围则添加方括号
let proxyHost = value.proxy_host;
if (
ipv6Regex.test(proxyHost) &&
!proxyHost.startsWith("[") &&
!proxyHost.endsWith("]")
) {
proxyHost = `[${proxyHost}]`;
}
if (proxyHost !== proxy_host) {
patch.proxy_host = proxyHost;
}
// 判断是否需要重置系统代理
const needResetProxy =
value.pac !== proxy_auto_config ||
proxyHost !== proxy_host ||
pacContent !== pac_file_content ||
value.bypass !== system_proxy_bypass ||
value.use_default !== use_default_bypass;
Promise.resolve().then(async () => {
try {
// 乐观更新本地状态
if (Object.keys(patch).length > 0) {
mutateVerge({ ...verge, ...patch }, false);
}
if (Object.keys(patch).length > 0) {
await patchVerge(patch);
}
setTimeout(async () => {
try {
await Promise.all([
mutate("getSystemProxy"),
mutate("getAutotemProxy"),
]);
// 如果需要重置代理且代理当前启用
if (needResetProxy && enabled) {
const [currentSysProxy, currentAutoProxy] = await Promise.all([
getSystemProxy(),
getAutotemProxy(),
]);
const isProxyActive = value.pac
? currentAutoProxy?.enable
: currentSysProxy?.enable;
if (isProxyActive) {
await patchVergeConfig({ enable_system_proxy: false });
await new Promise((resolve) => setTimeout(resolve, 50));
await patchVergeConfig({ enable_system_proxy: true });
await Promise.all([
mutate("getSystemProxy"),
mutate("getAutotemProxy"),
]);
}
}
} catch (err) {
console.warn("代理状态更新失败:", err);
}
}, 50);
} catch (err: any) {
console.error("配置保存失败:", err);
mutateVerge();
showNotice("error", err.toString());
// setOpen(true);
}
});
});
return (
<BaseDialog
open={open}
title={t("System Proxy Setting")}
contentSx={{ width: 450, maxHeight: 565 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
loading={saving}
disableOk={saving}
>
<List>
<BaseFieldset label={t("Current System Proxy")} padding="15px 10px">
<FlexBox>
<Typography className="label">{t("Enable status")}</Typography>
<Typography className="value">
{value.pac
? autoproxy?.enable
? t("Enabled")
: t("Disabled")
: sysproxy?.enable
? t("Enabled")
: t("Disabled")}
</Typography>
</FlexBox>
{!value.pac && (
<>
<FlexBox>
<Typography className="label">{t("Server Addr")}</Typography>
<Typography className="value">
{getSystemProxyAddress}
</Typography>
</FlexBox>
</>
)}
{value.pac && (
<FlexBox>
<Typography className="label">{t("PAC URL")}</Typography>
<Typography className="value">
{getCurrentPacUrl || "-"}
</Typography>
</FlexBox>
)}
</BaseFieldset>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Proxy Host")} />
<Autocomplete
size="small"
sx={{ width: 150 }}
options={hostOptions}
value={value.proxy_host}
freeSolo
renderInput={(params) => (
<TextField {...params} placeholder="127.0.0.1" size="small" />
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader><DialogTitle>{t("System Proxy Setting")}</DialogTitle></DialogHeader>
<div className="max-h-[70vh] overflow-y-auto space-y-4 py-4 px-1">
<BaseFieldset label={t("Current System Proxy")}>
<div className="text-sm space-y-2">
<div className="flex justify-between"><span className="text-muted-foreground">{t("Enable status")}</span><span>{value.pac ? (autoproxy?.enable ? t("Enabled") : t("Disabled")) : (sysproxy?.enable ? t("Enabled") : t("Disabled"))}</span></div>
{!value.pac && <div className="flex justify-between"><span className="text-muted-foreground">{t("Server Addr")}</span><span className="font-mono">{getSystemProxyAddress}</span></div>}
{value.pac && <div className="flex justify-between"><span className="text-muted-foreground">{t("PAC URL")}</span><span className="font-mono">{getCurrentPacUrl || "-"}</span></div>}
</div>
</BaseFieldset>
<SettingRow label={t("Proxy Host")}>
<Combobox options={hostOptions} value={value.proxy_host} onValueChange={(val) => setValue(v => ({...v, proxy_host: val}))} placeholder="127.0.0.1" />
</SettingRow>
<SettingRow label={t("Use PAC Mode")}>
<Switch disabled={!enabled} checked={value.pac} onCheckedChange={(e) => setValue((v) => ({ ...v, pac: e }))} />
</SettingRow>
<SettingRow label={<>{t("Proxy Guard")} <TooltipIcon tooltip={t("Proxy Guard Info")} /></>}>
<Switch disabled={!enabled} checked={value.guard} onCheckedChange={(e) => setValue((v) => ({ ...v, guard: e }))} />
</SettingRow>
<SettingRow label={t("Guard Duration")}>
<div className="flex items-center gap-2">
<Input disabled={!enabled} type="number" className="w-24 h-8" value={value.duration} onChange={(e) => setValue((v) => ({ ...v, duration: +e.target.value.replace(/\D/, "") }))}/>
<span className="text-sm text-muted-foreground">s</span>
</div>
</SettingRow>
{!value.pac && (
<SettingRow label={t("Always use Default Bypass")}>
<Switch disabled={!enabled} checked={value.use_default} onCheckedChange={(e) => setValue((v) => ({...v, use_default: e, bypass: !e && !v.bypass ? defaultBypass() : v.bypass}))}/>
</SettingRow>
)}
onChange={(_, newValue) => {
setValue((v) => ({
...v,
proxy_host: newValue || "127.0.0.1",
}));
}}
onInputChange={(_, newInputValue) => {
setValue((v) => ({
...v,
proxy_host: newInputValue || "127.0.0.1",
}));
}}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Use PAC Mode")} />
<Switch
edge="end"
disabled={!enabled}
checked={value.pac}
onChange={(_, e) => setValue((v) => ({ ...v, pac: e }))}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText
primary={t("Proxy Guard")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Proxy Guard Info")} sx={{ opacity: "0.7" }} />
<Switch
edge="end"
disabled={!enabled}
checked={value.guard}
onChange={(_, e) => setValue((v) => ({ ...v, guard: e }))}
sx={{ marginLeft: "auto" }}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Guard Duration")} />
<TextField
disabled={!enabled}
size="small"
value={value.duration}
sx={{ width: 100 }}
slotProps={{
input: {
endAdornment: <InputAdornment position="end">s</InputAdornment>,
},
}}
onChange={(e) => {
setValue((v) => ({
...v,
duration: +e.target.value.replace(/\D/, ""),
}));
}}
/>
</ListItem>
{!value.pac && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Always use Default Bypass")} />
<Switch
edge="end"
disabled={!enabled}
checked={value.use_default}
onChange={(_, e) =>
setValue((v) => ({
...v,
use_default: e,
// 当取消选择use_default且当前bypass为空时填充默认值
bypass: !e && !v.bypass ? defaultBypass() : v.bypass,
}))
}
/>
</ListItem>
)}
{!value.pac && !value.use_default && (
<>
<ListItemText primary={t("Proxy Bypass")} />
<TextField
error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled}
size="small"
multiline
rows={4}
sx={{ width: "100%" }}
value={value.bypass}
onChange={(e) => {
setValue((v) => ({ ...v, bypass: e.target.value }));
}}
/>
</>
)}
{!value.pac && value.use_default && (
<>
<ListItemText primary={t("Bypass")} />
<FlexBox>
<TextField
disabled={true}
size="small"
multiline
rows={4}
sx={{ width: "100%" }}
value={defaultBypass()}
/>
</FlexBox>
</>
)}
{value.pac && (
<>
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
<ListItemText
primary={t("PAC Script Content")}
sx={{ padding: "3px 0" }}
/>
<Button
startIcon={<EditRounded />}
variant="outlined"
onClick={() => {
setEditorOpen(true);
}}
>
{t("Edit")} PAC
</Button>
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} PAC`}
initialData={Promise.resolve(value.pac_content ?? "")}
language="javascript"
onSave={(_prev, curr) => {
let pac = DEFAULT_PAC;
if (curr && curr.trim().length > 0) {
pac = curr;
}
setValue((v) => ({ ...v, pac_content: pac }));
}}
onClose={() => setEditorOpen(false)}
/>
)}
</ListItem>
</>
)}
</List>
</BaseDialog>
{!value.pac && !value.use_default && (
<div className="space-y-2">
<Label>{t("Proxy Bypass")}</Label>
<Textarea
id="proxy-bypass"
disabled={!enabled}
rows={4}
value={value.bypass}
onChange={(e) => setValue((v) => ({ ...v, bypass: e.target.value }))}
// Вместо пропса `error` используем условные классы
className={cn(
(value.bypass && !validReg.test(value.bypass)) && "border-destructive focus-visible:ring-destructive"
)}
/>
</div>
)}
{value.pac && (
<SettingRow label={t("PAC Script Content")}>
<Button variant="outline" size="sm" onClick={() => setEditorOpen(true)}><Edit className="mr-2 h-4 w-4"/>{t("Edit")} PAC</Button>
</SettingRow>
)}
</div>
<DialogFooter>
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
<Button type="button" onClick={onSave} disabled={saving}>{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin"/>}{t("Save")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{editorOpen && <EditorViewer open={true} title={`${t("Edit")} PAC`} initialData={Promise.resolve(value.pac_content ?? "")} language="javascript" onSave={(_prev, curr) => { let pac = DEFAULT_PAC; if (curr && curr.trim().length > 0) { pac = curr; } setValue((v) => ({ ...v, pac_content: pac })); }} onClose={() => setEditorOpen(false)} />}
</>
);
});
const FlexBox = styled("div")`
display: flex;
margin-top: 4px;
.label {
flex: none;
//width: 85px;
}
`;