import { List, Paper, SvgIcon, ThemeProvider } from "@mui/material";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate, useRoutes } from "react-router-dom";
import { SWRConfig, mutate } from "swr";
import iconDark from "@/assets/image/icon_dark.svg?react";
import iconLight from "@/assets/image/icon_light.svg?react";
import LogoSvg from "@/assets/image/logo.svg?react";
import { NoticeManager } from "@/components/base/NoticeManager";
import { WindowControls } from "@/components/controller/window-controller";
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 { useConnectionData } from "@/hooks/use-connection-data";
import { useI18n } from "@/hooks/use-i18n";
import { useListen } from "@/hooks/use-listen";
import { useLogData } from "@/hooks/use-log-data-new";
import { useMemoryData } from "@/hooks/use-memory-data";
import { useTrafficData } from "@/hooks/use-traffic-data";
import { useVerge } from "@/hooks/use-verge";
import { useWindowDecorations } from "@/hooks/use-window";
import { getAxios } from "@/services/api";
import { showNotice } from "@/services/noticeService";
import { useThemeMode } from "@/services/states";
import getSystem from "@/utils/get-system";
import { routers } from "./_routers";
import "dayjs/locale/ru";
import "dayjs/locale/zh-cn";
const appWindow = getCurrentWebviewWindow();
export const 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 = () => {
useTrafficData();
useMemoryData();
useConnectionData();
useLogData();
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const { t } = useTranslation();
const { theme } = useCustomTheme();
const { verge } = useVerge();
const { language, start_page } = verge ?? {};
const { switchLanguage } = useI18n();
const navigate = useNavigate();
const location = useLocation();
const matchedElement = useRoutes(routers);
const routersEles = useMemo(() => {
if (!matchedElement) {
return null;
}
return (
{matchedElement}
);
}, [matchedElement, location.pathname]);
const { addListener } = useListen();
const initRef = useRef(false);
const overlayRemovedRef = useRef(false);
const lastStartPageRef = useRef(null);
const startPageAppliedRef = useRef(false);
const themeReady = useMemo(() => Boolean(theme), [theme]);
const windowControls = useRef(null);
const { decorated } = useWindowDecorations();
const customTitlebar = useMemo(() => {
console.debug(
"[Layout] Titlebar rendering - decorated:",
decorated,
"| showing:",
!decorated,
"| theme mode:",
mode,
);
if (!decorated) {
return (
);
}
return null;
}, [decorated, mode]);
useEffect(() => {
if (!themeReady || overlayRemovedRef.current) {
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(
(payload: [string, string]) => {
const [status, msg] = payload;
try {
handleNoticeMessage(status, msg, t, navigate);
} catch (error) {
console.error("[Layout] 处理通知消息失败:", error);
}
},
[t, navigate],
);
// 设置监听
useEffect(() => {
const unlisteners: Array<() => void> = [];
let disposed = false;
const register = (
maybeUnlisten: void | (() => void) | Promise 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 () => {
await getAxios(true);
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
mutate("getProxyProviders");
}),
);
register(
addListener("verge://refresh-verge-config", () => {
mutate("getVergeConfig");
mutate("getSystemProxy");
mutate("getAutotemProxy");
mutate("getRunningMode");
mutate("isServiceAvailable");
}),
);
register(
addListener("verge://notice-message", ({ payload }) =>
handleNotice(payload as [string, string]),
),
);
register(
(async () => {
const [hideUnlisten, showUnlisten] = await Promise.all([
listen("verge://hide-window", () => appWindow.hide()),
listen("verge://show-window", () => appWindow.show()),
]);
return () => {
hideUnlisten();
showUnlisten();
};
})(),
);
return () => {
disposed = true;
unlisteners.forEach((unlisten) => {
try {
unlisten();
} catch (error) {
console.error("[Layout] 清理事件监听器失败", error);
}
});
};
}, [addListener, 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 timers = new Set();
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) => {
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";
scheduleTimeout(() => {
try {
initialOverlay.remove();
} catch {
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 {
scheduleTimeout(checkReactMount, 50);
}
};
checkReactMount();
scheduleTimeout(() => {
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} 次重试`,
);
scheduleTimeout(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] 开始监听启动完成事件");
// TODO: 监听启动完成事件的实现
} catch (err) {
console.error("[Layout] 监听启动完成事件失败:", err);
}
};
void setupEventListener();
const checkImmediateInitialization = async () => {
try {
console.log("[Layout] 检查后端是否已就绪");
await invoke("update_ui_stage", { stage: "Loading" });
if (!hasEventTriggered && !isInitialized) {
console.log("[Layout] 后端已就绪,立即开始初始化");
hasEventTriggered = true;
performInitialization();
}
} catch {
console.log("[Layout] 后端尚未就绪,等待启动完成事件");
}
};
const backupInitialization = scheduleTimeout(() => {
if (!hasEventTriggered && !isInitialized) {
console.warn("[Layout] 备用初始化触发:1.5秒内未开始初始化");
hasEventTriggered = true;
performInitialization();
}
}, 1500);
const emergencyInitialization = scheduleTimeout(() => {
if (!isInitialized) {
console.error("[Layout] 紧急初始化触发,5秒内未完成初始化");
removeLoadingOverlay();
notifyBackend("UI就绪").catch(() => {});
isInitialized = true;
}
}, 5000);
const immediateInitTimer = scheduleTimeout(
checkImmediateInitialization,
100,
);
return () => {
window.clearTimeout(backupInitialization);
window.clearTimeout(emergencyInitialization);
window.clearTimeout(immediateInitTimer);
timers.delete(backupInitialization);
timers.delete(emergencyInitialization);
timers.delete(immediateInitTimer);
timers.forEach((timeoutId) => window.clearTimeout(timeoutId));
timers.clear();
};
}, []);
// 语言和起始页设置
useEffect(() => {
if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language);
switchLanguage(language);
}
}, [language, switchLanguage]);
useEffect(() => {
if (!start_page) {
lastStartPageRef.current = null;
startPageAppliedRef.current = false;
return;
}
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) {
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",
width: "100vw",
height: "100vh",
}
: {},
]}
>
{/* Custom titlebar - rendered only when decorated is false, memoized for performance */}
{customTitlebar}
{routers.map((router) => (
{t(router.label)}
))}
);
};
export default Layout;