import "@mantine/core/styles.css"; import "@mantine/dropzone/styles.css"; import "./styles/index.css"; import React, { useEffect } from "react"; import ReactDOM from "react-dom/client"; import { MantineProvider, createTheme, type MantineColorsTuple, } from "@mantine/core"; import { RouterProvider, createRouter } from "@tanstack/react-router"; import { ToastViewport } from "./ui/feedback"; import { routeTree } from "./routeTree.gen"; import { useAppStore, type ThemeMode } from "./store"; import { bootstrapAuth } from "./features/auth/auth"; import { startCjkAutospace } from "./utils/cjkAutospace"; import { initDesktopMenuBridge, isDesktop, loadDesktopSettings } from "./net/desktop"; // Mantine 需要 10 阶静态色,硬编码值与 tokens.semantic.css 的 --accent-* 阶 // 严格对齐(Dracula Purple 谱)。组件颜色仍读 var(--accent),这里仅满足 // Mantine 内部 primaryColor 索引。 // // primaryShade = { light: 7, dark: 4 }: // - light 取 [7] = #644AC9(Alucard Purple) // - dark 取 [4] = #AA99FF(Dracula Base BrightBlue,Purple 的官方亮版, // 在深色背景上对比度 6.7:1,作链接/选中文字可读性达 AAA Large) const accentTuple: MantineColorsTuple = [ "#F3F0FF", "#E4DEFF", "#C9BEFF", "#AC9BFE", "#AA99FF", "#9580FF", "#7458EE", "#644AC9", "#523CA8", "#3F2D85", ]; const theme = createTheme({ primaryColor: "accent", primaryShade: { light: 7, dark: 4 }, defaultRadius: "md", radius: { sm: "6px", md: "10px", lg: "14px" }, fontFamily: "var(--font-sans)", fontFamilyMonospace: "var(--font-mono)", headings: { fontFamily: "var(--font-sans)" }, colors: { accent: accentTuple }, }); const router = createRouter({ routeTree, defaultPreload: "intent" }); // 桌面端:菜单栏「设置」→ 路由到 /settings(浏览器里为 no-op)。 initDesktopMenuBridge(() => { void router.navigate({ to: "/settings" }); }); // 桌面端:启动即加载本地配置,把剪贴板同步开关应用到门控(浏览器里为 no-op)。 void loadDesktopSettings(); // Cmd+,(macOS)/ Ctrl+,(Windows·Linux)→ 打开设置,桌面与浏览器通用。桌面端 // 原生菜单未占用 ⌘,,故按键会落到 WebView 由此处理;未登录时 /settings 守卫会 // 自行重定向,无需额外判断。 window.addEventListener("keydown", (e) => { if ((e.metaKey || e.ctrlKey) && !e.altKey && e.key === ",") { e.preventDefault(); void router.navigate({ to: "/settings" }); } }); declare module "@tanstack/react-router" { interface Register { router: typeof router; } } // system 模式:移除 data-theme attribute,让 tokens.semantic.css 中的 // @media (prefers-color-scheme: dark) 接管;light / dark 时写死 attribute。 function applyTheme(mode: ThemeMode): void { const root = document.documentElement; if (mode === "system") { root.removeAttribute("data-theme"); } else { root.setAttribute("data-theme", mode); } } // 切语言时给 RouterProvider 换 key 强制全树重挂载,让所有 t() 调用以新 locale // 重新求值;t() 仍是纯函数,避免改造每个调用点。这样比 location.reload() 安全 // (内存里的 refresh_token 不丢)。 function App() { const locale = useAppStore((s) => s.locale); const theme$ = useAppStore((s) => s.theme); useEffect(() => { applyTheme(theme$); }, [ theme$ ]); // Mantine 自身也维护 colorScheme(决定 [data-mantine-color-scheme])。 // light / dark 用 forceColorScheme 锁死;system 时不传 force,让其 // 内部跟随 prefers-color-scheme。store.theme 作为单一来源同步双方。 const force = theme$ === "system" ? undefined : theme$; return ( ); } // 首屏在 React 挂载前先应用一次主题,避免 FOUC。桌面壳的登录态已在 store 初始化 // 时从注入的 window.__CDROP_BOOT__ 同步水合(store/helpers.ts),无需异步等待。 applyTheme(useAppStore.getState().theme); function mount(): void { ReactDOM.createRoot(document.getElementById("root")!).render( , ); // CJK 自动间距 shim:在 React 完成首次挂载后启动。MutationObserver 后续 // 增量处理 React 重渲染产生的新文本节点。放在 createRoot 之后,首帧绘制 // 完会触发一次 runShim() 处理已挂载的子树。 requestAnimationFrame(() => { startCjkAutospace(); }); } // 浏览器端「免重登」:首屏渲染前用 HttpOnly cookie 尝试静默续期。命中则直接以已 // 登录态挂载,避免登录页 / 设置页闪现;未命中(401)即照常落到登录页。桌面 / dev / // sessionStorage 已有令牌时 bootstrapAuth 立即返回,几乎无延迟。4s 超时兜底:万一 // refresh 卡死,也不至于把整个应用永久挡在首屏外。 const BOOT_AUTH_TIMEOUT_MS = 4000; void Promise.race([ bootstrapAuth().catch(() => { /* ignore:失败即落登录页 */ }), new Promise((resolve) => { window.setTimeout(resolve, BOOT_AUTH_TIMEOUT_MS); }), ]).finally(mount); // PWA service worker:仅在浏览器 + prod 注册。dev 下会干扰 Vite HMR;桌面壳 // (Wails WebView,wails:// / loopback origin)已是原生应用且 SW 行为不可靠, // 故跳过。SW 只缓存静态壳、绝不拦截 /api(SSE / 剪贴板 / 中继),见 public/sw.js。 if (import.meta.env.PROD && !isDesktop() && "serviceWorker" in navigator) { window.addEventListener("load", () => { void navigator.serviceWorker.register("/sw.js").catch(() => {}); }); }