f21fa5b5e8
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)。 本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
141 lines
5.1 KiB
TypeScript
141 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|