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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user