import { forwardRef, useImperativeHandle, useState, useEffect } from "react"; import { useLockFn } from "ahooks"; import yaml from "js-yaml"; import { useTranslation } from "react-i18next"; import MonacoEditor from "react-monaco-editor"; import { invoke } from "@tauri-apps/api/core"; // Новые импорты import { DialogRef } from "@/components/base"; import { useThemeMode } from "@/services/states"; import { showNotice } from "@/services/noticeService"; import getSystem from "@/utils/get-system"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Separator } from "@/components/ui/separator"; import { AlertTriangle, RotateCcw, Code } from "lucide-react"; const DEFAULT_DNS_CONFIG = { enable: true, listen: ":53", "enhanced-mode": "fake-ip" as "fake-ip" | "redir-host", "fake-ip-range": "198.18.0.1/16", "fake-ip-filter-mode": "blacklist" as "blacklist" | "whitelist", "prefer-h3": false, "respect-rules": false, "use-hosts": false, "use-system-hosts": false, ipv6: true, "fake-ip-filter": [ "*.lan", "*.local", "*.arpa", "time.*.com", "ntp.*.com", "+.market.xiaomi.com", "localhost.ptlogin2.qq.com", "*.msftncsi.com", "www.msftconnecttest.com", ], "default-nameserver": [ "system", "8.8.8.8", "1.1.1.1", "2001:4860:4860::8888", ], nameserver: [ "8.8.8.8", "https://doh.pub/dns-query", "https://dns.google/dns-query", "https://cloudflare-dns.com/dns-query", ], fallback: [], "nameserver-policy": {}, "proxy-server-nameserver": [ "https://doh.pub/dns-query", "https://dns.google/dns-query", "https://cloudflare-dns.com/dns-query", "tls://1.1.1.1", ], "direct-nameserver": [], "direct-nameserver-follow-policy": false, "fallback-filter": { geoip: true, "geoip-code": "CN", ipcidr: ["240.0.0.0/4", "0.0.0.0/32"], domain: ["+.google.com", "+.facebook.com", "+.youtube.com"], }, }; interface Props { onSave?: (prev?: string, curr?: string) => void; } // Функция-помощник, которая всегда возвращает состояние в правильном формате (со строками) const formatValues = (config: any = {}): any => { const dnsConfig = config.dns || {}; const hostsConfig = config.hosts || {}; const formatList = (arr: any[] | undefined = []): string => (arr || []).join(", "); const formatHosts = (hosts: any): string => !hosts ? "" : Object.entries(hosts) .map( ([domain, value]) => `${domain}=${Array.isArray(value) ? value.join(";") : value}`, ) .join(", "); const formatNameserverPolicy = (policy: any): string => !policy ? "" : Object.entries(policy) .map( ([domain, servers]) => `${domain}=${Array.isArray(servers) ? servers.join(";") : servers}`, ) .join(", "); const enhancedMode = dnsConfig["enhanced-mode"]; const validEnhancedMode = ["fake-ip", "redir-host"].includes(enhancedMode) ? enhancedMode : DEFAULT_DNS_CONFIG["enhanced-mode"]; const fakeIpFilterMode = dnsConfig["fake-ip-filter-mode"]; const validFakeIpFilterMode = ["blacklist", "whitelist"].includes( fakeIpFilterMode, ) ? fakeIpFilterMode : DEFAULT_DNS_CONFIG["fake-ip-filter-mode"]; return { enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable, listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen, enhancedMode: validEnhancedMode, fakeIpRange: dnsConfig["fake-ip-range"] ?? DEFAULT_DNS_CONFIG["fake-ip-range"], fakeIpFilterMode: validFakeIpFilterMode, preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"], respectRules: dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"], useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"], useSystemHosts: dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"], ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6, fakeIpFilter: formatList( dnsConfig["fake-ip-filter"] ?? DEFAULT_DNS_CONFIG["fake-ip-filter"], ), defaultNameserver: formatList( dnsConfig["default-nameserver"] ?? DEFAULT_DNS_CONFIG["default-nameserver"], ), nameserver: formatList( dnsConfig.nameserver ?? DEFAULT_DNS_CONFIG.nameserver, ), fallback: formatList(dnsConfig.fallback ?? DEFAULT_DNS_CONFIG.fallback), proxyServerNameserver: formatList( dnsConfig["proxy-server-nameserver"] ?? DEFAULT_DNS_CONFIG["proxy-server-nameserver"], ), directNameserver: formatList( dnsConfig["direct-nameserver"] ?? DEFAULT_DNS_CONFIG["direct-nameserver"], ), directNameserverFollowPolicy: dnsConfig["direct-nameserver-follow-policy"] ?? DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"], fallbackGeoip: dnsConfig["fallback-filter"]?.geoip ?? DEFAULT_DNS_CONFIG["fallback-filter"].geoip, fallbackGeoipCode: dnsConfig["fallback-filter"]?.["geoip-code"] ?? DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"], fallbackIpcidr: formatList(dnsConfig["fallback-filter"]?.ipcidr) ?? formatList(DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr), fallbackDomain: formatList(dnsConfig["fallback-filter"]?.domain) ?? formatList(DEFAULT_DNS_CONFIG["fallback-filter"].domain), nameserverPolicy: formatNameserverPolicy(dnsConfig["nameserver-policy"]) || "", hosts: formatHosts(hostsConfig) || "", }; }; export const DnsViewer = forwardRef(({ onSave }, ref) => { const { t } = useTranslation(); const themeMode = useThemeMode(); const [open, setOpen] = useState(false); const [visualization, setVisualization] = useState(true); const [values, setValues] = useState(() => formatValues({ dns: DEFAULT_DNS_CONFIG, hosts: {} }), ); const [yamlContent, setYamlContent] = useState(""); const [prevData, setPrevData] = useState(""); const parseList = (str: string = ""): string[] => str ? str .split(",") .map((s) => s.trim()) .filter(Boolean) : []; const parseHosts = (str: string): Record => str.split(",").reduce( (acc, item) => { const parts = item.trim().split("="); if (parts.length >= 2) { const domain = parts[0].trim(); const valueStr = parts.slice(1).join("=").trim(); acc[domain] = valueStr.includes(";") ? valueStr .split(";") .map((s) => s.trim()) .filter(Boolean) : valueStr; } return acc; }, {} as Record, ); const parseNameserverPolicy = (str: string): Record => str.split(",").reduce( (acc, item) => { const parts = item.trim().split("="); if (parts.length >= 2) { const domain = parts[0].trim(); const serversStr = parts.slice(1).join("=").trim(); acc[domain] = serversStr.includes(";") ? serversStr .split(";") .map((s) => s.trim()) .filter(Boolean) : serversStr; } return acc; }, {} as Record, ); const generateDnsConfig = () => { const dnsConfig: any = { enable: values.enable, listen: values.listen, "enhanced-mode": values.enhancedMode, "fake-ip-range": values.fakeIpRange, "fake-ip-filter-mode": values.fakeIpFilterMode, "prefer-h3": values.preferH3, "respect-rules": values.respectRules, "use-hosts": values.useHosts, "use-system-hosts": values.useSystemHosts, ipv6: values.ipv6, "fake-ip-filter": parseList(values.fakeIpFilter), "default-nameserver": parseList(values.defaultNameserver), nameserver: parseList(values.nameserver), "direct-nameserver-follow-policy": values.directNameserverFollowPolicy, "fallback-filter": { geoip: values.fallbackGeoip, "geoip-code": values.fallbackGeoipCode, ipcidr: parseList(values.fallbackIpcidr), domain: parseList(values.fallbackDomain), }, }; if (values.fallback) dnsConfig["fallback"] = parseList(values.fallback); const policy = parseNameserverPolicy(values.nameserverPolicy); if (Object.keys(policy).length > 0) dnsConfig["nameserver-policy"] = policy; if (values.proxyServerNameserver) dnsConfig["proxy-server-nameserver"] = parseList( values.proxyServerNameserver, ); if (values.directNameserver) dnsConfig["direct-nameserver"] = parseList(values.directNameserver); return dnsConfig; }; const updateYamlFromValues = () => { const config: Record = {}; const dnsConfig = generateDnsConfig(); if (Object.keys(dnsConfig).length > 0) { config.dns = dnsConfig; } const hosts = parseHosts(values.hosts); if (Object.keys(hosts).length > 0) { config.hosts = hosts; } setYamlContent(yaml.dump(config, { forceQuotes: true })); }; const resetToDefaults = () => { const defaultConfig = { dns: DEFAULT_DNS_CONFIG, hosts: {} }; setValues(formatValues(defaultConfig)); updateYamlFromValues(); }; const initDnsConfig = async () => { try { const dnsConfigExists = await invoke("check_dns_config_exists"); if (dnsConfigExists) { const content = await invoke("get_dns_config_content"); const config = yaml.load(content) as any; setValues(formatValues(config)); setYamlContent(content); setPrevData(content); } else { resetToDefaults(); } } catch (err) { console.error("Failed to initialize DNS config", err); resetToDefaults(); } }; const handleSave = useLockFn(async () => { try { let finalConfig: Record; if (visualization) { finalConfig = { dns: generateDnsConfig(), hosts: parseHosts(values.hosts), }; } else { const parsed = yaml.load(yamlContent); if (typeof parsed !== "object" || parsed === null) throw new Error(t("Invalid configuration")); finalConfig = parsed as Record; } const currentData = yaml.dump(finalConfig, { forceQuotes: true }); await invoke("save_dns_config", { dnsConfig: finalConfig }); const [isValid, errorMsg] = await invoke<[boolean, string]>( "validate_dns_config", {}, ); if (!isValid) { const cleanErrorMsg = errorMsg.split(/msg="([^"]+)"/)[1] || errorMsg; showNotice( "error", t("DNS configuration error") + ": " + cleanErrorMsg, ); return; } onSave?.(prevData, currentData); setOpen(false); showNotice("success", t("DNS settings saved")); } catch (err: any) { showNotice("error", err.message || err.toString()); } }); const handleChange = (field: keyof typeof values) => (e: React.ChangeEvent | string) => { const value = typeof e === "string" ? e : e.target.type === "checkbox" ? (e.target as any).checked : e.target.value; setValues((prev: any) => ({ ...prev, [field]: value })); }; const handleSwitchChange = (field: keyof typeof values) => (checked: boolean) => { setValues((prev: any) => ({ ...prev, [field]: checked })); }; useEffect(() => { if (visualization && open) updateYamlFromValues(); }, [values, visualization, open]); useImperativeHandle(ref, () => ({ open: () => { setOpen(true); initDnsConfig(); }, close: () => setOpen(false), })); return ( {/* --- НАЧАЛО ИЗМЕНЕНИЙ --- */} {/* Добавляем классы flex-wrap и gap-y-2 для корректного переноса */}
{t("DNS Overwrite")}
{/* --- КОНЕЦ ИЗМЕНЕНИЙ --- */}
{visualization ? (
{t("Warning")} {t("DNS Settings Warning")}

{t("DNS Settings")}