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(() => {});
});
}