cdrop — 跨 OS 剪贴板与文件传输服务

Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。

- 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。
- 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。
- 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。
- 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。

架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。

本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
2026-06-15 21:38:28 +08:00
commit f21fa5b5e8
239 changed files with 29010 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
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 { startCjkAutospace } from "./utils/cjkAutospace";
import { initDesktopMenuBridge, loadDesktopSettings } from "./net/desktop";
// Mantine 需要 10 阶静态色,硬编码值与 tokens.semantic.css 的 --accent-* 阶
// 严格对齐(Dracula Purple 谱)。组件颜色仍读 var(--accent),这里仅满足
// Mantine 内部 primaryColor 索引。
//
// primaryShade = { light: 7, dark: 4 }
// - light 取 [7] = #644AC9Alucard Purple
// - dark 取 [4] = #AA99FFDracula Base BrightBluePurple 的官方亮版,
// 在深色背景上对比度 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 (
<MantineProvider
theme={theme}
defaultColorScheme="auto"
forceColorScheme={force}
>
<ToastViewport />
<RouterProvider key={locale} router={router} />
</MantineProvider>
);
}
// 首屏在 React 挂载前先应用一次主题,避免 FOUC。桌面壳的登录态已在 store 初始化
// 时从注入的 window.__CDROP_BOOT__ 同步水合(store/helpers.ts),无需异步等待。
applyTheme(useAppStore.getState().theme);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
// CJK 自动间距 shim:在 React 完成首次挂载后启动。MutationObserver 后续
// 增量处理 React 重渲染产生的新文本节点。放在 createRoot 之后,首帧绘制
// 完会触发一次 runShim() 处理已挂载的子树。
requestAnimationFrame(() => { startCjkAutospace(); });