feat(m168): 中文字体 CDN 下载真接线(D28 后半)
M167 调研的结论是「中文字体 CDN 下载至今未实装」:`i18n::font` 只有 `Fetcher` trait 与
假实现,`needs-download` 分支什么也不做。这条把它接成一条真链路。
判据 → 下载 → 注册(一条链,四个 crate 各管一段):
* `unlucky_i18n::font`(零依赖):`Source::Pinned { urls, sha256, bytes }` 把「下什么」
钉死——google/fonts 的 `NotoSansSC[wght].ttf` 钉到 commit `2894aab…`(不是 `@main`),
17 772 300 B / sha256 `a3041811…`。可变字体一份覆盖 wght 100–900,省掉三份静态子集的 25 MiB。
`FetchError` 补第三档 `Disk`(网络 / 校验 / 磁盘,UI 按类别选词)。
* `unlucky_core::download::font`(新):缓存命中 → 共享引擎 `download_bytes` → 复验(sha256 +
sfnt 魔数 + 32 MiB 上限)→ 原子落盘。**复验在写盘之前**,所以「哈希不符」时盘上什么都没有;
缓存按 `<sha256 前 16 位>` 命名,pin 一换自动换名。
* `unlucky_app_iced::fonts::{cdn,install}`(新):`CdnFetcher` 把 pin 接进 core(`ui.font_cdn`
可覆盖 URL,哈希不换);`install` 是状态机 + 后台线程,只有 `NeedsDownload` 才起线程
(英文 / 系统已有 / 探测不出一律零网络)。注册走 `iced_graphics::text::FontSystem::load_font`,
族名从字体库读回来核对。失败只降级(继续用系统字体),不阻塞、不崩。
* 接入两条路径:`fonts::resolve_with`(启动)与 `fonts::cjk_for`(设置页切语言)都调
`install::after_probe`;设置页 General 组多一行状态(下载中 / 完成 / 失败 / 无需下载 + 三类失败
原因),文案进 catalog 金表(链尾追加,既有顺序不动);`App::advance` 每秒比对状态快照后重建
视图模型,所以那一行会自己刷新。
配置:新增 `ui.font_cdn`(`keys/font.rs`,schema 1.7.0),空 = 内置 jsDelivr 镜像(含备用边缘)。
守卫(三条都能变红,变异证明见报告):core 侧 8 条(缓存不联网 / 哈希不符拒落盘 / HTML 不当字体
/ 坏缓存自愈 / 上限 / pin 校验),app 侧 6 条(三种闲置策略零调用、失败不注册、类别不串、幂等、
截图钉子、单行日志),页面侧 1 条(四态 × 两语言无缺译、不写假百分比)。
测试:`cargo test -p unlucky-i18n --lib` 31 绿;`-p unlucky-core --lib` 622 绿;
`-p unlucky-app-iced --lib` 684 绿;`cargo fmt --check` 干净。截图三张见报告。
This commit is contained in:
1 parent
2a004a5754
commit
449ded2cb7
22 files changed
+2191
-184
No files matched your search
@@ -33,12 +33,12 @@ pub const LOG_LEVELS: &[&str] = &["error", "warn", "info", "debug", "trace"];
|
||||
pub const INSTANCE_MODES: &[&str] = &["isolated", "shared"];
|
||||
/// `java.installRoot` 未设置时的哨兵(空串 = 按数据目录模式派生落点)。
|
||||
pub const JAVA_INSTALL_ROOT_UNSET: &str = "";
|
||||
/// `java.vendorPriority` 的允许值(D23:四家发行版,顺序即优先级)。
|
||||
/// 与 `game::java::vendor::VENDOR_IDS` 必须同一份集合(setting 层不依赖 game 层),
|
||||
/// 一致性由 `tests/setting/java_install_keys.rs` 的契约测试固定。
|
||||
/// `java.vendorPriority` 的允许值(D23:四家发行版,顺序即优先级)。与 `game::java::vendor::VENDOR_IDS`
|
||||
/// 必须同一份集合(setting 层不依赖 game 层),一致性由 `tests/setting/java_install_keys.rs` 守。
|
||||
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
||||
|
||||
/// 自更新两键(M161;拆到自己的文件守 400 行)。
|
||||
/// 拆到各自文件的键域:中文字体 CDN(M168)/ 自更新(M161)。主表各留一行登记。
|
||||
pub mod font;
|
||||
pub mod update;
|
||||
|
||||
/// launcher.json 当前 schema 版本。
|
||||
@@ -54,17 +54,17 @@ pub mod update;
|
||||
/// `1.5.0` 登记 M116 的窗口圆角策略(`appearance.window_corner`,出厂 `system`):纯增量。
|
||||
/// `1.6.0` 登记 M161 的「自更新」两键(见 [`update`]):纯增量但带一步数据迁移
|
||||
/// (此前住在 `cache/update/prefs.json`,迁移见 [`super::update_prefs`])。
|
||||
/// `1.7.0` 登记 M168 的中文字体下载地址覆盖键(`ui.font_cdn`,见 [`font`]):纯增量、无迁移。
|
||||
///
|
||||
/// **旧读新(方向相反,措辞必须精确)**:1.4.0 的客户端读 1.5.0 的文件时,
|
||||
/// [`super::schema::SchemaCompat::check_url`] 走 `found.minor > expected.minor` →
|
||||
/// `ReadOnlyPreserveSchema` → [`super::schema::SchemaCompat::access`] → `FileAccess::ReadOnly`。
|
||||
/// 也就是说**整份配置对它只读**(值照读、`store.set` 一律 `SettingError::ReadOnly`、
|
||||
/// `save` 跳过),不是「只是少一档」。这是 minor 提升的既定代价——也正是这条键必须推 minor
|
||||
/// 而不是偷偷塞进 1.4.0 的原因:回退到旧版本会出现「设置改不动」,而原因只在
|
||||
/// `$schema` 那一行里,不看版本号无从排查。
|
||||
/// 也就是说**整份配置对它只读**(值照读、`store.set` 一律 `SettingError::ReadOnly`、`save` 跳过),
|
||||
/// 不是「只是少一档」。这是 minor 提升的既定代价——也正是新键必须推 minor 而不是偷偷塞进旧版本的
|
||||
/// 原因:回退到旧版本会出现「设置改不动」,而原因只在 `$schema` 那一行里,不看版本号无从排查。
|
||||
#[must_use]
|
||||
pub fn launcher_version() -> SchemaVersion {
|
||||
SchemaVersion::new(1, 6, 0)
|
||||
SchemaVersion::new(1, 7, 0)
|
||||
}
|
||||
|
||||
/// `config/launcher.json` 的 schema。
|
||||
@@ -396,4 +396,5 @@ pub fn launcher_schema() -> SettingSchema {
|
||||
"启动器日志级别",
|
||||
))
|
||||
.with_all(update::specs())
|
||||
.with(font::cdn_spec())
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//! 中文字体下载地址的覆盖键(M168:D28 后半「下载开源中文字体」)。
|
||||
//!
|
||||
//! 从 `keys.rs` 拆出(那边贴着 400 行硬线):主表只留一行 `.with(font::cdn_spec())`。
|
||||
//!
|
||||
//! ## 为什么需要这个键
|
||||
//!
|
||||
//! 内置 pin(`unlucky_i18n::font::NOTO_SANS_SC`)把 URL 钉到了 jsDelivr 的一个 git commit 上
|
||||
//! ——那是**一个**出口。用户所在网络如果连不上它(或想走自建镜像 / 代理),要有办法换,否则
|
||||
//! 「自动下载中文字体」在某些网络上等于不存在。
|
||||
//!
|
||||
//! ## 为什么只换 URL,不换哈希
|
||||
//!
|
||||
//! 字体是**注册进渲染器的可执行内容**,不是一张图。这个键只允许改「从哪拿」,`sha256` 与字节数
|
||||
//! 仍取内置那一份:镜像给了别的内容 ⇒ 校验当场拒 ⇒ 降级回系统字体,并在设置页那一行说「镜像
|
||||
//! 内容校验不通过」。**宁可不下载,也不把没校验过的东西注册成字体。**
|
||||
//!
|
||||
//! 想让用户能换「整套字体」(换族、换子集)就得连哈希一起进配置——那是另一件事:现在的 pin 是
|
||||
//! 代码里的常量,改它要发版。理由是此刻只有一套官方子集,多条可写路径只会多一个「校验值和字体
|
||||
//! 对不上」的支持面。
|
||||
|
||||
use crate::setting::spec::{AiPolicy, SettingSpec};
|
||||
|
||||
/// `ui.font_cdn` 的登记项(走 [`SettingSpec::text`]:空串 = 用内置镜像)。
|
||||
#[must_use]
|
||||
pub fn cdn_spec() -> SettingSpec {
|
||||
SettingSpec::text(
|
||||
"ui.font_cdn",
|
||||
"",
|
||||
512,
|
||||
AiPolicy::audited(),
|
||||
"中文字体下载地址(空 = 内置国内可达镜像;填整条 https URL,内容必须与内置 sha256 一致)",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
/// 默认值是空串(= 用内置镜像),且不是 `Path` 类型——空串对 `Path` 会被 schema 拒。
|
||||
///
|
||||
/// 抓得住:有人把键改成 `SettingSpec::path(...)`(空串过不了校验 ⇒ 出厂配置自己就非法)。
|
||||
#[test]
|
||||
fn the_cdn_key_defaults_to_the_builtin_mirror() {
|
||||
let spec = cdn_spec();
|
||||
assert_eq!(spec.key, "ui.font_cdn");
|
||||
assert_eq!(spec.default, json!(""));
|
||||
assert!(
|
||||
spec.kind.check(&json!("")).is_ok(),
|
||||
"空串(= 内置镜像)必须过校验"
|
||||
);
|
||||
assert!(
|
||||
spec.kind
|
||||
.check(&json!("https://mirror.example/NotoSansSC.ttf"))
|
||||
.is_ok(),
|
||||
"一条 https URL 必须过校验"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user