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
@@ -0,0 +1,140 @@
import { useEffect, useState } from "react";
import { Group, Stack, Switch, Text } from "@mantine/core";
import { Button, Panel } from "../../ui/primitives";
import { toast } from "../../ui/feedback";
import { t } from "../../i18n";
import {
chooseDownloadDir,
effectiveDownloadDir,
loadDesktopSettings,
saveDesktopSettings,
type DesktopConfig,
} from "../../net/desktop";
// DesktopSettings 是桌面专属的设置区块,仅在 Wails 壳内渲染(调用点用 isDesktop()
// 门控)。复用 web 设计系统(Panel + Mantine Switch)。
export function DesktopSettings()
{
const [ cfg, setCfg ] = useState<DesktopConfig | null>(null);
const [ saving, setSaving ] = useState(false);
// 当前实际落盘目录(配置覆盖或系统默认),仅用于显示;config 里 download_dir
// 为空时这里仍展示解析后的默认路径。
const [ effectiveDir, setEffectiveDir ] = useState("");
useEffect(() =>
{
let alive = true;
void loadDesktopSettings().then((c) => { if (alive && c) { setCfg(c); } });
void effectiveDownloadDir().then((d) => { if (alive) { setEffectiveDir(d); } });
return () => { alive = false; };
}, []);
const update = async (patch: Partial<DesktopConfig>) =>
{
if (!cfg) { return; }
const prev = cfg;
const next = { ...cfg, ...patch };
setCfg(next);
setSaving(true);
try
{
await saveDesktopSettings(next);
}
catch (e)
{
setCfg(prev); // 回滚 UI 到保存前状态
toast.error(e instanceof Error ? e.message : String(e));
}
finally
{
setSaving(false);
}
};
const chooseDir = async () =>
{
const picked = await chooseDownloadDir(t("settings.desktop.downloadDirPicker"));
if (!picked) { return; } // 用户取消
await update({ download_dir: picked });
setEffectiveDir(picked);
};
const resetDir = async () =>
{
await update({ download_dir: "" }); // 空=回到系统默认目录
setEffectiveDir(await effectiveDownloadDir());
};
if (!cfg) { return null; }
return (
<Panel title={t("settings.desktop.title")}>
<Stack gap="md">
<ToggleRow
title={t("settings.desktop.clipboardSync")}
hint={t("settings.desktop.clipboardSyncHint")}
checked={cfg.clipboard_sync_enabled}
disabled={saving}
onChange={(v) => void update({ clipboard_sync_enabled: v })}
/>
<ToggleRow
title={t("settings.desktop.launchAtLogin")}
hint={t("settings.desktop.launchAtLoginHint")}
checked={cfg.launch_at_login}
disabled={saving}
onChange={(v) => void update({ launch_at_login: v })}
/>
<Group justify="space-between" wrap="nowrap" align="flex-start">
<Stack gap={2} style={{ minWidth: 0 }}>
<Text size="sm" fw={500}>{t("settings.desktop.downloadDir")}</Text>
<Text size="xs" c="dimmed">{t("settings.desktop.downloadDirHint")}</Text>
<Text
size="xs"
style={{ fontFamily: "var(--font-mono, ui-monospace, monospace)", wordBreak: "break-all" }}
>
{effectiveDir}
</Text>
</Stack>
<Stack gap={4} style={{ flexShrink: 0 }}>
<Button variant="secondary" size="sm" disabled={saving} onClick={() => void chooseDir()}>
{t("settings.desktop.downloadDirChoose")}
</Button>
{cfg.download_dir !== "" && (
<Button variant="ghost" size="sm" disabled={saving} onClick={() => void resetDir()}>
{t("settings.desktop.downloadDirReset")}
</Button>
)}
</Stack>
</Group>
<div style={{ height: 1, background: "var(--divider)" }} />
<Text size="xs" c="dimmed">
{t("settings.desktop.ttlNote")}
</Text>
</Stack>
</Panel>
);
}
function ToggleRow(props: {
title: string;
hint: string;
checked: boolean;
disabled: boolean;
onChange: (value: boolean) => void;
})
{
const { title, hint, checked, disabled, onChange } = props;
return (
<Group justify="space-between" wrap="nowrap" align="flex-start">
<Stack gap={2} style={{ minWidth: 0 }}>
<Text size="sm" fw={500}>{title}</Text>
<Text size="xs" c="dimmed">{hint}</Text>
</Stack>
<Switch
checked={checked}
disabled={disabled}
onChange={(e) => onChange(e.currentTarget.checked)}
/>
</Group>
);
}