Files
clash-verge-rev/src/pages/_layout.tsx
2025-05-04 22:17:08 +08:00

382 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import dayjs from "dayjs";
import i18next from "i18next";
import relativeTime from "dayjs/plugin/relativeTime";
import { SWRConfig, mutate } from "swr";
import { useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useRoutes, useNavigate } from "react-router-dom";
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { routers } from "./_routers";
import { getAxios } from "@/services/api";
import { useVerge } from "@/hooks/use-verge";
import LogoSvg from "@/assets/image/logo.svg?react";
import iconLight from "@/assets/image/icon_light.svg?react";
import iconDark from "@/assets/image/icon_dark.svg?react";
import { useThemeMode, useEnableLog } from "@/services/states";
import { LayoutItem } from "@/components/layout/layout-item";
import { LayoutControl } from "@/components/layout/layout-control";
import { LayoutTraffic } from "@/components/layout/layout-traffic";
import { UpdateButton } from "@/components/layout/update-button";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
import getSystem from "@/utils/get-system";
import "dayjs/locale/ru";
import "dayjs/locale/zh-cn";
import { getPortableFlag } from "@/services/cmds";
import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import { useListen } from "@/hooks/use-listen";
import { listen } from "@tauri-apps/api/event";
import { useClashInfo } from "@/hooks/use-clash";
import { initGlobalLogService } from "@/services/global-log-service";
import { invoke } from "@tauri-apps/api/core";
import { showNotice } from "@/services/noticeService";
import { NoticeManager } from "@/components/base/NoticeManager";
const appWindow = getCurrentWebviewWindow();
export let portableFlag = false;
dayjs.extend(relativeTime);
const OS = getSystem();
// 通知处理函数
const handleNoticeMessage = (
status: string,
msg: string,
t: (key: string) => string,
navigate: (path: string, options?: any) => void,
) => {
console.log("[通知监听 V2] 收到消息:", status, msg);
switch (status) {
case "import_sub_url::ok":
navigate("/profile", { state: { current: msg } });
showNotice('success', t("Import Subscription Successful"));
break;
case "import_sub_url::error":
navigate("/profile");
showNotice('error', msg);
break;
case "set_config::error":
showNotice('error', msg);
break;
case "update_with_clash_proxy":
showNotice('success', `${t("Update with Clash proxy successfully")} ${msg}`);
break;
case "update_retry_with_clash":
showNotice('info', t("Update failed, retrying with Clash proxy..."));
break;
case "update_failed_even_with_clash":
showNotice('error', `${t("Update failed even with Clash proxy")}: ${msg}`);
break;
case "update_failed":
showNotice('error', msg);
break;
case "config_validate::boot_error":
showNotice('error', `${t("Boot Config Validation Failed")} ${msg}`);
break;
case "config_validate::core_change":
showNotice('error', `${t("Core Change Config Validation Failed")} ${msg}`);
break;
case "config_validate::error":
showNotice('error', `${t("Config Validation Failed")} ${msg}`);
break;
case "config_validate::process_terminated":
showNotice('error', t("Config Validation Process Terminated"));
break;
case "config_validate::stdout_error":
showNotice('error', `${t("Config Validation Failed")} ${msg}`);
break;
case "config_validate::script_error":
showNotice('error', `${t("Script File Error")} ${msg}`);
break;
case "config_validate::script_syntax_error":
showNotice('error', `${t("Script Syntax Error")} ${msg}`);
break;
case "config_validate::script_missing_main":
showNotice('error', `${t("Script Missing Main")} ${msg}`);
break;
case "config_validate::file_not_found":
showNotice('error', `${t("File Not Found")} ${msg}`);
break;
case "config_validate::yaml_syntax_error":
showNotice('error', `${t("YAML Syntax Error")} ${msg}`);
break;
case "config_validate::yaml_read_error":
showNotice('error', `${t("YAML Read Error")} ${msg}`);
break;
case "config_validate::yaml_mapping_error":
showNotice('error', `${t("YAML Mapping Error")} ${msg}`);
break;
case "config_validate::yaml_key_error":
showNotice('error', `${t("YAML Key Error")} ${msg}`);
break;
case "config_validate::yaml_error":
showNotice('error', `${t("YAML Error")} ${msg}`);
break;
case "config_validate::merge_syntax_error":
showNotice('error', `${t("Merge File Syntax Error")} ${msg}`);
break;
case "config_validate::merge_mapping_error":
showNotice('error', `${t("Merge File Mapping Error")} ${msg}`);
break;
case "config_validate::merge_key_error":
showNotice('error', `${t("Merge File Key Error")} ${msg}`);
break;
case "config_validate::merge_error":
showNotice('error', `${t("Merge File Error")} ${msg}`);
break;
case "config_core::change_success":
showNotice('success', `${t("Core Changed Successfully")}: ${msg}`);
break;
case "config_core::change_error":
showNotice('error', `${t("Failed to Change Core")}: ${msg}`);
break;
default: // Optional: Log unhandled statuses
console.warn(`[通知监听 V2] 未处理的状态: ${status}`);
break;
}
};
const Layout = () => {
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const { t } = useTranslation();
const { theme } = useCustomTheme();
const { verge } = useVerge();
const { clashInfo } = useClashInfo();
const [enableLog] = useEnableLog();
const { language, start_page } = verge ?? {};
const navigate = useNavigate();
const location = useLocation();
const routersEles = useRoutes(routers);
const { addListener, setupCloseListener } = useListen();
const handleNotice = useCallback(
(payload: [string, string]) => {
const [status, msg] = payload;
handleNoticeMessage(status, msg, t, navigate);
},
[t, navigate],
);
// 初始化全局日志服务
useEffect(() => {
if (clashInfo) {
const { server = "", secret = "" } = clashInfo;
// 使用本地存储中的enableLog值初始化全局日志服务
initGlobalLogService(server, secret, enableLog, "info");
}
}, [clashInfo, enableLog]);
// 设置监听器
useEffect(() => {
const listeners = [
// 配置更新监听
addListener("verge://refresh-clash-config", async () => {
await getAxios(true);
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
mutate("getProxyProviders");
}),
// verge 配置更新监听
addListener("verge://refresh-verge-config", () => {
mutate("getVergeConfig");
// 添加对系统代理状态的刷新
mutate("getSystemProxy");
mutate("getAutotemProxy");
}),
// 通知消息监听
addListener("verge://notice-message", ({ payload }) =>
handleNotice(payload as [string, string]),
),
];
// 设置窗口显示/隐藏监听
const setupWindowListeners = async () => {
const [hideUnlisten, showUnlisten] = await Promise.all([
listen("verge://hide-window", () => appWindow.hide()),
listen("verge://show-window", () => appWindow.show()),
]);
return () => {
hideUnlisten();
showUnlisten();
};
};
// 初始化
setupCloseListener();
const cleanupWindow = setupWindowListeners();
// 清理函数
return () => {
// 清理主要监听器
listeners.forEach((listener) => {
if (typeof listener.then === "function") {
listener.then((unlisten) => unlisten());
}
});
// 清理窗口监听器
cleanupWindow.then((cleanup) => cleanup());
};
}, [handleNotice]);
// 监听启动完成事件并通知UI已加载
useEffect(() => {
const notifyUiReady = async () => {
try {
await invoke("notify_ui_ready");
console.log("已通知后端UI准备就绪");
} catch (err) {
console.error("通知UI准备就绪失败:", err);
}
};
// 监听后端发送的启动完成事件
const listenStartupCompleted = async () => {
try {
const unlisten = await listen("verge://startup-completed", () => {
console.log("收到启动完成事件开始通知UI就绪");
notifyUiReady();
});
return unlisten;
} catch (err) {
console.error("监听启动完成事件失败:", err);
return () => {};
}
};
// 初始加载时也通知一次
console.log("页面初始加载通知UI就绪");
notifyUiReady();
const unlistenPromise = listenStartupCompleted();
return () => {
unlistenPromise.then(unlisten => unlisten());
};
}, []);
// 语言和起始页设置
useEffect(() => {
if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language);
i18next.changeLanguage(language);
}
}, [language]);
useEffect(() => {
if (start_page) {
navigate(start_page, { replace: true });
}
}, [start_page]);
if (!routersEles) return null;
return (
<SWRConfig value={{ errorRetryCount: 3 }}>
<ThemeProvider theme={theme}>
<NoticeManager />
<Paper
square
elevation={0}
className={`${OS} layout`}
onContextMenu={(e) => {
if (
OS === "windows" &&
!["input", "textarea"].includes(
e.currentTarget.tagName.toLowerCase(),
) &&
!e.currentTarget.isContentEditable
) {
e.preventDefault();
}
}}
sx={[
({ palette }) => ({ bgcolor: palette.background.paper }),
OS === "linux"
? {
borderRadius: "8px",
border: "1px solid var(--divider-color)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
}
: {},
]}
>
<div className="layout__left">
<div className="the-logo" data-tauri-drag-region="true">
<div
data-tauri-drag-region="true"
style={{
height: "27px",
display: "flex",
justifyContent: "space-between",
}}
>
<SvgIcon
component={isDark ? iconDark : iconLight}
style={{
height: "36px",
width: "36px",
marginTop: "-3px",
marginRight: "5px",
marginLeft: "-3px",
}}
inheritViewBox
/>
<LogoSvg fill={isDark ? "white" : "black"} />
</div>
<UpdateButton className="the-newbtn" />
</div>
<List className="the-menu">
{routers.map((router) => (
<LayoutItem
key={router.label}
to={router.path}
icon={router.icon}
>
{t(router.label)}
</LayoutItem>
))}
</List>
<div className="the-traffic">
<LayoutTraffic />
</div>
</div>
<div className="layout__right">
<div className="the-bar">
<div
className="the-dragbar"
data-tauri-drag-region="true"
style={{ width: "100%" }}
/>
{OS !== "macos" && <LayoutControl />}
</div>
<TransitionGroup className="the-content">
<CSSTransition
key={location.pathname}
timeout={300}
classNames="page"
>
{React.cloneElement(routersEles, { key: location.pathname })}
</CSSTransition>
</TransitionGroup>
</div>
</Paper>
</ThemeProvider>
</SWRConfig>
);
};
export default Layout;