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 { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { routers } from "./_routers"; import { getAxios } from "@/services/api"; import { useVerge } from "@/hooks/use-verge"; import { useThemeMode, useEnableLog } from "@/services/states"; 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 { Toaster } from "@/components/ui/sonner"; import { SidebarProvider, useSidebar } from "@/components/ui/sidebar"; import { AppSidebar } from "@/components/layout/sidebar"; import { useZoomControls } from "@/hooks/useZoomControls"; import { HwidErrorDialog } from "@/components/profile/hwid-error-dialog"; const appWindow = getCurrentWebviewWindow(); export let portableFlag = false; dayjs.extend(relativeTime); const OS = getSystem(); // Notification Handler const handleNoticeMessage = ( status: string, msg: string, t: (key: string) => string, navigate: (path: string, options?: any) => void, ) => { console.log("[Notification Listener V2] Receiving a message:", status, msg); switch (status) { case "import_sub_url::ok": mutate("getProfiles"); navigate("/"); showNotice("success", t("Import Subscription Successful")); sessionStorage.setItem("activateProfile", msg); break; case "import_sub_url::error": console.log(msg); if ( msg.toLowerCase().includes("device") || msg.toLowerCase().includes("устройств") ) { window.dispatchEvent( new CustomEvent("show-hwid-error", { detail: msg }), ); } else { 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(`[Notification Listener V2] Unprocessed state: ${status}`); break; } }; const Layout = () => { const mode = useThemeMode(); useZoomControls(); const isDark = mode === "light" ? false : true; const { t } = useTranslation(); 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 handleNotice = useCallback( (payload: [string, string]) => { const [status, msg] = payload; setTimeout(() => { try { handleNoticeMessage(status, msg, t, navigate); } catch (error) { console.error( "[Layout] Failure to process a notification message:", error, ); } }, 0); }, [t, navigate], ); // Initialize the global logging service useEffect(() => { if (clashInfo) { const { server = "", secret = "" } = clashInfo; initGlobalLogService(server, secret, enableLog, "info"); } }, [clashInfo, enableLog]); // Setting up a listener useEffect(() => { const listeners = [ addListener("verge://refresh-clash-config", async () => { await getAxios(true); 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] Failed to clear event listener:", error, ); } }) .catch((error) => { console.error( "[Layout] Failed to get unlisten function:", error, ); }); } }); cleanupWindow .then((cleanup) => { try { cleanup(); } catch (error) { console.error("[Layout] Failed to clear window listener:", error); } }) .catch((error) => { console.error("[Layout] Failed to get cleanup function:", error); }); }, 0); }; }, [handleNotice]); useEffect(() => { if (initRef.current) { console.log( "[Layout] Initialization code has already been executed, skip", ); return; } console.log("[Layout] Begin executing initialization code"); 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] Notification Backend ${action}: ${stage}`); await invoke("update_ui_stage", { stage }); } else { console.log(`[Layout] Notification Backend ${action}`); await invoke("notify_ui_ready"); } } catch (err) { console.error(`[Layout] Notification failure ${action}:`, err); } }; const removeLoadingOverlay = () => { const initialOverlay = document.getElementById("initial-loading-overlay"); if (initialOverlay) { console.log("[Layout] Remove loading indicator"); initialOverlay.style.opacity = "0"; setTimeout(() => { try { initialOverlay.remove(); } catch (e) { console.log("[Layout] Load indicator has been removed"); } }, 300); } }; const performInitialization = async () => { if (isInitialized) { console.log("[Layout] Already initialized, skip"); return; } initializationAttempts++; console.log( `[Layout] Start ${initializationAttempts} for the first time`, ); try { removeLoadingOverlay(); await notifyBackend("Loading phase", "Loading"); await new Promise((resolve) => { const checkReactMount = () => { const rootElement = document.getElementById("root"); if (rootElement && rootElement.children.length > 0) { console.log("[Layout] React components are mounted"); resolve(); } else { setTimeout(checkReactMount, 50); } }; checkReactMount(); setTimeout(() => { console.log( "[Layout] React components mount check timeout, continue execution", ); resolve(); }, 2000); }); await notifyBackend("DOM ready", "DomReady"); await new Promise((resolve) => { requestAnimationFrame(() => resolve()); }); await notifyBackend("Resource loading completed", "ResourcesLoaded"); await notifyBackend("UI ready"); isInitialized = true; console.log( `[Layout] The ${initializationAttempts} initialization is complete`, ); } catch (error) { console.error( `[Layout] Initialization failure at ${initializationAttempts}:`, error, ); if (initializationAttempts < maxAttempts) { console.log( `[Layout] The first ${initializationAttempts + 1} retry will be made after 500ms`, ); setTimeout(performInitialization, 500); } else { console.error( "[Layout] All initialization attempts fail, perform emergency initialization", ); removeLoadingOverlay(); try { await notifyBackend("UI ready"); isInitialized = true; } catch (e) { console.error("[Layout] Emergency initialization also failed:", e); } } } }; let hasEventTriggered = false; const setupEventListener = async () => { try { console.log("[Layout] Start listening for startup completion events"); const unlisten = await listen("verge://startup-completed", () => { if (!hasEventTriggered) { console.log( "[Layout] Receive startup completion event, start initialization", ); hasEventTriggered = true; performInitialization(); } }); return unlisten; } catch (err) { console.error( "[Layout] Failed to listen for startup completion event:", err, ); return () => {}; } }; const checkImmediateInitialization = async () => { try { console.log("[Layout] Check if the backend is ready"); await invoke("update_ui_stage", { stage: "Loading" }); if (!hasEventTriggered && !isInitialized) { console.log( "[Layout] Backend is ready, start initialization immediately", ); hasEventTriggered = true; performInitialization(); } } catch (err) { console.log( "[Layout] Backend not yet ready, waiting for startup completion event", ); } }; const backupInitialization = setTimeout(() => { if (!hasEventTriggered && !isInitialized) { console.warn( "[Layout] Standby initialization trigger: initialization not started within 1.5 seconds", ); hasEventTriggered = true; performInitialization(); } }, 1500); const emergencyInitialization = setTimeout(() => { if (!isInitialized) { console.error( "[Layout] Emergency initialization trigger: initialization not completed within 5 seconds", ); removeLoadingOverlay(); notifyBackend("UI ready").catch(() => {}); isInitialized = true; } }, 5000); const unlistenPromise = setupEventListener(); setTimeout(checkImmediateInitialization, 100); return () => { clearTimeout(backupInitialization); clearTimeout(emergencyInitialization); unlistenPromise.then((unlisten) => unlisten()); }; }, []); // Language and start page settings useEffect(() => { if (language) { dayjs.locale(language === "ru" ? "ru-ru" : language); i18next.changeLanguage(language); } }, [language]); useEffect(() => { if (start_page) { navigate(start_page, { replace: true }); } }, [start_page]); if (!routersEles) { return
; } const AppLayout = () => { const { state, isMobile } = useSidebar(); const location = useLocation(); const routersEles = useRoutes(routers); return ( <>
{routersEles && React.cloneElement(routersEles, { key: location.pathname })}
); }; return ( ); }; export default Layout;