import dayjs from "dayjs"; import i18next from "i18next"; import relativeTime from "dayjs/plugin/relativeTime"; import { SWRConfig, mutate } from "swr"; import { useEffect, useCallback, useState, useRef } 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 { forceRefreshClashConfig } from "@/services/cmds"; 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 { 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 React from "react"; 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 initRef = useRef(false); const [themeReady, setThemeReady] = useState(false); useEffect(() => { setThemeReady(true); }, [theme]); const handleNotice = useCallback( (payload: [string, string]) => { const [status, msg] = payload; setTimeout(() => { try { handleNoticeMessage(status, msg, t, navigate); } catch (error) { console.error("[Layout] 处理通知消息失败:", error); } }, 0); }, [t, navigate], ); // 初始化全局日志服务 useEffect(() => { if (clashInfo) { const { server = "", secret = "" } = clashInfo; initGlobalLogService(server, secret, enableLog, "info"); } }, [clashInfo, enableLog]); // 设置监听器 useEffect(() => { const listeners = [ addListener("verge://refresh-clash-config", async () => { await getAxios(true); // 后端配置变更事件触发,强制刷新配置缓存 await forceRefreshClashConfig(); mutate("getProxies"); mutate("getVersion"); mutate("getClashConfig"); mutate("getProxyProviders"); }), 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 () => { setTimeout(() => { listeners.forEach((listener) => { if (typeof listener.then === "function") { listener .then((unlisten) => { try { 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]); useEffect(() => { if (initRef.current) { console.log("[Layout] 初始化代码已执行过,跳过"); return; } console.log("[Layout] 开始执行初始化代码"); initRef.current = true; let isInitialized = false; let initializationAttempts = 0; const maxAttempts = 3; const notifyBackend = async (action: string, stage?: string) => { try { if (stage) { console.log(`[Layout] 通知后端 ${action}: ${stage}`); await invoke("update_ui_stage", { stage }); } else { console.log(`[Layout] 通知后端 ${action}`); await invoke("notify_ui_ready"); } } catch (err) { console.error(`[Layout] 通知失败 ${action}:`, err); } }; const removeLoadingOverlay = () => { const initialOverlay = document.getElementById("initial-loading-overlay"); if (initialOverlay) { console.log("[Layout] 移除加载指示器"); initialOverlay.style.opacity = "0"; setTimeout(() => { try { initialOverlay.remove(); } catch (e) { console.log("[Layout] 加载指示器已被移除"); } }, 300); } }; const performInitialization = async () => { if (isInitialized) { console.log("[Layout] 已经初始化过,跳过"); return; } initializationAttempts++; console.log(`[Layout] 开始第 ${initializationAttempts} 次初始化尝试`); try { removeLoadingOverlay(); await notifyBackend("加载阶段", "Loading"); await new Promise((resolve) => { const checkReactMount = () => { const rootElement = document.getElementById("root"); if (rootElement && rootElement.children.length > 0) { console.log("[Layout] React组件已挂载"); resolve(); } else { setTimeout(checkReactMount, 50); } }; checkReactMount(); setTimeout(() => { console.log("[Layout] React组件挂载检查超时,继续执行"); resolve(); }, 2000); }); await notifyBackend("DOM就绪", "DomReady"); await new Promise((resolve) => { requestAnimationFrame(() => resolve()); }); await notifyBackend("资源加载完成", "ResourcesLoaded"); await notifyBackend("UI就绪"); isInitialized = true; console.log(`[Layout] 第 ${initializationAttempts} 次初始化完成`); } catch (error) { console.error( `[Layout] 第 ${initializationAttempts} 次初始化失败:`, error, ); if (initializationAttempts < maxAttempts) { console.log( `[Layout] 将在500ms后进行第 ${initializationAttempts + 1} 次重试`, ); setTimeout(performInitialization, 500); } else { console.error("[Layout] 所有初始化尝试都失败,执行紧急初始化"); removeLoadingOverlay(); try { await notifyBackend("UI就绪"); isInitialized = true; } catch (e) { console.error("[Layout] 紧急初始化也失败:", e); } } } }; let hasEventTriggered = false; const setupEventListener = async () => { try { console.log("[Layout] 开始监听启动完成事件"); const unlisten = await listen("verge://startup-completed", () => { if (!hasEventTriggered) { console.log("[Layout] 收到启动完成事件,开始初始化"); hasEventTriggered = true; performInitialization(); } }); return unlisten; } catch (err) { console.error("[Layout] 监听启动完成事件失败:", err); return () => {}; } }; const checkImmediateInitialization = async () => { try { console.log("[Layout] 检查后端是否已就绪"); await invoke("update_ui_stage", { stage: "Loading" }); if (!hasEventTriggered && !isInitialized) { console.log("[Layout] 后端已就绪,立即开始初始化"); hasEventTriggered = true; performInitialization(); } } catch (err) { console.log("[Layout] 后端尚未就绪,等待启动完成事件"); } }; const backupInitialization = setTimeout(() => { if (!hasEventTriggered && !isInitialized) { console.warn("[Layout] 备用初始化触发:1.5秒内未开始初始化"); hasEventTriggered = true; performInitialization(); } }, 1500); const emergencyInitialization = setTimeout(() => { if (!isInitialized) { console.error("[Layout] 紧急初始化触发:5秒内未完成初始化"); removeLoadingOverlay(); notifyBackend("UI就绪").catch(() => {}); isInitialized = true; } }, 5000); const unlistenPromise = setupEventListener(); setTimeout(checkImmediateInitialization, 100); return () => { clearTimeout(backupInitialization); clearTimeout(emergencyInitialization); 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 (!themeReady) { return (
); } if (!routersEles) { return (
); } return ( { console.error(`[SWR Error] Key: ${key}, Error:`, error); if (key !== "getAutotemProxy") { console.error(`SWR Error for ${key}:`, error); } }, dedupingInterval: 2000, }} >
{ 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: "100vw", height: "100vh", } : {}, ]} >
{routers.map((router) => ( {t(router.label)} ))}
{React.cloneElement(routersEles, { key: location.pathname })}
); }; export default Layout;