refactor: frontend (#5068)
* refactor: setting components * refactor: frontend * fix: settings router
This commit is contained in:
@@ -37,7 +37,7 @@ export const WindowControls = forwardRef(function WindowControls(props, ref) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
// 通过前端对 tauri 窗口进行翻转全屏时会短暂地与系统图标重叠渲染。
|
||||||
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准
|
// 这可能是上游缺陷,保险起见跨平台以窗口的最大化翻转为准。
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -71,10 +71,11 @@ export const BackupConfigViewer = memo(
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (webdav_url && webdav_username && webdav_password) {
|
if (!webdav_url || !webdav_username || !webdav_password) {
|
||||||
onInit();
|
return;
|
||||||
}
|
}
|
||||||
}, []);
|
void onInit();
|
||||||
|
}, [webdav_url, webdav_username, webdav_password, onInit]);
|
||||||
|
|
||||||
const checkForm = () => {
|
const checkForm = () => {
|
||||||
const username = usernameRef.current?.value;
|
const username = usernameRef.current?.value;
|
||||||
|
|||||||
@@ -103,94 +103,97 @@ export const BackupTableViewer = memo(
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{datasource.length > 0 ? (
|
{datasource.length > 0 ? (
|
||||||
datasource?.map((file, index) => (
|
datasource.map((file) => {
|
||||||
<TableRow key={index}>
|
const rowKey = `${file.platform}-${file.filename}-${file.backup_time.valueOf()}`;
|
||||||
<TableCell component="th" scope="row">
|
return (
|
||||||
{file.platform === "windows" ? (
|
<TableRow key={rowKey}>
|
||||||
<WindowsIcon className="h-full w-full" />
|
<TableCell component="th" scope="row">
|
||||||
) : file.platform === "linux" ? (
|
{file.platform === "windows" ? (
|
||||||
<LinuxIcon className="h-full w-full" />
|
<WindowsIcon className="h-full w-full" />
|
||||||
) : (
|
) : file.platform === "linux" ? (
|
||||||
<MacIcon className="h-full w-full" />
|
<LinuxIcon className="h-full w-full" />
|
||||||
)}
|
) : (
|
||||||
{file.filename}
|
<MacIcon className="h-full w-full" />
|
||||||
</TableCell>
|
|
||||||
<TableCell align="center">
|
|
||||||
{file.backup_time.fromNow()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{onExport && (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
color="primary"
|
|
||||||
aria-label={t("Export")}
|
|
||||||
size="small"
|
|
||||||
title={t("Export Backup")}
|
|
||||||
onClick={async (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
await handleExport(file.filename);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DownloadIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Divider
|
|
||||||
orientation="vertical"
|
|
||||||
flexItem
|
|
||||||
sx={{ mx: 1, height: 24 }}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<IconButton
|
{file.filename}
|
||||||
color="secondary"
|
</TableCell>
|
||||||
aria-label={t("Delete")}
|
<TableCell align="center">
|
||||||
size="small"
|
{file.backup_time.fromNow()}
|
||||||
title={t("Delete Backup")}
|
</TableCell>
|
||||||
onClick={async (e: React.MouseEvent) => {
|
<TableCell align="right">
|
||||||
e.preventDefault();
|
<Box
|
||||||
const confirmed = await window.confirm(
|
sx={{
|
||||||
t("Confirm to delete this backup file?"),
|
display: "flex",
|
||||||
);
|
alignItems: "center",
|
||||||
if (confirmed) {
|
justifyContent: "flex-end",
|
||||||
await handleDelete(file.filename);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DeleteIcon />
|
{onExport && (
|
||||||
</IconButton>
|
<>
|
||||||
<Divider
|
<IconButton
|
||||||
orientation="vertical"
|
color="primary"
|
||||||
flexItem
|
aria-label={t("Export")}
|
||||||
sx={{ mx: 1, height: 24 }}
|
size="small"
|
||||||
/>
|
title={t("Export Backup")}
|
||||||
<IconButton
|
onClick={async (e: React.MouseEvent) => {
|
||||||
color="primary"
|
e.preventDefault();
|
||||||
aria-label={t("Restore")}
|
await handleExport(file.filename);
|
||||||
size="small"
|
}}
|
||||||
title={t("Restore Backup")}
|
>
|
||||||
disabled={!file.allow_apply}
|
<DownloadIcon />
|
||||||
onClick={async (e: React.MouseEvent) => {
|
</IconButton>
|
||||||
e.preventDefault();
|
<Divider
|
||||||
const confirmed = await window.confirm(
|
orientation="vertical"
|
||||||
t("Confirm to restore this backup file?"),
|
flexItem
|
||||||
);
|
sx={{ mx: 1, height: 24 }}
|
||||||
if (confirmed) {
|
/>
|
||||||
await handleRestore(file.filename);
|
</>
|
||||||
}
|
)}
|
||||||
}}
|
<IconButton
|
||||||
>
|
color="secondary"
|
||||||
<RestoreIcon />
|
aria-label={t("Delete")}
|
||||||
</IconButton>
|
size="small"
|
||||||
</Box>
|
title={t("Delete Backup")}
|
||||||
</TableCell>
|
onClick={(e: React.MouseEvent) => {
|
||||||
</TableRow>
|
e.preventDefault();
|
||||||
))
|
const confirmed = window.confirm(
|
||||||
|
t("Confirm to delete this backup file?"),
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
void handleDelete(file.filename);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Divider
|
||||||
|
orientation="vertical"
|
||||||
|
flexItem
|
||||||
|
sx={{ mx: 1, height: 24 }}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
aria-label={t("Restore")}
|
||||||
|
size="small"
|
||||||
|
title={t("Restore Backup")}
|
||||||
|
disabled={!file.allow_apply}
|
||||||
|
onClick={(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
t("Confirm to restore this backup file?"),
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
void handleRestore(file.filename);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RestoreIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} align="center">
|
<TableCell colSpan={3} align="center">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useReducer,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
@@ -36,16 +37,20 @@ dayjs.extend(customParseFormat);
|
|||||||
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
|
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
|
||||||
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
|
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
|
||||||
type BackupSource = "local" | "webdav";
|
type BackupSource = "local" | "webdav";
|
||||||
|
type CloseButtonPosition = { top: number; left: number } | null;
|
||||||
|
|
||||||
export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [dialogPaper, setDialogPaper] = useState<HTMLElement | null>(null);
|
const [dialogPaper, setDialogPaper] = useReducer(
|
||||||
const [closeButtonPosition, setCloseButtonPosition] = useState<{
|
(_: HTMLElement | null, next: HTMLElement | null) => next,
|
||||||
top: number;
|
null as HTMLElement | null,
|
||||||
left: number;
|
);
|
||||||
} | null>(null);
|
const [closeButtonPosition, setCloseButtonPosition] = useReducer(
|
||||||
|
(_: CloseButtonPosition, next: CloseButtonPosition) => next,
|
||||||
|
null as CloseButtonPosition,
|
||||||
|
);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
|
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
|
||||||
|
|||||||
@@ -16,7 +16,14 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import type { Ref } from "react";
|
import type { Ref } from "react";
|
||||||
import { useEffect, useImperativeHandle, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import MonacoEditor from "react-monaco-editor";
|
import MonacoEditor from "react-monaco-editor";
|
||||||
|
|
||||||
@@ -35,6 +42,91 @@ const Item = styled(ListItem)(() => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
type NameserverPolicy = Record<string, any>;
|
||||||
|
|
||||||
|
function parseNameserverPolicy(str: string): NameserverPolicy {
|
||||||
|
const result: NameserverPolicy = {};
|
||||||
|
if (!str) return result;
|
||||||
|
|
||||||
|
const ruleRegex = /\s*([^=]+?)\s*=\s*([^,]+)(?:,|$)/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
|
||||||
|
while ((match = ruleRegex.exec(str)) !== null) {
|
||||||
|
const [, domainsPart, serversPart] = match;
|
||||||
|
|
||||||
|
const domains = [domainsPart.trim()];
|
||||||
|
const servers = serversPart.split(";").map((s) => s.trim());
|
||||||
|
|
||||||
|
domains.forEach((domain) => {
|
||||||
|
result[domain] = servers;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNameserverPolicy(policy: unknown): string {
|
||||||
|
if (!policy || typeof policy !== "object") return "";
|
||||||
|
|
||||||
|
return Object.entries(policy as Record<string, unknown>)
|
||||||
|
.map(([domain, servers]) => {
|
||||||
|
const serversStr = Array.isArray(servers) ? servers.join(";") : servers;
|
||||||
|
return `${domain}=${serversStr}`;
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHosts(hosts: unknown): string {
|
||||||
|
if (!hosts || typeof hosts !== "object") return "";
|
||||||
|
|
||||||
|
const result: string[] = [];
|
||||||
|
|
||||||
|
Object.entries(hosts as Record<string, unknown>).forEach(
|
||||||
|
([domain, value]) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const ipsStr = value.join(";");
|
||||||
|
result.push(`${domain}=${ipsStr}`);
|
||||||
|
} else {
|
||||||
|
result.push(`${domain}=${value}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHosts(str: string): NameserverPolicy {
|
||||||
|
const result: NameserverPolicy = {};
|
||||||
|
if (!str) return result;
|
||||||
|
|
||||||
|
str.split(",").forEach((item) => {
|
||||||
|
const parts = item.trim().split("=");
|
||||||
|
if (parts.length < 2) return;
|
||||||
|
|
||||||
|
const domain = parts[0].trim();
|
||||||
|
const valueStr = parts.slice(1).join("=").trim();
|
||||||
|
|
||||||
|
if (valueStr.includes(";")) {
|
||||||
|
result[domain] = valueStr
|
||||||
|
.split(";")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
} else {
|
||||||
|
result[domain] = valueStr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseList(str: string): string[] {
|
||||||
|
if (!str?.trim()) return [];
|
||||||
|
return str
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
// 默认DNS配置
|
// 默认DNS配置
|
||||||
const DEFAULT_DNS_CONFIG = {
|
const DEFAULT_DNS_CONFIG = {
|
||||||
enable: true,
|
enable: true,
|
||||||
@@ -95,6 +187,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [visualization, setVisualization] = useState(true);
|
const [visualization, setVisualization] = useState(true);
|
||||||
|
const skipYamlSyncRef = useRef(false);
|
||||||
const [values, setValues] = useState<{
|
const [values, setValues] = useState<{
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
listen: string;
|
listen: string;
|
||||||
@@ -150,304 +243,91 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 用于YAML编辑模式
|
// 用于YAML编辑模式
|
||||||
const [yamlContent, setYamlContent] = useState("");
|
const [yamlContent, setYamlContent] = useReducer(
|
||||||
|
(_: string, next: string) => next,
|
||||||
useImperativeHandle(ref, () => ({
|
"",
|
||||||
open: () => {
|
);
|
||||||
setOpen(true);
|
|
||||||
// 获取DNS配置文件并初始化表单
|
|
||||||
initDnsConfig();
|
|
||||||
},
|
|
||||||
close: () => setOpen(false),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 初始化DNS配置
|
|
||||||
const initDnsConfig = async () => {
|
|
||||||
try {
|
|
||||||
// 尝试从dns_config.yaml文件读取配置
|
|
||||||
const dnsConfigExists = await invoke<boolean>(
|
|
||||||
"check_dns_config_exists",
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dnsConfigExists) {
|
|
||||||
// 如果存在配置文件,加载其内容
|
|
||||||
const dnsConfig = await invoke<string>("get_dns_config_content", {});
|
|
||||||
const config = yaml.load(dnsConfig) as any;
|
|
||||||
|
|
||||||
// 更新表单数据
|
|
||||||
updateValuesFromConfig(config);
|
|
||||||
// 更新YAML编辑器内容
|
|
||||||
setYamlContent(dnsConfig);
|
|
||||||
} else {
|
|
||||||
// 如果不存在配置文件,使用默认值
|
|
||||||
resetToDefaults();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to initialize DNS config", err);
|
|
||||||
resetToDefaults();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从配置对象更新表单值
|
// 从配置对象更新表单值
|
||||||
const updateValuesFromConfig = (config: any) => {
|
const updateValuesFromConfig = useCallback(
|
||||||
if (!config) return;
|
(config: any) => {
|
||||||
|
if (!config) return;
|
||||||
|
|
||||||
// 提取dns配置
|
const dnsConfig = config.dns || {};
|
||||||
const dnsConfig = config.dns || {};
|
const hostsConfig = config.hosts || {};
|
||||||
// 提取hosts配置(与dns同级)
|
|
||||||
const hostsConfig = config.hosts || {};
|
|
||||||
|
|
||||||
const enhancedMode =
|
const enhancedMode =
|
||||||
dnsConfig["enhanced-mode"] || DEFAULT_DNS_CONFIG["enhanced-mode"];
|
dnsConfig["enhanced-mode"] || DEFAULT_DNS_CONFIG["enhanced-mode"];
|
||||||
const validEnhancedMode =
|
const validEnhancedMode =
|
||||||
enhancedMode === "fake-ip" || enhancedMode === "redir-host"
|
enhancedMode === "fake-ip" || enhancedMode === "redir-host"
|
||||||
? enhancedMode
|
? enhancedMode
|
||||||
: DEFAULT_DNS_CONFIG["enhanced-mode"];
|
: DEFAULT_DNS_CONFIG["enhanced-mode"];
|
||||||
|
|
||||||
const fakeIpFilterMode =
|
const fakeIpFilterMode =
|
||||||
dnsConfig["fake-ip-filter-mode"] ||
|
dnsConfig["fake-ip-filter-mode"] ||
|
||||||
DEFAULT_DNS_CONFIG["fake-ip-filter-mode"];
|
DEFAULT_DNS_CONFIG["fake-ip-filter-mode"];
|
||||||
const validFakeIpFilterMode =
|
const validFakeIpFilterMode =
|
||||||
fakeIpFilterMode === "blacklist" || fakeIpFilterMode === "whitelist"
|
fakeIpFilterMode === "blacklist" || fakeIpFilterMode === "whitelist"
|
||||||
? fakeIpFilterMode
|
? fakeIpFilterMode
|
||||||
: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"];
|
: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"];
|
||||||
|
|
||||||
setValues({
|
setValues({
|
||||||
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
|
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
|
||||||
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
|
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
|
||||||
enhancedMode: validEnhancedMode,
|
enhancedMode: validEnhancedMode,
|
||||||
fakeIpRange:
|
fakeIpRange:
|
||||||
dnsConfig["fake-ip-range"] ?? DEFAULT_DNS_CONFIG["fake-ip-range"],
|
dnsConfig["fake-ip-range"] ?? DEFAULT_DNS_CONFIG["fake-ip-range"],
|
||||||
fakeIpFilterMode: validFakeIpFilterMode,
|
fakeIpFilterMode: validFakeIpFilterMode,
|
||||||
preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"],
|
preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"],
|
||||||
respectRules:
|
respectRules:
|
||||||
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
|
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
|
||||||
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
|
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
|
||||||
useSystemHosts:
|
useSystemHosts:
|
||||||
dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
|
dnsConfig["use-system-hosts"] ??
|
||||||
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
|
DEFAULT_DNS_CONFIG["use-system-hosts"],
|
||||||
fakeIpFilter:
|
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
|
||||||
dnsConfig["fake-ip-filter"]?.join(", ") ??
|
fakeIpFilter:
|
||||||
DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
dnsConfig["fake-ip-filter"]?.join(", ") ??
|
||||||
nameserver:
|
DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
||||||
dnsConfig.nameserver?.join(", ") ??
|
nameserver:
|
||||||
DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
dnsConfig.nameserver?.join(", ") ??
|
||||||
fallback:
|
DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
||||||
dnsConfig.fallback?.join(", ") ??
|
fallback:
|
||||||
DEFAULT_DNS_CONFIG.fallback.join(", "),
|
dnsConfig.fallback?.join(", ") ??
|
||||||
defaultNameserver:
|
DEFAULT_DNS_CONFIG.fallback.join(", "),
|
||||||
dnsConfig["default-nameserver"]?.join(", ") ??
|
defaultNameserver:
|
||||||
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
dnsConfig["default-nameserver"]?.join(", ") ??
|
||||||
proxyServerNameserver:
|
DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
||||||
dnsConfig["proxy-server-nameserver"]?.join(", ") ??
|
proxyServerNameserver:
|
||||||
(DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || ""),
|
dnsConfig["proxy-server-nameserver"]?.join(", ") ??
|
||||||
directNameserver:
|
(DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || ""),
|
||||||
dnsConfig["direct-nameserver"]?.join(", ") ??
|
directNameserver:
|
||||||
(DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || ""),
|
dnsConfig["direct-nameserver"]?.join(", ") ??
|
||||||
directNameserverFollowPolicy:
|
(DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || ""),
|
||||||
dnsConfig["direct-nameserver-follow-policy"] ??
|
directNameserverFollowPolicy:
|
||||||
DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"],
|
dnsConfig["direct-nameserver-follow-policy"] ??
|
||||||
fallbackGeoip:
|
DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"],
|
||||||
dnsConfig["fallback-filter"]?.geoip ??
|
fallbackGeoip:
|
||||||
DEFAULT_DNS_CONFIG["fallback-filter"].geoip,
|
dnsConfig["fallback-filter"]?.geoip ??
|
||||||
fallbackGeoipCode:
|
DEFAULT_DNS_CONFIG["fallback-filter"].geoip,
|
||||||
dnsConfig["fallback-filter"]?.["geoip-code"] ??
|
fallbackGeoipCode:
|
||||||
DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"],
|
dnsConfig["fallback-filter"]?.["geoip-code"] ??
|
||||||
fallbackIpcidr:
|
DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"],
|
||||||
dnsConfig["fallback-filter"]?.ipcidr?.join(", ") ??
|
fallbackIpcidr:
|
||||||
DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr.join(", "),
|
dnsConfig["fallback-filter"]?.ipcidr?.join(", ") ??
|
||||||
fallbackDomain:
|
DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr.join(", "),
|
||||||
dnsConfig["fallback-filter"]?.domain?.join(", ") ??
|
fallbackDomain:
|
||||||
DEFAULT_DNS_CONFIG["fallback-filter"].domain.join(", "),
|
dnsConfig["fallback-filter"]?.domain?.join(", ") ??
|
||||||
nameserverPolicy:
|
DEFAULT_DNS_CONFIG["fallback-filter"].domain.join(", "),
|
||||||
formatNameserverPolicy(dnsConfig["nameserver-policy"]) || "",
|
nameserverPolicy:
|
||||||
hosts: formatHosts(hostsConfig) || "",
|
formatNameserverPolicy(dnsConfig["nameserver-policy"]) || "",
|
||||||
});
|
hosts: formatHosts(hostsConfig) || "",
|
||||||
};
|
|
||||||
|
|
||||||
// 重置为默认值
|
|
||||||
const resetToDefaults = () => {
|
|
||||||
setValues({
|
|
||||||
enable: DEFAULT_DNS_CONFIG.enable,
|
|
||||||
listen: DEFAULT_DNS_CONFIG.listen,
|
|
||||||
enhancedMode: DEFAULT_DNS_CONFIG["enhanced-mode"],
|
|
||||||
fakeIpRange: DEFAULT_DNS_CONFIG["fake-ip-range"],
|
|
||||||
fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"],
|
|
||||||
preferH3: DEFAULT_DNS_CONFIG["prefer-h3"],
|
|
||||||
respectRules: DEFAULT_DNS_CONFIG["respect-rules"],
|
|
||||||
useHosts: DEFAULT_DNS_CONFIG["use-hosts"],
|
|
||||||
useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"],
|
|
||||||
ipv6: DEFAULT_DNS_CONFIG.ipv6,
|
|
||||||
fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
|
||||||
defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
|
||||||
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
|
||||||
fallback: DEFAULT_DNS_CONFIG.fallback.join(", "),
|
|
||||||
proxyServerNameserver:
|
|
||||||
DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "",
|
|
||||||
directNameserver:
|
|
||||||
DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || "",
|
|
||||||
directNameserverFollowPolicy:
|
|
||||||
DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"] || false,
|
|
||||||
fallbackGeoip: DEFAULT_DNS_CONFIG["fallback-filter"].geoip,
|
|
||||||
fallbackGeoipCode: DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"],
|
|
||||||
fallbackIpcidr:
|
|
||||||
DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr?.join(", ") || "",
|
|
||||||
fallbackDomain:
|
|
||||||
DEFAULT_DNS_CONFIG["fallback-filter"].domain?.join(", ") || "",
|
|
||||||
nameserverPolicy: "",
|
|
||||||
hosts: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新YAML编辑器内容
|
|
||||||
updateYamlFromValues();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从表单值更新YAML内容
|
|
||||||
const updateYamlFromValues = () => {
|
|
||||||
const config: Record<string, any> = {};
|
|
||||||
|
|
||||||
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 }));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从YAML更新表单值
|
|
||||||
const updateValuesFromYaml = () => {
|
|
||||||
try {
|
|
||||||
const parsedYaml = yaml.load(yamlContent) as any;
|
|
||||||
if (!parsedYaml) return;
|
|
||||||
|
|
||||||
updateValuesFromConfig(parsedYaml);
|
|
||||||
} catch {
|
|
||||||
showNotice("error", t("Invalid YAML format"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解析nameserver-policy为对象
|
|
||||||
const parseNameserverPolicy = (str: string): Record<string, any> => {
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
if (!str) return result;
|
|
||||||
|
|
||||||
// 处理geosite:xxx,yyy格式
|
|
||||||
const ruleRegex = /\s*([^=]+?)\s*=\s*([^,]+)(?:,|$)/g;
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = ruleRegex.exec(str)) !== null) {
|
|
||||||
const [, domainsPart, serversPart] = match;
|
|
||||||
|
|
||||||
// 处理域名部分
|
|
||||||
let domains;
|
|
||||||
if (domainsPart.startsWith("geosite:")) {
|
|
||||||
domains = [domainsPart.trim()];
|
|
||||||
} else {
|
|
||||||
domains = [domainsPart.trim()];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理服务器部分
|
|
||||||
const servers = serversPart.split(";").map((s) => s.trim());
|
|
||||||
|
|
||||||
// 为每个域名组分配相同的服务器列表
|
|
||||||
domains.forEach((domain) => {
|
|
||||||
result[domain] = servers;
|
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
[setValues],
|
||||||
|
);
|
||||||
|
|
||||||
return result;
|
const generateDnsConfig = useCallback(() => {
|
||||||
};
|
|
||||||
|
|
||||||
// 格式化nameserver-policy为字符串
|
|
||||||
const formatNameserverPolicy = (policy: any): string => {
|
|
||||||
if (!policy || typeof policy !== "object") return "";
|
|
||||||
|
|
||||||
// 直接将对象转换为字符串格式
|
|
||||||
return Object.entries(policy)
|
|
||||||
.map(([domain, servers]) => {
|
|
||||||
const serversStr = Array.isArray(servers) ? servers.join(";") : servers;
|
|
||||||
return `${domain}=${serversStr}`;
|
|
||||||
})
|
|
||||||
.join(", ");
|
|
||||||
};
|
|
||||||
|
|
||||||
// 格式化hosts为字符串
|
|
||||||
const formatHosts = (hosts: any): string => {
|
|
||||||
if (!hosts || typeof hosts !== "object") return "";
|
|
||||||
|
|
||||||
const result: string[] = [];
|
|
||||||
|
|
||||||
Object.entries(hosts).forEach(([domain, value]) => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
// 处理数组格式的IP
|
|
||||||
const ipsStr = value.join(";");
|
|
||||||
result.push(`${domain}=${ipsStr}`);
|
|
||||||
} else {
|
|
||||||
// 处理单个IP或域名
|
|
||||||
result.push(`${domain}=${value}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.join(", ");
|
|
||||||
};
|
|
||||||
|
|
||||||
// 解析hosts字符串为对象
|
|
||||||
const parseHosts = (str: string): Record<string, any> => {
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
if (!str) return result;
|
|
||||||
|
|
||||||
str.split(",").forEach((item) => {
|
|
||||||
const parts = item.trim().split("=");
|
|
||||||
if (parts.length < 2) return;
|
|
||||||
|
|
||||||
const domain = parts[0].trim();
|
|
||||||
const valueStr = parts.slice(1).join("=").trim();
|
|
||||||
|
|
||||||
// 检查是否包含多个分号分隔的IP
|
|
||||||
if (valueStr.includes(";")) {
|
|
||||||
result[domain] = valueStr
|
|
||||||
.split(";")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
} else {
|
|
||||||
result[domain] = valueStr;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化时设置默认YAML
|
|
||||||
useEffect(() => {
|
|
||||||
updateYamlFromValues();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 切换编辑模式时的处理
|
|
||||||
useEffect(() => {
|
|
||||||
if (visualization) {
|
|
||||||
updateValuesFromYaml();
|
|
||||||
} else {
|
|
||||||
updateYamlFromValues();
|
|
||||||
}
|
|
||||||
}, [visualization]);
|
|
||||||
|
|
||||||
// 解析列表字符串为数组
|
|
||||||
const parseList = (str: string): string[] => {
|
|
||||||
if (!str?.trim()) return [];
|
|
||||||
return str
|
|
||||||
.split(",")
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生成DNS配置对象
|
|
||||||
const generateDnsConfig = () => {
|
|
||||||
const dnsConfig: any = {
|
const dnsConfig: any = {
|
||||||
enable: values.enable,
|
enable: values.enable,
|
||||||
listen: values.listen,
|
listen: values.listen,
|
||||||
@@ -481,8 +361,132 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return dnsConfig;
|
return dnsConfig;
|
||||||
};
|
}, [values]);
|
||||||
|
|
||||||
|
const updateYamlFromValues = useCallback(() => {
|
||||||
|
const config: Record<string, any> = {};
|
||||||
|
|
||||||
|
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 }));
|
||||||
|
}, [generateDnsConfig, setYamlContent, values.hosts]);
|
||||||
|
|
||||||
|
// 重置为默认值
|
||||||
|
const resetToDefaults = useCallback(() => {
|
||||||
|
setValues({
|
||||||
|
enable: DEFAULT_DNS_CONFIG.enable,
|
||||||
|
listen: DEFAULT_DNS_CONFIG.listen,
|
||||||
|
enhancedMode: DEFAULT_DNS_CONFIG["enhanced-mode"],
|
||||||
|
fakeIpRange: DEFAULT_DNS_CONFIG["fake-ip-range"],
|
||||||
|
fakeIpFilterMode: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"],
|
||||||
|
preferH3: DEFAULT_DNS_CONFIG["prefer-h3"],
|
||||||
|
respectRules: DEFAULT_DNS_CONFIG["respect-rules"],
|
||||||
|
useHosts: DEFAULT_DNS_CONFIG["use-hosts"],
|
||||||
|
useSystemHosts: DEFAULT_DNS_CONFIG["use-system-hosts"],
|
||||||
|
ipv6: DEFAULT_DNS_CONFIG.ipv6,
|
||||||
|
fakeIpFilter: DEFAULT_DNS_CONFIG["fake-ip-filter"].join(", "),
|
||||||
|
defaultNameserver: DEFAULT_DNS_CONFIG["default-nameserver"].join(", "),
|
||||||
|
nameserver: DEFAULT_DNS_CONFIG.nameserver.join(", "),
|
||||||
|
fallback: DEFAULT_DNS_CONFIG.fallback.join(", "),
|
||||||
|
proxyServerNameserver:
|
||||||
|
DEFAULT_DNS_CONFIG["proxy-server-nameserver"]?.join(", ") || "",
|
||||||
|
directNameserver:
|
||||||
|
DEFAULT_DNS_CONFIG["direct-nameserver"]?.join(", ") || "",
|
||||||
|
directNameserverFollowPolicy:
|
||||||
|
DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"] || false,
|
||||||
|
fallbackGeoip: DEFAULT_DNS_CONFIG["fallback-filter"].geoip,
|
||||||
|
fallbackGeoipCode: DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"],
|
||||||
|
fallbackIpcidr:
|
||||||
|
DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr?.join(", ") || "",
|
||||||
|
fallbackDomain:
|
||||||
|
DEFAULT_DNS_CONFIG["fallback-filter"].domain?.join(", ") || "",
|
||||||
|
nameserverPolicy: "",
|
||||||
|
hosts: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
updateYamlFromValues();
|
||||||
|
}, [setValues, updateYamlFromValues]);
|
||||||
|
|
||||||
|
// 从YAML更新表单值
|
||||||
|
const updateValuesFromYaml = useCallback(() => {
|
||||||
|
try {
|
||||||
|
const parsedYaml = yaml.load(yamlContent) as any;
|
||||||
|
if (!parsedYaml) return;
|
||||||
|
|
||||||
|
skipYamlSyncRef.current = true;
|
||||||
|
updateValuesFromConfig(parsedYaml);
|
||||||
|
} catch {
|
||||||
|
showNotice("error", t("Invalid YAML format"));
|
||||||
|
}
|
||||||
|
}, [yamlContent, t, updateValuesFromConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (skipYamlSyncRef.current) {
|
||||||
|
skipYamlSyncRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateYamlFromValues();
|
||||||
|
}, [updateYamlFromValues]);
|
||||||
|
|
||||||
|
const latestUpdateValuesFromYamlRef = useRef(updateValuesFromYaml);
|
||||||
|
const latestUpdateYamlFromValuesRef = useRef(updateYamlFromValues);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestUpdateValuesFromYamlRef.current = updateValuesFromYaml;
|
||||||
|
latestUpdateYamlFromValuesRef.current = updateYamlFromValues;
|
||||||
|
}, [updateValuesFromYaml, updateYamlFromValues]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visualization) {
|
||||||
|
latestUpdateValuesFromYamlRef.current();
|
||||||
|
} else {
|
||||||
|
latestUpdateYamlFromValuesRef.current();
|
||||||
|
}
|
||||||
|
}, [visualization]);
|
||||||
|
|
||||||
|
const initDnsConfig = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const dnsConfigExists = await invoke<boolean>(
|
||||||
|
"check_dns_config_exists",
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dnsConfigExists) {
|
||||||
|
const dnsConfig = await invoke<string>("get_dns_config_content", {});
|
||||||
|
const config = yaml.load(dnsConfig) as any;
|
||||||
|
|
||||||
|
updateValuesFromConfig(config);
|
||||||
|
setYamlContent(dnsConfig);
|
||||||
|
} else {
|
||||||
|
resetToDefaults();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to initialize DNS config", err);
|
||||||
|
resetToDefaults();
|
||||||
|
}
|
||||||
|
}, [resetToDefaults, setYamlContent, updateValuesFromConfig]);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
open: () => {
|
||||||
|
setOpen(true);
|
||||||
|
void initDnsConfig();
|
||||||
|
},
|
||||||
|
close: () => setOpen(false),
|
||||||
|
}),
|
||||||
|
[initDnsConfig],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 生成DNS配置对象
|
||||||
// 处理保存操作
|
// 处理保存操作
|
||||||
const onSave = useLockFn(async () => {
|
const onSave = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Delete as DeleteIcon } from "@mui/icons-material";
|
import { Delete as DeleteIcon } from "@mui/icons-material";
|
||||||
import { Box, Button, Divider, List, ListItem, TextField } from "@mui/material";
|
import { Box, Button, Divider, List, ListItem, TextField } from "@mui/material";
|
||||||
import { useLockFn, useRequest } from "ahooks";
|
import { useLockFn, useRequest } from "ahooks";
|
||||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { BaseDialog, Switch } from "@/components/base";
|
import { BaseDialog, Switch } from "@/components/base";
|
||||||
@@ -165,6 +165,19 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
await saveConfig();
|
await saveConfig();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const originEntries = useMemo(() => {
|
||||||
|
const counts: Record<string, number> = {};
|
||||||
|
return corsConfig.allowOrigins.map((origin, index) => {
|
||||||
|
const occurrence = (counts[origin] = (counts[origin] ?? 0) + 1);
|
||||||
|
const keyBase = origin || "origin";
|
||||||
|
return {
|
||||||
|
origin,
|
||||||
|
index,
|
||||||
|
key: `${keyBase}-${occurrence}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [corsConfig.allowOrigins]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
@@ -207,9 +220,9 @@ export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|||||||
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
|
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
|
||||||
{t("Allowed Origins")}
|
{t("Allowed Origins")}
|
||||||
</div>
|
</div>
|
||||||
{corsConfig.allowOrigins.map((origin, index) => (
|
{originEntries.map(({ origin, index, key }) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={key}
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cloneElement, isValidElement, ReactNode, useRef } from "react";
|
import { createElement, isValidElement, ReactNode, useRef } from "react";
|
||||||
|
|
||||||
import noop from "@/utils/noop";
|
import noop from "@/utils/noop";
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export function GuardState<T>(props: Props<T>) {
|
|||||||
onGuard = noop,
|
onGuard = noop,
|
||||||
onCatch = noop,
|
onCatch = noop,
|
||||||
onChange = noop,
|
onChange = noop,
|
||||||
onFormat = (v: T) => v,
|
onFormat,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const lockRef = useRef(false);
|
const lockRef = useRef(false);
|
||||||
@@ -45,7 +45,7 @@ export function GuardState<T>(props: Props<T>) {
|
|||||||
lockRef.current = true;
|
lockRef.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newValue = (onFormat as any)(...args);
|
const newValue = onFormat ? (onFormat as any)(...args) : (args[0] as T);
|
||||||
// 先在ui上响应操作
|
// 先在ui上响应操作
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
|
|
||||||
@@ -81,5 +81,7 @@ export function GuardState<T>(props: Props<T>) {
|
|||||||
}
|
}
|
||||||
lockRef.current = false;
|
lockRef.current = false;
|
||||||
};
|
};
|
||||||
return cloneElement(children, childProps);
|
const { children: nestedChildren, ...restProps } = childProps;
|
||||||
|
|
||||||
|
return createElement(children.type, restProps, nestedChildren);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
const { verge, patchVerge } = useVerge();
|
const { verge, patchVerge } = useVerge();
|
||||||
|
|
||||||
const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
|
const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
|
||||||
const [enableGlobalHotkey, setEnableHotkey] = useState(
|
const [enableGlobalHotkey, setEnableGlobalHotkey] = useState(
|
||||||
verge?.enable_global_hotkey ?? true,
|
verge?.enable_global_hotkey ?? true,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={enableGlobalHotkey}
|
checked={enableGlobalHotkey}
|
||||||
onChange={(e) => setEnableHotkey(e.target.checked)}
|
onChange={(e) => setEnableGlobalHotkey(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ItemWrapper>
|
</ItemWrapper>
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
valueProps="checked"
|
valueProps="checked"
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
onFormat={onSwitchFormat}
|
onFormat={onSwitchFormat}
|
||||||
onChange={async (e) => {
|
onChange={async () => {
|
||||||
await toggleDecorations();
|
await toggleDecorations();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -198,8 +198,8 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
value={verge?.menu_icon ?? "monochrome"}
|
value={verge?.menu_icon ?? "monochrome"}
|
||||||
onCatch={onError}
|
onCatch={onError}
|
||||||
onFormat={(e: any) => e.target.value}
|
onFormat={(e: any) => e.target.value}
|
||||||
onChange={(e) => onChangeData({ menu_icon: e })}
|
onChange={(value) => onChangeData({ menu_icon: value })}
|
||||||
onGuard={(e) => patchVerge({ menu_icon: e })}
|
onGuard={(value) => patchVerge({ menu_icon: value })}
|
||||||
>
|
>
|
||||||
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
|
||||||
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
|
<MenuItem value="monochrome">{t("Monochrome")}</MenuItem>
|
||||||
|
|||||||
@@ -102,7 +102,13 @@ export function NetworkInterfaceViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddressDisplay = (props: { label: string; content: string }) => {
|
const AddressDisplay = ({
|
||||||
|
label,
|
||||||
|
content,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
content: string;
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -113,7 +119,7 @@ const AddressDisplay = (props: { label: string; content: string }) => {
|
|||||||
margin: "8px 0",
|
margin: "8px 0",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>{props.label}</Box>
|
<Box>{label}</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
@@ -124,13 +130,11 @@ const AddressDisplay = (props: { label: string; content: string }) => {
|
|||||||
: alpha(palette.grey[400], 0.3),
|
: alpha(palette.grey[400], 0.3),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "inline", userSelect: "text" }}>
|
<Box sx={{ display: "inline", userSelect: "text" }}>{content}</Box>
|
||||||
{props.content}
|
|
||||||
</Box>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await writeText(props.content);
|
await writeText(content);
|
||||||
showNotice("success", t("Copy Success"));
|
showNotice("success", t("Copy Success"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
TextField,
|
TextField,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -19,10 +19,6 @@ export const PasswordInput = (props: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [passwd, setPasswd] = useState("");
|
const [passwd, setPasswd] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) return;
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} maxWidth="xs" fullWidth>
|
<Dialog open={true} maxWidth="xs" fullWidth>
|
||||||
<DialogTitle>{t("Please enter your root password")}</DialogTitle>
|
<DialogTitle>{t("Please enter your root password")}</DialogTitle>
|
||||||
|
|||||||
@@ -20,8 +20,13 @@ interface ItemProps {
|
|||||||
onClick?: () => void | Promise<any>;
|
onClick?: () => void | Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingItem: React.FC<ItemProps> = (props) => {
|
export const SettingItem: React.FC<ItemProps> = ({
|
||||||
const { label, extra, children, secondary, onClick } = props;
|
label,
|
||||||
|
extra,
|
||||||
|
children,
|
||||||
|
secondary,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
const clickable = !!onClick;
|
const clickable = !!onClick;
|
||||||
|
|
||||||
const primary = (
|
const primary = (
|
||||||
@@ -65,7 +70,7 @@ export const SettingItem: React.FC<ItemProps> = (props) => {
|
|||||||
export const SettingList: React.FC<{
|
export const SettingList: React.FC<{
|
||||||
title: string;
|
title: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}> = (props) => (
|
}> = ({ title, children }) => (
|
||||||
<List>
|
<List>
|
||||||
<ListSubheader
|
<ListSubheader
|
||||||
sx={[
|
sx={[
|
||||||
@@ -78,9 +83,9 @@ export const SettingList: React.FC<{
|
|||||||
]}
|
]}
|
||||||
disableSticky
|
disableSticky
|
||||||
>
|
>
|
||||||
{props.title}
|
{title}
|
||||||
</ListSubheader>
|
</ListSubheader>
|
||||||
|
|
||||||
{props.children}
|
{children}
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -38,6 +39,11 @@ import {
|
|||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from "@/utils/get-system";
|
||||||
|
|
||||||
|
const sleep = (ms: number) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
|
||||||
const DEFAULT_PAC = `function FindProxyForURL(url, host) {
|
const DEFAULT_PAC = `function FindProxyForURL(url, host) {
|
||||||
return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;";
|
return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;";
|
||||||
}`;
|
}`;
|
||||||
@@ -130,40 +136,37 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
|
|||||||
errorRetryInterval: 5000,
|
errorRetryInterval: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [prevMixedPort, setPrevMixedPort] = useState(clashConfig?.mixedPort);
|
const prevMixedPortRef = useRef(clashConfig?.mixedPort);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (clashConfig?.mixedPort && clashConfig.mixedPort !== prevMixedPort) {
|
const mixedPort = clashConfig?.mixedPort;
|
||||||
setPrevMixedPort(clashConfig.mixedPort);
|
if (!mixedPort || mixedPort === prevMixedPortRef.current) {
|
||||||
resetSystemProxy();
|
return;
|
||||||
}
|
}
|
||||||
}, [clashConfig?.mixedPort]);
|
|
||||||
|
|
||||||
const resetSystemProxy = async () => {
|
prevMixedPortRef.current = mixedPort;
|
||||||
try {
|
|
||||||
const currentSysProxy = await getSystemProxy();
|
|
||||||
const currentAutoProxy = await getAutotemProxy();
|
|
||||||
|
|
||||||
if (value.pac ? currentAutoProxy?.enable : currentSysProxy?.enable) {
|
const updateProxy = async () => {
|
||||||
// 临时关闭系统代理
|
try {
|
||||||
await patchVergeConfig({ enable_system_proxy: false });
|
const currentSysProxy = await getSystemProxy();
|
||||||
|
const currentAutoProxy = await getAutotemProxy();
|
||||||
|
|
||||||
// 减少等待时间
|
if (value.pac ? currentAutoProxy?.enable : currentSysProxy?.enable) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
await patchVergeConfig({ enable_system_proxy: false });
|
||||||
|
await sleep(200);
|
||||||
// 重新开启系统代理
|
await patchVergeConfig({ enable_system_proxy: true });
|
||||||
await patchVergeConfig({ enable_system_proxy: true });
|
await Promise.all([
|
||||||
|
mutate("getSystemProxy"),
|
||||||
// 更新UI状态
|
mutate("getAutotemProxy"),
|
||||||
await Promise.all([
|
]);
|
||||||
mutate("getSystemProxy"),
|
}
|
||||||
mutate("getAutotemProxy"),
|
} catch (err: any) {
|
||||||
]);
|
showNotice("error", err.toString());
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
};
|
||||||
showNotice("error", err.toString());
|
|
||||||
}
|
updateProxy();
|
||||||
};
|
}, [clashConfig?.mixedPort, value.pac]);
|
||||||
|
|
||||||
const { systemProxyAddress } = useAppData();
|
const { systemProxyAddress } = useAppData();
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -38,6 +38,14 @@ export const WebUIItem = (props: Props) => {
|
|||||||
const [editValue, setEditValue] = useState(value);
|
const [editValue, setEditValue] = useState(value);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const highlightedParts = useMemo(() => {
|
||||||
|
const placeholderRegex = /(%host|%port|%secret)/g;
|
||||||
|
if (!value) {
|
||||||
|
return ["NULL"];
|
||||||
|
}
|
||||||
|
return value.split(placeholderRegex).filter((part) => part !== "");
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
if (editing || onlyEdit) {
|
if (editing || onlyEdit) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -78,10 +86,26 @@ export const WebUIItem = (props: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const html = value
|
const placeholderCounts: Record<string, number> = {};
|
||||||
?.replace("%host", "<span>%host</span>")
|
let textCounter = 0;
|
||||||
.replace("%port", "<span>%port</span>")
|
const renderedParts = highlightedParts.map((part) => {
|
||||||
.replace("%secret", "<span>%secret</span>");
|
const isPlaceholder =
|
||||||
|
part === "%host" || part === "%port" || part === "%secret";
|
||||||
|
|
||||||
|
if (isPlaceholder) {
|
||||||
|
const count = placeholderCounts[part] ?? 0;
|
||||||
|
placeholderCounts[part] = count + 1;
|
||||||
|
return (
|
||||||
|
<span key={`placeholder-${part}-${count}`} className="placeholder">
|
||||||
|
{part}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `text-${textCounter}-${part || "empty"}`;
|
||||||
|
textCounter += 1;
|
||||||
|
return <span key={key}>{part}</span>;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -94,12 +118,13 @@ export const WebUIItem = (props: Props) => {
|
|||||||
sx={({ palette }) => ({
|
sx={({ palette }) => ({
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
"> span": {
|
"> .placeholder": {
|
||||||
color: palette.primary.main,
|
color: palette.primary.main,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
dangerouslySetInnerHTML={{ __html: html || "NULL" }}
|
>
|
||||||
/>
|
{renderedParts}
|
||||||
|
</Typography>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title={t("Open URL")}
|
title={t("Open URL")}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Box, Button, Typography } from "@mui/material";
|
import { Box, Button, Typography } from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import type { Ref } from "react";
|
import type { Ref } from "react";
|
||||||
import { useImperativeHandle, useState } from "react";
|
import { useImperativeHandle, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { BaseDialog, BaseEmpty, DialogRef } from "@/components/base";
|
import { BaseDialog, BaseEmpty, DialogRef } from "@/components/base";
|
||||||
@@ -12,6 +12,12 @@ import { showNotice } from "@/services/noticeService";
|
|||||||
|
|
||||||
import { WebUIItem } from "./web-ui-item";
|
import { WebUIItem } from "./web-ui-item";
|
||||||
|
|
||||||
|
const DEFAULT_WEB_UI_LIST = [
|
||||||
|
"https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret",
|
||||||
|
"https://yacd.metacubex.one/?hostname=%host&port=%port&secret=%secret",
|
||||||
|
"https://board.zash.run.place/#/setup?http=true&hostname=%host&port=%port&secret=%secret",
|
||||||
|
];
|
||||||
|
|
||||||
export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -26,11 +32,21 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
close: () => setOpen(false),
|
close: () => setOpen(false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const webUIList = verge?.web_ui_list || [
|
const webUIList = verge?.web_ui_list || DEFAULT_WEB_UI_LIST;
|
||||||
"https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret",
|
|
||||||
"https://yacd.metacubex.one/?hostname=%host&port=%port&secret=%secret",
|
const webUIEntries = useMemo(() => {
|
||||||
"https://board.zash.run.place/#/setup?http=true&hostname=%host&port=%port&secret=%secret",
|
const counts: Record<string, number> = {};
|
||||||
];
|
return webUIList.map((item, index) => {
|
||||||
|
const keyBase = item && item.trim().length > 0 ? item : "entry";
|
||||||
|
const count = counts[keyBase] ?? 0;
|
||||||
|
counts[keyBase] = count + 1;
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
key: `${keyBase}-${count}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [webUIList]);
|
||||||
|
|
||||||
const handleAdd = useLockFn(async (value: string) => {
|
const handleAdd = useLockFn(async (value: string) => {
|
||||||
const newList = [...webUIList, value];
|
const newList = [...webUIList, value];
|
||||||
@@ -118,9 +134,9 @@ export function WebUIViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{webUIList.map((item, index) => (
|
{webUIEntries.map(({ item, index, key }) => (
|
||||||
<WebUIItem
|
<WebUIItem
|
||||||
key={index}
|
key={key}
|
||||||
value={item}
|
value={item}
|
||||||
onChange={(v) => handleChange(index, v)}
|
onChange={(v) => handleChange(index, v)}
|
||||||
onDelete={() => handleDelete(index)}
|
onDelete={() => handleDelete(index)}
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ interface Props {
|
|||||||
onDelete: (uid: string) => void;
|
onDelete: (uid: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TestItem = (props: Props) => {
|
export const TestItem = ({
|
||||||
const { itemData, onEdit, onDelete: onDeleteItem } = props;
|
id,
|
||||||
|
itemData,
|
||||||
|
onEdit,
|
||||||
|
onDelete: removeTest,
|
||||||
|
}: Props) => {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@@ -33,7 +37,7 @@ export const TestItem = (props: Props) => {
|
|||||||
transition,
|
transition,
|
||||||
isDragging,
|
isDragging,
|
||||||
} = useSortable({
|
} = useSortable({
|
||||||
id: props.id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -50,17 +54,19 @@ export const TestItem = (props: Props) => {
|
|||||||
setDelay(result);
|
setDelay(result);
|
||||||
}, [url]);
|
}, [url]);
|
||||||
|
|
||||||
useEffect(() => {
|
const initIconCachePath = useCallback(async () => {
|
||||||
initIconCachePath();
|
|
||||||
}, [icon]);
|
|
||||||
|
|
||||||
async function initIconCachePath() {
|
|
||||||
if (icon && icon.trim().startsWith("http")) {
|
if (icon && icon.trim().startsWith("http")) {
|
||||||
const fileName = uid + "-" + getFileName(icon);
|
const fileName = uid + "-" + getFileName(icon);
|
||||||
const iconPath = await downloadIconCache(icon, fileName);
|
const iconPath = await downloadIconCache(icon, fileName);
|
||||||
setIconCachePath(convertFileSrc(iconPath));
|
setIconCachePath(convertFileSrc(iconPath));
|
||||||
|
} else {
|
||||||
|
setIconCachePath("");
|
||||||
}
|
}
|
||||||
}
|
}, [icon, uid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void initIconCachePath();
|
||||||
|
}, [initIconCachePath]);
|
||||||
|
|
||||||
function getFileName(url: string) {
|
function getFileName(url: string) {
|
||||||
return url.substring(url.lastIndexOf("/") + 1);
|
return url.substring(url.lastIndexOf("/") + 1);
|
||||||
@@ -74,7 +80,7 @@ export const TestItem = (props: Props) => {
|
|||||||
const onDelete = useLockFn(async () => {
|
const onDelete = useLockFn(async () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
try {
|
try {
|
||||||
onDeleteItem(uid);
|
removeTest(uid);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showNotice("error", err.message || err.toString());
|
showNotice("error", err.message || err.toString());
|
||||||
}
|
}
|
||||||
@@ -102,12 +108,12 @@ export const TestItem = (props: Props) => {
|
|||||||
return () => {
|
return () => {
|
||||||
if (unlistenFn) {
|
if (unlistenFn) {
|
||||||
console.log(
|
console.log(
|
||||||
`TestItem for ${props.id} unmounting or url changed, cleaning up test-all listener.`,
|
`TestItem for ${id} unmounting or url changed, cleaning up test-all listener.`,
|
||||||
);
|
);
|
||||||
unlistenFn();
|
unlistenFn();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [url, addListener, onDelay, props.id]);
|
}, [url, addListener, onDelay, id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -26,12 +26,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { verge, patchVerge } = useVerge();
|
const { verge, patchVerge } = useVerge();
|
||||||
const testList = verge?.test_list ?? [];
|
const testList = verge?.test_list ?? [];
|
||||||
const {
|
const { control, ...formIns } = useForm<IVergeTestItem>({
|
||||||
control,
|
|
||||||
watch: _watch,
|
|
||||||
register: _register,
|
|
||||||
...formIns
|
|
||||||
} = useForm<IVergeTestItem>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
icon: "",
|
icon: "",
|
||||||
|
|||||||
@@ -133,11 +133,22 @@ export const useLogData = () => {
|
|||||||
mutate(`$sub$${subscriptKey}`);
|
mutate(`$sub$${subscriptKey}`);
|
||||||
}, [date, subscriptKey]);
|
}, [date, subscriptKey]);
|
||||||
|
|
||||||
|
const previousLogLevel = useRef<string | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logLevel) return;
|
if (!logLevel) {
|
||||||
|
previousLogLevel.current = logLevel ?? undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousLogLevel.current === logLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousLogLevel.current = logLevel;
|
||||||
ws.current?.close();
|
ws.current?.close();
|
||||||
setDate(Date.now());
|
setDate(Date.now());
|
||||||
}, [logLevel]);
|
}, [logLevel, setDate]);
|
||||||
|
|
||||||
const refreshGetClashLog = (clear = false) => {
|
const refreshGetClashLog = (clear = false) => {
|
||||||
if (clear) {
|
if (clear) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import { useEffect, useRef, useCallback, useReducer } from "react";
|
||||||
|
|
||||||
// import { useClashInfo } from "@/hooks/use-clash";
|
// import { useClashInfo } from "@/hooks/use-clash";
|
||||||
// import { useVisibility } from "@/hooks/use-visibility";
|
// import { useVisibility } from "@/hooks/use-visibility";
|
||||||
@@ -196,12 +196,11 @@ export const useTrafficMonitorEnhanced = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, forceUpdate] = useState({});
|
const [, forceRender] = useReducer((version: number) => version + 1, 0);
|
||||||
const cleanupRef = useRef<(() => void) | null>(null);
|
const cleanupRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
// 强制组件更新
|
const bumpRenderVersion = useCallback(() => {
|
||||||
const triggerUpdate = useCallback(() => {
|
forceRender();
|
||||||
forceUpdate({});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 注册引用计数
|
// 注册引用计数
|
||||||
@@ -250,9 +249,8 @@ export const useTrafficMonitorEnhanced = () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
globalSampler.addDataPoint(dataPoint);
|
globalSampler.addDataPoint(dataPoint);
|
||||||
triggerUpdate();
|
|
||||||
}
|
}
|
||||||
}, [traffic, triggerUpdate]);
|
}, [traffic]);
|
||||||
|
|
||||||
// const { data: monitorData, error } = useSWR<ISystemMonitorOverview>(
|
// const { data: monitorData, error } = useSWR<ISystemMonitorOverview>(
|
||||||
// shouldFetch ? "getSystemMonitorOverviewSafe" : null,
|
// shouldFetch ? "getSystemMonitorOverviewSafe" : null,
|
||||||
@@ -328,9 +326,9 @@ export const useTrafficMonitorEnhanced = () => {
|
|||||||
const clearData = useCallback(() => {
|
const clearData = useCallback(() => {
|
||||||
if (globalSampler) {
|
if (globalSampler) {
|
||||||
globalSampler.clear();
|
globalSampler.clear();
|
||||||
triggerUpdate();
|
bumpRenderVersion();
|
||||||
}
|
}
|
||||||
}, [triggerUpdate]);
|
}, [bumpRenderVersion]);
|
||||||
|
|
||||||
// 获取采样器统计信息
|
// 获取采样器统计信息
|
||||||
const getSamplerStats = useCallback(() => {
|
const getSamplerStats = useCallback(() => {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const useVisibility = () => {
|
export const useVisibility = () => {
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(() =>
|
||||||
|
typeof document === "undefined"
|
||||||
|
? true
|
||||||
|
: document.visibilityState === "visible",
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => {
|
||||||
@@ -9,16 +13,15 @@ export const useVisibility = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = () => setVisible(true);
|
const handleFocus = () => setVisible(true);
|
||||||
const handleClick = () => setVisible(true);
|
const handlePointerDown = () => setVisible(true);
|
||||||
|
|
||||||
handleVisibilityChange();
|
|
||||||
document.addEventListener("focus", handleFocus);
|
document.addEventListener("focus", handleFocus);
|
||||||
document.addEventListener("pointerdown", handleClick);
|
document.addEventListener("pointerdown", handlePointerDown);
|
||||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("focus", handleFocus);
|
document.removeEventListener("focus", handleFocus);
|
||||||
document.removeEventListener("pointerdown", handleClick);
|
document.removeEventListener("pointerdown", handlePointerDown);
|
||||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
46
src/hooks/use-window.ts
Normal file
46
src/hooks/use-window.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { use } from "react";
|
||||||
|
|
||||||
|
import { WindowContext, type WindowContextType } from "@/providers/window";
|
||||||
|
|
||||||
|
export const useWindow = () => {
|
||||||
|
const context = use(WindowContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useWindow must be used within WindowProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWindowControls = () => {
|
||||||
|
const {
|
||||||
|
maximized,
|
||||||
|
minimize,
|
||||||
|
toggleMaximize,
|
||||||
|
close,
|
||||||
|
toggleFullscreen,
|
||||||
|
currentWindow,
|
||||||
|
} = useWindow();
|
||||||
|
return {
|
||||||
|
maximized,
|
||||||
|
minimize,
|
||||||
|
toggleMaximize,
|
||||||
|
close,
|
||||||
|
toggleFullscreen,
|
||||||
|
currentWindow,
|
||||||
|
} satisfies Pick<
|
||||||
|
WindowContextType,
|
||||||
|
| "maximized"
|
||||||
|
| "minimize"
|
||||||
|
| "toggleMaximize"
|
||||||
|
| "close"
|
||||||
|
| "toggleFullscreen"
|
||||||
|
| "currentWindow"
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWindowDecorations = () => {
|
||||||
|
const { decorated, toggleDecorations, refreshDecorated } = useWindow();
|
||||||
|
return { decorated, toggleDecorations, refreshDecorated } satisfies Pick<
|
||||||
|
WindowContextType,
|
||||||
|
"decorated" | "toggleDecorations" | "refreshDecorated"
|
||||||
|
>;
|
||||||
|
};
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
||||||
import { debounce } from "lodash-es";
|
|
||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
use,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
interface WindowContextType {
|
|
||||||
decorated: boolean | null;
|
|
||||||
maximized: boolean | null;
|
|
||||||
toggleDecorations: () => Promise<void>;
|
|
||||||
refreshDecorated: () => Promise<boolean>;
|
|
||||||
minimize: () => void;
|
|
||||||
close: () => void;
|
|
||||||
toggleMaximize: () => Promise<void>;
|
|
||||||
toggleFullscreen: () => Promise<void>;
|
|
||||||
currentWindow: ReturnType<typeof getCurrentWindow>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WindowContext = createContext<WindowContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const currentWindow = getCurrentWindow();
|
|
||||||
const [decorated, setDecorated] = useState<boolean | null>(null);
|
|
||||||
const [maximized, setMaximized] = useState<boolean | null>(null);
|
|
||||||
|
|
||||||
const close = useCallback(() => currentWindow.close(), [currentWindow]);
|
|
||||||
const minimize = useCallback(() => currentWindow.minimize(), [currentWindow]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkMaximized = debounce(async () => {
|
|
||||||
const value = await currentWindow.isMaximized();
|
|
||||||
if (maximized !== value) {
|
|
||||||
setMaximized(value);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
const unlistenResize = currentWindow.onResized(checkMaximized);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unlistenResize.then((fn) => fn());
|
|
||||||
};
|
|
||||||
}, [currentWindow, maximized]);
|
|
||||||
|
|
||||||
const toggleMaximize = useCallback(async () => {
|
|
||||||
if (await currentWindow.isMaximized()) {
|
|
||||||
await currentWindow.unmaximize();
|
|
||||||
setMaximized(false);
|
|
||||||
} else {
|
|
||||||
await currentWindow.maximize();
|
|
||||||
setMaximized(true);
|
|
||||||
}
|
|
||||||
}, [currentWindow]);
|
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(async () => {
|
|
||||||
await currentWindow.setFullscreen(!(await currentWindow.isFullscreen()));
|
|
||||||
}, [currentWindow]);
|
|
||||||
|
|
||||||
const refreshDecorated = useCallback(async () => {
|
|
||||||
const val = await currentWindow.isDecorated();
|
|
||||||
setDecorated(val);
|
|
||||||
return val;
|
|
||||||
}, [currentWindow]);
|
|
||||||
|
|
||||||
const toggleDecorations = useCallback(async () => {
|
|
||||||
const currentVal = await currentWindow.isDecorated();
|
|
||||||
await currentWindow.setDecorations(!currentVal);
|
|
||||||
setDecorated(!currentVal);
|
|
||||||
}, [currentWindow]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refreshDecorated();
|
|
||||||
currentWindow.setMinimizable?.(true);
|
|
||||||
}, [currentWindow, refreshDecorated]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WindowContext
|
|
||||||
value={{
|
|
||||||
decorated,
|
|
||||||
maximized,
|
|
||||||
toggleDecorations,
|
|
||||||
refreshDecorated,
|
|
||||||
minimize,
|
|
||||||
close,
|
|
||||||
toggleMaximize,
|
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</WindowContext>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useWindow = () => {
|
|
||||||
const context = use(WindowContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error("useWindow must be used within WindowProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useWindowControls = () => {
|
|
||||||
const {
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
toggleMaximize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
|
||||||
} = useWindow();
|
|
||||||
return {
|
|
||||||
maximized,
|
|
||||||
minimize,
|
|
||||||
toggleMaximize,
|
|
||||||
close,
|
|
||||||
toggleFullscreen,
|
|
||||||
currentWindow,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useWindowDecorations = () => {
|
|
||||||
const { decorated, toggleDecorations, refreshDecorated } = useWindow();
|
|
||||||
return { decorated, toggleDecorations, refreshDecorated };
|
|
||||||
};
|
|
||||||
@@ -10,9 +10,9 @@ import { BrowserRouter } from "react-router-dom";
|
|||||||
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
import { MihomoWebSocket } from "tauri-plugin-mihomo-api";
|
||||||
|
|
||||||
import { BaseErrorBoundary } from "./components/base";
|
import { BaseErrorBoundary } from "./components/base";
|
||||||
import { WindowProvider } from "./hooks/use-window";
|
|
||||||
import Layout from "./pages/_layout";
|
import Layout from "./pages/_layout";
|
||||||
import { AppDataProvider } from "./providers/app-data-provider";
|
import { AppDataProvider } from "./providers/app-data-provider";
|
||||||
|
import { WindowProvider } from "./providers/window";
|
||||||
import { initializeLanguage } from "./services/i18n";
|
import { initializeLanguage } from "./services/i18n";
|
||||||
import {
|
import {
|
||||||
LoadingCacheProvider,
|
LoadingCacheProvider,
|
||||||
|
|||||||
@@ -4,13 +4,7 @@ import { listen } from "@tauri-apps/api/event";
|
|||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import React, {
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useNavigate, useRoutes } from "react-router-dom";
|
import { useLocation, useNavigate, useRoutes } from "react-router-dom";
|
||||||
import { SWRConfig, mutate } from "swr";
|
import { SWRConfig, mutate } from "swr";
|
||||||
@@ -19,11 +13,11 @@ import iconDark from "@/assets/image/icon_dark.svg?react";
|
|||||||
import iconLight from "@/assets/image/icon_light.svg?react";
|
import iconLight from "@/assets/image/icon_light.svg?react";
|
||||||
import LogoSvg from "@/assets/image/logo.svg?react";
|
import LogoSvg from "@/assets/image/logo.svg?react";
|
||||||
import { NoticeManager } from "@/components/base/NoticeManager";
|
import { NoticeManager } from "@/components/base/NoticeManager";
|
||||||
|
import { WindowControls } from "@/components/controller/window-controller";
|
||||||
import { LayoutItem } from "@/components/layout/layout-item";
|
import { LayoutItem } from "@/components/layout/layout-item";
|
||||||
import { LayoutTraffic } from "@/components/layout/layout-traffic";
|
import { LayoutTraffic } from "@/components/layout/layout-traffic";
|
||||||
import { UpdateButton } from "@/components/layout/update-button";
|
import { UpdateButton } from "@/components/layout/update-button";
|
||||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||||
import { useClashInfo } from "@/hooks/use-clash";
|
|
||||||
import { useConnectionData } from "@/hooks/use-connection-data";
|
import { useConnectionData } from "@/hooks/use-connection-data";
|
||||||
import { useI18n } from "@/hooks/use-i18n";
|
import { useI18n } from "@/hooks/use-i18n";
|
||||||
import { useListen } from "@/hooks/use-listen";
|
import { useListen } from "@/hooks/use-listen";
|
||||||
@@ -34,7 +28,7 @@ import { useVerge } from "@/hooks/use-verge";
|
|||||||
import { useWindowDecorations } from "@/hooks/use-window";
|
import { useWindowDecorations } from "@/hooks/use-window";
|
||||||
import { getAxios } from "@/services/api";
|
import { getAxios } from "@/services/api";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { useClashLog, useThemeMode } from "@/services/states";
|
import { useThemeMode } from "@/services/states";
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from "@/utils/get-system";
|
||||||
|
|
||||||
import { routers } from "./_routers";
|
import { routers } from "./_routers";
|
||||||
@@ -42,9 +36,6 @@ import { routers } from "./_routers";
|
|||||||
import "dayjs/locale/ru";
|
import "dayjs/locale/ru";
|
||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
|
|
||||||
import { WindowControls } from "@/components/controller/window-controller";
|
|
||||||
// 删除重复导入
|
|
||||||
|
|
||||||
const appWindow = getCurrentWebviewWindow();
|
const appWindow = getCurrentWebviewWindow();
|
||||||
export const portableFlag = false;
|
export const portableFlag = false;
|
||||||
|
|
||||||
@@ -170,19 +161,25 @@ const Layout = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { theme } = useCustomTheme();
|
const { theme } = useCustomTheme();
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { clashInfo } = useClashInfo();
|
|
||||||
const [clashLog] = useClashLog();
|
|
||||||
const enableLog = clashLog.enable;
|
|
||||||
const logLevel = clashLog.logLevel;
|
|
||||||
// const [logLevel] = useLocalStorage<LogLevel>("log:log-level", "info");
|
|
||||||
const { language, start_page } = verge ?? {};
|
const { language, start_page } = verge ?? {};
|
||||||
const { switchLanguage } = useI18n();
|
const { switchLanguage } = useI18n();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const routersEles = useRoutes(routers);
|
const matchedElement = useRoutes(routers);
|
||||||
|
const routersEles = useMemo(() => {
|
||||||
|
if (!matchedElement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<React.Fragment key={location.pathname}>{matchedElement}</React.Fragment>
|
||||||
|
);
|
||||||
|
}, [matchedElement, location.pathname]);
|
||||||
const { addListener } = useListen();
|
const { addListener } = useListen();
|
||||||
const initRef = useRef(false);
|
const initRef = useRef(false);
|
||||||
const [themeReady, setThemeReady] = useState(false);
|
const overlayRemovedRef = useRef(false);
|
||||||
|
const lastStartPageRef = useRef<string | null>(null);
|
||||||
|
const startPageAppliedRef = useRef(false);
|
||||||
|
const themeReady = useMemo(() => Boolean(theme), [theme]);
|
||||||
|
|
||||||
const windowControls = useRef<any>(null);
|
const windowControls = useRef<any>(null);
|
||||||
const { decorated } = useWindowDecorations();
|
const { decorated } = useWindowDecorations();
|
||||||
@@ -207,33 +204,102 @@ const Layout = () => {
|
|||||||
}, [decorated, mode]);
|
}, [decorated, mode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setThemeReady(true);
|
if (!themeReady || overlayRemovedRef.current) {
|
||||||
}, [theme]);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fadeTimer: number | null = null;
|
||||||
|
let retryTimer: number | null = null;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 50;
|
||||||
|
let stopped = false;
|
||||||
|
|
||||||
|
const tryRemoveOverlay = () => {
|
||||||
|
if (stopped || overlayRemovedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlay = document.getElementById("initial-loading-overlay");
|
||||||
|
if (overlay) {
|
||||||
|
overlayRemovedRef.current = true;
|
||||||
|
overlay.style.opacity = "0";
|
||||||
|
overlay.style.pointerEvents = "none";
|
||||||
|
|
||||||
|
fadeTimer = window.setTimeout(() => {
|
||||||
|
try {
|
||||||
|
overlay.remove();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Layout] Failed to remove loading overlay:", error);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempts < maxAttempts) {
|
||||||
|
attempts += 1;
|
||||||
|
retryTimer = window.setTimeout(tryRemoveOverlay, 100);
|
||||||
|
} else {
|
||||||
|
console.warn("[Layout] Loading overlay not found after retries");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tryRemoveOverlay();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopped = true;
|
||||||
|
if (fadeTimer) {
|
||||||
|
window.clearTimeout(fadeTimer);
|
||||||
|
}
|
||||||
|
if (retryTimer) {
|
||||||
|
window.clearTimeout(retryTimer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [themeReady]);
|
||||||
|
|
||||||
const handleNotice = useCallback(
|
const handleNotice = useCallback(
|
||||||
(payload: [string, string]) => {
|
(payload: [string, string]) => {
|
||||||
const [status, msg] = payload;
|
const [status, msg] = payload;
|
||||||
setTimeout(() => {
|
try {
|
||||||
try {
|
handleNoticeMessage(status, msg, t, navigate);
|
||||||
handleNoticeMessage(status, msg, t, navigate);
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error("[Layout] 处理通知消息失败:", error);
|
||||||
console.error("[Layout] 处理通知消息失败:", error);
|
}
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
},
|
},
|
||||||
[t, navigate],
|
[t, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化全局日志服务
|
// 设置监听
|
||||||
// useEffect(() => {
|
|
||||||
// if (clashInfo) {
|
|
||||||
// initGlobalLogService(enableLog, logLevel);
|
|
||||||
// }
|
|
||||||
// }, [clashInfo, enableLog, logLevel]);
|
|
||||||
|
|
||||||
// 设置监听器
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listeners = [
|
const unlisteners: Array<() => void> = [];
|
||||||
|
let disposed = false;
|
||||||
|
|
||||||
|
const register = (
|
||||||
|
maybeUnlisten: void | (() => void) | Promise<void | (() => void)>,
|
||||||
|
) => {
|
||||||
|
if (!maybeUnlisten) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof maybeUnlisten === "function") {
|
||||||
|
unlisteners.push(maybeUnlisten);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
maybeUnlisten
|
||||||
|
.then((unlisten) => {
|
||||||
|
if (!unlisten) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (disposed) {
|
||||||
|
unlisten();
|
||||||
|
} else {
|
||||||
|
unlisteners.push(unlisten);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("[Layout] 注册事件监听失败", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
register(
|
||||||
addListener("verge://refresh-clash-config", async () => {
|
addListener("verge://refresh-clash-config", async () => {
|
||||||
await getAxios(true);
|
await getAxios(true);
|
||||||
mutate("getProxies");
|
mutate("getProxies");
|
||||||
@@ -241,67 +307,48 @@ const Layout = () => {
|
|||||||
mutate("getClashConfig");
|
mutate("getClashConfig");
|
||||||
mutate("getProxyProviders");
|
mutate("getProxyProviders");
|
||||||
}),
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
register(
|
||||||
addListener("verge://refresh-verge-config", () => {
|
addListener("verge://refresh-verge-config", () => {
|
||||||
mutate("getVergeConfig");
|
mutate("getVergeConfig");
|
||||||
mutate("getSystemProxy");
|
mutate("getSystemProxy");
|
||||||
mutate("getAutotemProxy");
|
mutate("getAutotemProxy");
|
||||||
// 运行模式变更时也需要刷新相关状态
|
|
||||||
mutate("getRunningMode");
|
mutate("getRunningMode");
|
||||||
mutate("isServiceAvailable");
|
mutate("isServiceAvailable");
|
||||||
}),
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
register(
|
||||||
addListener("verge://notice-message", ({ payload }) =>
|
addListener("verge://notice-message", ({ payload }) =>
|
||||||
handleNotice(payload as [string, string]),
|
handleNotice(payload as [string, string]),
|
||||||
),
|
),
|
||||||
];
|
);
|
||||||
|
|
||||||
const setupWindowListeners = async () => {
|
register(
|
||||||
const [hideUnlisten, showUnlisten] = await Promise.all([
|
(async () => {
|
||||||
listen("verge://hide-window", () => appWindow.hide()),
|
const [hideUnlisten, showUnlisten] = await Promise.all([
|
||||||
listen("verge://show-window", () => appWindow.show()),
|
listen("verge://hide-window", () => appWindow.hide()),
|
||||||
]);
|
listen("verge://show-window", () => appWindow.show()),
|
||||||
|
]);
|
||||||
return () => {
|
return () => {
|
||||||
hideUnlisten();
|
hideUnlisten();
|
||||||
showUnlisten();
|
showUnlisten();
|
||||||
};
|
};
|
||||||
};
|
})(),
|
||||||
|
);
|
||||||
const cleanupWindow = setupWindowListeners();
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setTimeout(() => {
|
disposed = true;
|
||||||
listeners.forEach((listener) => {
|
unlisteners.forEach((unlisten) => {
|
||||||
if (typeof listener.then === "function") {
|
try {
|
||||||
listener
|
unlisten();
|
||||||
.then((unlisten) => {
|
} catch (error) {
|
||||||
try {
|
console.error("[Layout] 清理事件监听器失败", error);
|
||||||
unlisten();
|
}
|
||||||
} catch (error) {
|
});
|
||||||
console.error("[Layout] 清理事件监听器失败:", error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("[Layout] 获取unlisten函数失败:", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cleanupWindow
|
|
||||||
.then((cleanup) => {
|
|
||||||
try {
|
|
||||||
cleanup();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Layout] 清理窗口监听器失败:", error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("[Layout] 获取cleanup函数失败:", error);
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
};
|
};
|
||||||
}, [handleNotice]);
|
}, [addListener, handleNotice]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initRef.current) {
|
if (initRef.current) {
|
||||||
@@ -314,6 +361,14 @@ const Layout = () => {
|
|||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let initializationAttempts = 0;
|
let initializationAttempts = 0;
|
||||||
const maxAttempts = 3;
|
const maxAttempts = 3;
|
||||||
|
const timers = new Set<number>();
|
||||||
|
|
||||||
|
const scheduleTimeout = (handler: () => void, delay: number) => {
|
||||||
|
/* eslint-disable-next-line @eslint-react/web-api/no-leaked-timeout -- timeout is registered in the timers set and cleared during cleanup */
|
||||||
|
const id = window.setTimeout(handler, delay);
|
||||||
|
timers.add(id);
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
const notifyBackend = async (action: string, stage?: string) => {
|
const notifyBackend = async (action: string, stage?: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -334,7 +389,7 @@ const Layout = () => {
|
|||||||
if (initialOverlay) {
|
if (initialOverlay) {
|
||||||
console.log("[Layout] 移除加载指示器");
|
console.log("[Layout] 移除加载指示器");
|
||||||
initialOverlay.style.opacity = "0";
|
initialOverlay.style.opacity = "0";
|
||||||
setTimeout(() => {
|
scheduleTimeout(() => {
|
||||||
try {
|
try {
|
||||||
initialOverlay.remove();
|
initialOverlay.remove();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -365,13 +420,13 @@ const Layout = () => {
|
|||||||
console.log("[Layout] React组件已挂载");
|
console.log("[Layout] React组件已挂载");
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(checkReactMount, 50);
|
scheduleTimeout(checkReactMount, 50);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkReactMount();
|
checkReactMount();
|
||||||
|
|
||||||
setTimeout(() => {
|
scheduleTimeout(() => {
|
||||||
console.log("[Layout] React组件挂载检查超时,继续执行");
|
console.log("[Layout] React组件挂载检查超时,继续执行");
|
||||||
resolve();
|
resolve();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -399,7 +454,7 @@ const Layout = () => {
|
|||||||
console.log(
|
console.log(
|
||||||
`[Layout] 将在500ms后进行第 ${initializationAttempts + 1} 次重试`,
|
`[Layout] 将在500ms后进行第 ${initializationAttempts + 1} 次重试`,
|
||||||
);
|
);
|
||||||
setTimeout(performInitialization, 500);
|
scheduleTimeout(performInitialization, 500);
|
||||||
} else {
|
} else {
|
||||||
console.error("[Layout] 所有初始化尝试都失败,执行紧急初始化");
|
console.error("[Layout] 所有初始化尝试都失败,执行紧急初始化");
|
||||||
|
|
||||||
@@ -408,7 +463,7 @@ const Layout = () => {
|
|||||||
await notifyBackend("UI就绪");
|
await notifyBackend("UI就绪");
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Layout] 紧急初始化也失败:", e);
|
console.error("[Layout] 紧急初始化也失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -419,11 +474,12 @@ const Layout = () => {
|
|||||||
const setupEventListener = async () => {
|
const setupEventListener = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("[Layout] 开始监听启动完成事件");
|
console.log("[Layout] 开始监听启动完成事件");
|
||||||
|
// TODO: 监听启动完成事件的实现
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Layout] 监听启动完成事件失败:", err);
|
console.error("[Layout] 监听启动完成事件失败:", err);
|
||||||
return () => {};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
void setupEventListener();
|
||||||
|
|
||||||
const checkImmediateInitialization = async () => {
|
const checkImmediateInitialization = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -440,7 +496,7 @@ const Layout = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const backupInitialization = setTimeout(() => {
|
const backupInitialization = scheduleTimeout(() => {
|
||||||
if (!hasEventTriggered && !isInitialized) {
|
if (!hasEventTriggered && !isInitialized) {
|
||||||
console.warn("[Layout] 备用初始化触发:1.5秒内未开始初始化");
|
console.warn("[Layout] 备用初始化触发:1.5秒内未开始初始化");
|
||||||
hasEventTriggered = true;
|
hasEventTriggered = true;
|
||||||
@@ -448,20 +504,29 @@ const Layout = () => {
|
|||||||
}
|
}
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
const emergencyInitialization = setTimeout(() => {
|
const emergencyInitialization = scheduleTimeout(() => {
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
console.error("[Layout] 紧急初始化触发:5秒内未完成初始化");
|
console.error("[Layout] 紧急初始化触发,5秒内未完成初始化");
|
||||||
removeLoadingOverlay();
|
removeLoadingOverlay();
|
||||||
notifyBackend("UI就绪").catch(() => {});
|
notifyBackend("UI就绪").catch(() => {});
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
setTimeout(checkImmediateInitialization, 100);
|
const immediateInitTimer = scheduleTimeout(
|
||||||
|
checkImmediateInitialization,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(backupInitialization);
|
window.clearTimeout(backupInitialization);
|
||||||
clearTimeout(emergencyInitialization);
|
window.clearTimeout(emergencyInitialization);
|
||||||
|
window.clearTimeout(immediateInitTimer);
|
||||||
|
timers.delete(backupInitialization);
|
||||||
|
timers.delete(emergencyInitialization);
|
||||||
|
timers.delete(immediateInitTimer);
|
||||||
|
timers.forEach((timeoutId) => window.clearTimeout(timeoutId));
|
||||||
|
timers.clear();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -474,10 +539,33 @@ const Layout = () => {
|
|||||||
}, [language, switchLanguage]);
|
}, [language, switchLanguage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (start_page) {
|
if (!start_page) {
|
||||||
navigate(start_page, { replace: true });
|
lastStartPageRef.current = null;
|
||||||
|
startPageAppliedRef.current = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [start_page]);
|
|
||||||
|
const normalizedStartPage = start_page.startsWith("/")
|
||||||
|
? start_page
|
||||||
|
: `/${start_page}`;
|
||||||
|
|
||||||
|
if (lastStartPageRef.current !== normalizedStartPage) {
|
||||||
|
lastStartPageRef.current = normalizedStartPage;
|
||||||
|
startPageAppliedRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPageAppliedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startPageAppliedRef.current = true;
|
||||||
|
|
||||||
|
if (location.pathname === normalizedStartPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(normalizedStartPage, { replace: true });
|
||||||
|
}, [start_page, navigate, location.pathname]);
|
||||||
|
|
||||||
if (!themeReady) {
|
if (!themeReady) {
|
||||||
return (
|
return (
|
||||||
@@ -622,9 +710,7 @@ const Layout = () => {
|
|||||||
|
|
||||||
<div className="layout-content__right">
|
<div className="layout-content__right">
|
||||||
<div className="the-bar"></div>
|
<div className="the-bar"></div>
|
||||||
<div className="the-content">
|
<div className="the-content">{routersEles}</div>
|
||||||
{React.cloneElement(routersEles, { key: location.pathname })}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
|
|||||||
const ConnectionsPage = () => {
|
const ConnectionsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const pageVisible = useVisibility();
|
const pageVisible = useVisibility();
|
||||||
const [match, setMatch] = useState(() => (_: string) => true);
|
const [match, setMatch] = useState<(input: string) => boolean>(
|
||||||
const [curOrderOpt, setOrderOpt] = useState("Default");
|
() => () => true,
|
||||||
|
);
|
||||||
|
const [curOrderOpt, setCurOrderOpt] = useState("Default");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response: { data: connections },
|
response: { data: connections },
|
||||||
@@ -195,7 +197,7 @@ const ConnectionsPage = () => {
|
|||||||
{!isTableLayout && (
|
{!isTableLayout && (
|
||||||
<BaseStyledSelect
|
<BaseStyledSelect
|
||||||
value={curOrderOpt}
|
value={curOrderOpt}
|
||||||
onChange={(e) => setOrderOpt(e.target.value)}
|
onChange={(e) => setCurOrderOpt(e.target.value)}
|
||||||
>
|
>
|
||||||
{Object.keys(orderOpts).map((opt) => (
|
{Object.keys(orderOpts).map((opt) => (
|
||||||
<MenuItem key={opt} value={opt}>
|
<MenuItem key={opt} value={opt}>
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ const ProfilePage = () => {
|
|||||||
// Batch selection states
|
// Batch selection states
|
||||||
const [batchMode, setBatchMode] = useState(false);
|
const [batchMode, setBatchMode] = useState(false);
|
||||||
const [selectedProfiles, setSelectedProfiles] = useState<Set<string>>(
|
const [selectedProfiles, setSelectedProfiles] = useState<Set<string>>(
|
||||||
new Set(),
|
() => new Set(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 防止重复切换
|
// 防止重复切换
|
||||||
@@ -899,6 +899,8 @@ const ProfilePage = () => {
|
|||||||
let lastUpdateTime = 0;
|
let lastUpdateTime = 0;
|
||||||
const debounceDelay = 200;
|
const debounceDelay = 200;
|
||||||
|
|
||||||
|
let refreshTimer: number | null = null;
|
||||||
|
|
||||||
const setupListener = async () => {
|
const setupListener = async () => {
|
||||||
unlistenPromise = listen<string>("profile-changed", (event) => {
|
unlistenPromise = listen<string>("profile-changed", (event) => {
|
||||||
const newProfileId = event.payload;
|
const newProfileId = event.payload;
|
||||||
@@ -919,11 +921,16 @@ const ProfilePage = () => {
|
|||||||
|
|
||||||
console.log(`[Profile] 执行配置数据刷新`);
|
console.log(`[Profile] 执行配置数据刷新`);
|
||||||
|
|
||||||
|
if (refreshTimer !== null) {
|
||||||
|
window.clearTimeout(refreshTimer);
|
||||||
|
}
|
||||||
|
|
||||||
// 使用异步调度避免阻塞事件处理
|
// 使用异步调度避免阻塞事件处理
|
||||||
setTimeout(() => {
|
refreshTimer = window.setTimeout(() => {
|
||||||
mutateProfiles().catch((error) => {
|
mutateProfiles().catch((error) => {
|
||||||
console.error("[Profile] 配置数据刷新失败:", error);
|
console.error("[Profile] 配置数据刷新失败:", error);
|
||||||
});
|
});
|
||||||
|
refreshTimer = null;
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -931,6 +938,9 @@ const ProfilePage = () => {
|
|||||||
setupListener();
|
setupListener();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
if (refreshTimer !== null) {
|
||||||
|
window.clearTimeout(refreshTimer);
|
||||||
|
}
|
||||||
unlistenPromise?.then((unlisten) => unlisten()).catch(console.error);
|
unlistenPromise?.then((unlisten) => unlisten()).catch(console.error);
|
||||||
};
|
};
|
||||||
}, [mutateProfiles]);
|
}, [mutateProfiles]);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box, Button, ButtonGroup } from "@mui/material";
|
import { Box, Button, ButtonGroup } from "@mui/material";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { closeAllConnections, getBaseConfig } from "tauri-plugin-mihomo-api";
|
import { closeAllConnections, getBaseConfig } from "tauri-plugin-mihomo-api";
|
||||||
@@ -28,7 +28,14 @@ const ProxyPage = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [chainConfigData, setChainConfigData] = useState<string | null>(null);
|
const [chainConfigData, dispatchChainConfigData] = useReducer(
|
||||||
|
(_: string | null, action: string | null) => action,
|
||||||
|
null as string | null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateChainConfigData = useCallback((value: string | null) => {
|
||||||
|
dispatchChainConfigData(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||||
"getClashConfig",
|
"getClashConfig",
|
||||||
@@ -78,30 +85,43 @@ const ProxyPage = () => {
|
|||||||
|
|
||||||
// 当开启链式代理模式时,获取配置数据
|
// 当开启链式代理模式时,获取配置数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isChainMode) {
|
if (!isChainMode) {
|
||||||
const fetchChainConfig = async () => {
|
updateChainConfigData(null);
|
||||||
try {
|
return;
|
||||||
const exitNode = localStorage.getItem("proxy-chain-exit-node");
|
|
||||||
|
|
||||||
if (!exitNode) {
|
|
||||||
console.error("No proxy chain exit node found in localStorage");
|
|
||||||
setChainConfigData("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configData = await getRuntimeProxyChainConfig(exitNode);
|
|
||||||
setChainConfigData(configData || "");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to get runtime proxy chain config:", error);
|
|
||||||
setChainConfigData("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchChainConfig();
|
|
||||||
} else {
|
|
||||||
setChainConfigData(null);
|
|
||||||
}
|
}
|
||||||
}, [isChainMode]);
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const fetchChainConfig = async () => {
|
||||||
|
try {
|
||||||
|
const exitNode = localStorage.getItem("proxy-chain-exit-node");
|
||||||
|
|
||||||
|
if (!exitNode) {
|
||||||
|
console.error("No proxy chain exit node found in localStorage");
|
||||||
|
if (!cancelled) {
|
||||||
|
updateChainConfigData("");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configData = await getRuntimeProxyChainConfig(exitNode);
|
||||||
|
if (!cancelled) {
|
||||||
|
updateChainConfigData(configData || "");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get runtime proxy chain config:", error);
|
||||||
|
if (!cancelled) {
|
||||||
|
updateChainConfigData("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchChainConfig();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [isChainMode, updateChainConfigData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (curMode && !modeList.includes(curMode)) {
|
if (curMode && !modeList.includes(curMode)) {
|
||||||
|
|||||||
@@ -38,6 +38,51 @@ export const AppDataProvider = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
|
||||||
|
"getClashConfig",
|
||||||
|
getBaseConfig,
|
||||||
|
{
|
||||||
|
refreshInterval: 60000, // 60秒刷新间隔,减少频繁请求
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 提供者数据
|
||||||
|
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
|
||||||
|
"getProxyProviders",
|
||||||
|
calcuProxyProviders,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
dedupingInterval: 3000,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
|
||||||
|
"getRuleProviders",
|
||||||
|
getRuleProviders,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 低频率更新数据
|
||||||
|
const { data: rulesData, mutate: refreshRules } = useSWR(
|
||||||
|
"getRules",
|
||||||
|
getRules,
|
||||||
|
{
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
suspense: false,
|
||||||
|
errorRetryCount: 3,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 监听profile和clash配置变更事件
|
// 监听profile和clash配置变更事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let lastProfileId: string | null = null;
|
let lastProfileId: string | null = null;
|
||||||
@@ -47,7 +92,6 @@ export const AppDataProvider = ({
|
|||||||
let isUnmounted = false;
|
let isUnmounted = false;
|
||||||
const scheduledTimeouts = new Set<ReturnType<typeof setTimeout>>();
|
const scheduledTimeouts = new Set<ReturnType<typeof setTimeout>>();
|
||||||
const cleanupFns: Array<() => void> = [];
|
const cleanupFns: Array<() => void> = [];
|
||||||
const fallbackWindowListeners: Array<[string, EventListener]> = [];
|
|
||||||
|
|
||||||
const registerCleanup = (fn: () => void) => {
|
const registerCleanup = (fn: () => void) => {
|
||||||
if (isUnmounted) {
|
if (isUnmounted) {
|
||||||
@@ -57,6 +101,12 @@ export const AppDataProvider = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addWindowListener = (eventName: string, handler: EventListener) => {
|
||||||
|
// eslint-disable-next-line @eslint-react/web-api/no-leaked-event-listener -- cleanup is returned by this helper
|
||||||
|
window.addEventListener(eventName, handler);
|
||||||
|
return () => window.removeEventListener(eventName, handler);
|
||||||
|
};
|
||||||
|
|
||||||
const scheduleTimeout = (
|
const scheduleTimeout = (
|
||||||
callback: () => void | Promise<void>,
|
callback: () => void | Promise<void>,
|
||||||
delay: number,
|
delay: number,
|
||||||
@@ -70,40 +120,11 @@ export const AppDataProvider = ({
|
|||||||
return timeoutId;
|
return timeoutId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearScheduledTimeout = (
|
|
||||||
timeoutId: ReturnType<typeof setTimeout>,
|
|
||||||
) => {
|
|
||||||
if (scheduledTimeouts.has(timeoutId)) {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
scheduledTimeouts.delete(timeoutId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearAllTimeouts = () => {
|
const clearAllTimeouts = () => {
|
||||||
scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
|
scheduledTimeouts.forEach((timeoutId) => clearTimeout(timeoutId));
|
||||||
scheduledTimeouts.clear();
|
scheduledTimeouts.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
const withTimeout = async <T,>(
|
|
||||||
promise: Promise<T>,
|
|
||||||
timeoutMs: number,
|
|
||||||
label: string,
|
|
||||||
): Promise<T> => {
|
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
||||||
timeoutId = scheduleTimeout(() => reject(new Error(label)), timeoutMs);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await Promise.race([promise, timeoutPromise]);
|
|
||||||
} finally {
|
|
||||||
if (timeoutId !== null) {
|
|
||||||
clearScheduledTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProfileChanged = (event: { payload: string }) => {
|
const handleProfileChanged = (event: { payload: string }) => {
|
||||||
const newProfileId = event.payload;
|
const newProfileId = event.payload;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -204,8 +225,7 @@ export const AppDataProvider = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
fallbackHandlers.forEach(([eventName, handler]) => {
|
fallbackHandlers.forEach(([eventName, handler]) => {
|
||||||
window.addEventListener(eventName, handler);
|
registerCleanup(addWindowListener(eventName, handler));
|
||||||
fallbackWindowListeners.push([eventName, handler]);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -215,57 +235,9 @@ export const AppDataProvider = ({
|
|||||||
return () => {
|
return () => {
|
||||||
isUnmounted = true;
|
isUnmounted = true;
|
||||||
clearAllTimeouts();
|
clearAllTimeouts();
|
||||||
fallbackWindowListeners.splice(0).forEach(([eventName, handler]) => {
|
|
||||||
window.removeEventListener(eventName, handler);
|
|
||||||
});
|
|
||||||
cleanupFns.splice(0).forEach((fn) => fn());
|
cleanupFns.splice(0).forEach((fn) => fn());
|
||||||
};
|
};
|
||||||
}, [refreshProxy]);
|
}, [refreshProxy, refreshRules, refreshRuleProviders]);
|
||||||
|
|
||||||
const { data: clashConfig, mutate: refreshClashConfig } = useSWR(
|
|
||||||
"getClashConfig",
|
|
||||||
getBaseConfig,
|
|
||||||
{
|
|
||||||
refreshInterval: 60000, // 60秒刷新间隔,减少频繁请求
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
suspense: false,
|
|
||||||
errorRetryCount: 3,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 提供者数据
|
|
||||||
const { data: proxyProviders, mutate: refreshProxyProviders } = useSWR(
|
|
||||||
"getProxyProviders",
|
|
||||||
calcuProxyProviders,
|
|
||||||
{
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
dedupingInterval: 3000,
|
|
||||||
suspense: false,
|
|
||||||
errorRetryCount: 3,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: ruleProviders, mutate: refreshRuleProviders } = useSWR(
|
|
||||||
"getRuleProviders",
|
|
||||||
getRuleProviders,
|
|
||||||
{
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
suspense: false,
|
|
||||||
errorRetryCount: 3,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 低频率更新数据
|
|
||||||
const { data: rulesData, mutate: refreshRules } = useSWR(
|
|
||||||
"getRules",
|
|
||||||
getRules,
|
|
||||||
{
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
suspense: false,
|
|
||||||
errorRetryCount: 3,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
|
const { data: sysproxy, mutate: refreshSysproxy } = useSWR(
|
||||||
"getSystemProxy",
|
"getSystemProxy",
|
||||||
|
|||||||
20
src/providers/chain-proxy-context.ts
Normal file
20
src/providers/chain-proxy-context.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { createContext, use } from "react";
|
||||||
|
|
||||||
|
export interface ChainProxyContextType {
|
||||||
|
isChainMode: boolean;
|
||||||
|
setChainMode: (isChain: boolean) => void;
|
||||||
|
chainConfigData: string | null;
|
||||||
|
setChainConfigData: (data: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChainProxyContext = createContext<ChainProxyContextType | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useChainProxy = () => {
|
||||||
|
const context = use(ChainProxyContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useChainProxy must be used within a ChainProxyProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
import React, { createContext, useCallback, use, useState } from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
|
|
||||||
interface ChainProxyContextType {
|
import { ChainProxyContext } from "./chain-proxy-context";
|
||||||
isChainMode: boolean;
|
|
||||||
setChainMode: (isChain: boolean) => void;
|
|
||||||
chainConfigData: string | null;
|
|
||||||
setChainConfigData: (data: string | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChainProxyContext = createContext<ChainProxyContextType | null>(null);
|
|
||||||
|
|
||||||
export const ChainProxyProvider = ({
|
export const ChainProxyProvider = ({
|
||||||
children,
|
children,
|
||||||
@@ -25,24 +18,15 @@ export const ChainProxyProvider = ({
|
|||||||
setChainConfigData(data);
|
setChainConfigData(data);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
const contextValue = useMemo(
|
||||||
<ChainProxyContext
|
() => ({
|
||||||
value={{
|
isChainMode,
|
||||||
isChainMode,
|
setChainMode,
|
||||||
setChainMode,
|
chainConfigData,
|
||||||
chainConfigData,
|
setChainConfigData: setChainConfigDataCallback,
|
||||||
setChainConfigData: setChainConfigDataCallback,
|
}),
|
||||||
}}
|
[isChainMode, setChainMode, chainConfigData, setChainConfigDataCallback],
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ChainProxyContext>
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export const useChainProxy = () => {
|
return <ChainProxyContext value={contextValue}>{children}</ChainProxyContext>;
|
||||||
const context = use(ChainProxyContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useChainProxy must be used within a ChainProxyProvider");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
};
|
||||||
|
|||||||
18
src/providers/window/WindowContext.ts
Normal file
18
src/providers/window/WindowContext.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export interface WindowContextType {
|
||||||
|
decorated: boolean | null;
|
||||||
|
maximized: boolean | null;
|
||||||
|
toggleDecorations: () => Promise<void>;
|
||||||
|
refreshDecorated: () => Promise<boolean>;
|
||||||
|
minimize: () => void;
|
||||||
|
close: () => void;
|
||||||
|
toggleMaximize: () => Promise<void>;
|
||||||
|
toggleFullscreen: () => Promise<void>;
|
||||||
|
currentWindow: ReturnType<typeof getCurrentWindow>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WindowContext = createContext<WindowContextType | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
91
src/providers/window/WindowProvider.tsx
Normal file
91
src/providers/window/WindowProvider.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { WindowContext } from "./WindowContext";
|
||||||
|
|
||||||
|
export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const currentWindow = useMemo(() => getCurrentWindow(), []);
|
||||||
|
const [decorated, setDecorated] = useState<boolean | null>(null);
|
||||||
|
const [maximized, setMaximized] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
const close = useCallback(() => currentWindow.close(), [currentWindow]);
|
||||||
|
const minimize = useCallback(() => currentWindow.minimize(), [currentWindow]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
const updateMaximized = async () => {
|
||||||
|
const value = await currentWindow.isMaximized();
|
||||||
|
if (!active) return;
|
||||||
|
setMaximized((prev) => (prev === value ? prev : value));
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMaximized();
|
||||||
|
const unlistenPromise = currentWindow.onResized(updateMaximized);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
unlistenPromise.then((unlisten) => unlisten());
|
||||||
|
};
|
||||||
|
}, [currentWindow]);
|
||||||
|
|
||||||
|
const toggleMaximize = useCallback(async () => {
|
||||||
|
if (await currentWindow.isMaximized()) {
|
||||||
|
await currentWindow.unmaximize();
|
||||||
|
setMaximized(false);
|
||||||
|
} else {
|
||||||
|
await currentWindow.maximize();
|
||||||
|
setMaximized(true);
|
||||||
|
}
|
||||||
|
}, [currentWindow]);
|
||||||
|
|
||||||
|
const toggleFullscreen = useCallback(async () => {
|
||||||
|
await currentWindow.setFullscreen(!(await currentWindow.isFullscreen()));
|
||||||
|
}, [currentWindow]);
|
||||||
|
|
||||||
|
const refreshDecorated = useCallback(async () => {
|
||||||
|
const val = await currentWindow.isDecorated();
|
||||||
|
setDecorated((prev) => (prev === val ? prev : val));
|
||||||
|
return val;
|
||||||
|
}, [currentWindow]);
|
||||||
|
|
||||||
|
const toggleDecorations = useCallback(async () => {
|
||||||
|
const currentVal = await currentWindow.isDecorated();
|
||||||
|
await currentWindow.setDecorations(!currentVal);
|
||||||
|
setDecorated(!currentVal);
|
||||||
|
}, [currentWindow]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshDecorated();
|
||||||
|
currentWindow.setMinimizable?.(true);
|
||||||
|
}, [currentWindow, refreshDecorated]);
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
decorated,
|
||||||
|
maximized,
|
||||||
|
toggleDecorations,
|
||||||
|
refreshDecorated,
|
||||||
|
minimize,
|
||||||
|
close,
|
||||||
|
toggleMaximize,
|
||||||
|
toggleFullscreen,
|
||||||
|
currentWindow,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
decorated,
|
||||||
|
maximized,
|
||||||
|
toggleDecorations,
|
||||||
|
refreshDecorated,
|
||||||
|
minimize,
|
||||||
|
close,
|
||||||
|
toggleMaximize,
|
||||||
|
toggleFullscreen,
|
||||||
|
currentWindow,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return <WindowContext value={contextValue}>{children}</WindowContext>;
|
||||||
|
};
|
||||||
2
src/providers/window/index.ts
Normal file
2
src/providers/window/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./WindowContext";
|
||||||
|
export * from "./WindowProvider";
|
||||||
@@ -14,8 +14,7 @@ interface ILogItem {
|
|||||||
// Start logs monitoring with specified level
|
// Start logs monitoring with specified level
|
||||||
export const startLogsStreaming = async (logLevel: LogLevel = "info") => {
|
export const startLogsStreaming = async (logLevel: LogLevel = "info") => {
|
||||||
try {
|
try {
|
||||||
const level = logLevel === "all" ? undefined : logLevel;
|
// await startLogsMonitoring(logLevel === "all" ? undefined : logLevel);
|
||||||
// await startLogsMonitoring(level);
|
|
||||||
console.log(
|
console.log(
|
||||||
`[IPC-LogService] Started logs monitoring with level: ${logLevel}`,
|
`[IPC-LogService] Started logs monitoring with level: ${logLevel}`,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user