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,94 @@
|
||||
/*
|
||||
* CJK 排版 —— 薄壳。核心由聚珍(Juzhen,cjk-autospace submodule 2.x)提供,
|
||||
* 本档只负责 cdrop 的实例配置与 React 生命周期下的增量观察。
|
||||
*
|
||||
* 上游 lib:git.commilitia.net/admin/cjk-autospace.git(聚珍 / Juzhen)
|
||||
* API:createJuzhen(options).render(root) / .revert(root)
|
||||
* 细节:lib/cjk-autospace/{README.md,ARCHITECTURE.md};CSS 必配(视觉尺寸
|
||||
* 全在 dist/juzhen.css,由 styles/index.css @import 引入)。
|
||||
*
|
||||
* 功能分级(聚珍 §6.5):功能分两级,按元素分配。
|
||||
* 文本级(任意文本片段适用):
|
||||
* - spacing 中西间隙(CJK ↔ 西文边界插 copy-clean 的 jz-hws)
|
||||
* - jiya 标点挤压(charify 为 jz-char;本项目全角式 → 句内点号不挤,
|
||||
* 仅承载 justify 单份间距与行端收缩)
|
||||
* 段落级(依赖行/段布局,仅作用于段落级元素):
|
||||
* - jinze 禁则避头尾(闭类/点号绑前字、开类绑后字,亦为 justify 载体)
|
||||
* - orphan 垂悬字避免(段末行保留 ≥2 个 CJK 实义字;上游新功能、默认开)
|
||||
* - longWord 长英文段 <span lang="en"> 包装(配 CSS hyphens 原生断词)
|
||||
*
|
||||
* 分级分配(level.text + data-jz-level):cdrop 是 UI 重的工具,绝大多数是 chrome
|
||||
* (按钮/徽章/设备名/标签,单行短文,段落级功能有害无益)。故全树默认文本级
|
||||
* (level.text ".juzhen" 命中作用域根),仅真正的流动正文用 data-jz-level="paragraph"
|
||||
* opt-in 回段落级——消息正文、剪贴板内容、Callout/Toast 正文、Field hint/error、
|
||||
* 登录说明,均已带全局 .justify 类。失败模式良性:漏标的 prose 只少了段落功能,
|
||||
* 不会让 chrome 被强加 orphan/jinze。(hanging 标点悬挂已被上游移除:纯负 margin
|
||||
* 机制级死路。)
|
||||
*
|
||||
* 政策:全角式(quanjiao)—— lang.style 全局覆盖聚珍内置逐语言预设(默认
|
||||
* 简体走开明式)。无 lang 祖先时回退 zh-Hans。html lang(zh-CN/zh-TW/en-US)
|
||||
* 由聚珍 classify 归类(zh-cn→zh-Hans、zh-tw→zh-Hant、en-us→other)。
|
||||
*
|
||||
* 作用域:scope.include ".juzhen" —— 仅处理带 .juzhen 类的子树(#root 已加),
|
||||
* normalize 与处理均收敛于此域,body 上的非 .juzhen 节点(若有 portal)不受扰。
|
||||
*
|
||||
* 增量更新:MutationObserver(childList + subtree) 监听 React 重渲染产生的新
|
||||
* 节点;render 幂等(聚珍对各 pass 产物打标记,重跑跳过),处理期间临时断开
|
||||
* observer 避免自反馈循环。
|
||||
*/
|
||||
|
||||
import { createJuzhen } from "cjk-autospace";
|
||||
|
||||
// 行内元素三分类(聚珍 §4.1)——按元素的「语义本质」划分,而非看当前内容:
|
||||
// 透明(styleInlines,如 <strong> / <em>):纯样式,遍历穿越、run 延续、可切分。
|
||||
// 隔离(isolateSelector):视觉文本块——自带视觉容器(背景/边框)、但内部仍是要
|
||||
// 正常排版的文字。两端补边界间距、断 run,内部照常处理(spacing/jiya)。Badge
|
||||
// 经全局类 .cjk-isolate 加入;<mark> 高亮同属此类(含在默认 "mark" 里)。
|
||||
// 整体(pillSelector):不希望样式被改动的部分——code / kbd / 公式,内部对一切 pass
|
||||
// 不可见、禁则整体绑定(改内部间距会破坏代码/公式语义)。
|
||||
const jz = createJuzhen({
|
||||
lang: { default: "zh-Hans", style: "quanjiao" },
|
||||
scope: { include: ".juzhen" },
|
||||
// 全树默认文本级(命中作用域根 .juzhen);prose 各自 data-jz-level="paragraph"
|
||||
// 覆盖回段落级。见上「分级分配」。
|
||||
level: { text: ".juzhen" },
|
||||
pillSelector: "code, kbd",
|
||||
isolateSelector: "mark, .cjk-isolate",
|
||||
});
|
||||
|
||||
let observer: MutationObserver | null = null;
|
||||
let pending = false;
|
||||
|
||||
function runShim(): void
|
||||
{
|
||||
observer?.disconnect();
|
||||
try { jz.render(document.body); }
|
||||
finally
|
||||
{
|
||||
observer?.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
|
||||
function schedule(): void
|
||||
{
|
||||
if (pending) { return; }
|
||||
pending = true;
|
||||
queueMicrotask(() =>
|
||||
{
|
||||
pending = false;
|
||||
runShim();
|
||||
});
|
||||
}
|
||||
|
||||
export function startCjkAutospace(): () => void
|
||||
{
|
||||
runShim();
|
||||
observer = new MutationObserver(schedule);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
return () =>
|
||||
{
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { t } from "../i18n";
|
||||
|
||||
// 字节数→可读字符串(语言无关,单位是国际通用 SI 简写)。
|
||||
export function formatBytes(bytes: number): string
|
||||
{
|
||||
if (bytes < 1024) { return `${bytes} B`; }
|
||||
if (bytes < 1024 * 1024) { return `${( bytes / 1024 ).toFixed(1)} KB`; }
|
||||
if (bytes < 1024 * 1024 * 1024) { return `${( bytes / 1024 / 1024 ).toFixed(1)} MB`; }
|
||||
return `${( bytes / 1024 / 1024 / 1024 ).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
// 设备名称全局限制为 ASCII:名字要塞进 X-Device-Name 这个 HTTP 头,非 ASCII 在
|
||||
// 浏览器 fetch 里会直接抛错、也无法可靠传输,故输入处校验、传输前兜底剥离。
|
||||
const ASCII_DEVICE_NAME = /^[\x20-\x7E]*$/;
|
||||
|
||||
// isAsciiDeviceName 判断名字是否仅由可打印 ASCII 组成(输入校验用)。
|
||||
export function isAsciiDeviceName(name: string): boolean
|
||||
{
|
||||
return ASCII_DEVICE_NAME.test(name);
|
||||
}
|
||||
|
||||
// toAsciiDeviceName 剥掉非可打印 ASCII 并 trim,与后端 sanitizeDeviceName 对齐——
|
||||
// 给旧的非 ASCII 名字兜底,避免设进 X-Device-Name 头时 fetch 抛错。
|
||||
export function toAsciiDeviceName(name: string): string
|
||||
{
|
||||
let out = "";
|
||||
for (const ch of name)
|
||||
{
|
||||
const c = ch.codePointAt(0) ?? 0;
|
||||
if (c >= 0x20 && c <= 0x7e) { out += ch; }
|
||||
}
|
||||
return out.trim();
|
||||
}
|
||||
|
||||
// 相对时间("刚刚" / "5 分钟前")。> 1 天回退到本地化的绝对日期时间。
|
||||
export function formatRelative(epochSec: number): string
|
||||
{
|
||||
if (!epochSec) { return ""; }
|
||||
const sec = Math.floor(Date.now() / 1000) - epochSec;
|
||||
if (sec < 5) { return t("time.justNow"); }
|
||||
if (sec < 60) { return t("time.secondsAgo", { n: sec }); }
|
||||
if (sec < 3600) { return t("time.minutesAgo", { n: Math.floor(sec / 60) }); }
|
||||
if (sec < 86400) { return t("time.hoursAgo", { n: Math.floor(sec / 3600) }); }
|
||||
return new Date(epochSec * 1000).toLocaleString();
|
||||
}
|
||||
Reference in New Issue
Block a user