feat(app-iced): M109 CONTENTDL —— 内容库页(整合包/Mod/资源包/光影)

用户令「下载页只负责下载整合包和Mod/光影」:新开一页做**内容发现与装入**,
检索走 Modrinth(优先,无需 key)/ CurseForge(`ULL_CF_API_KEY` 环境变量,日志只打
set/unset),装入走 core 的下载引擎真下到实例目录,进度/取消/失败复用下载页那块安装面板。

- **入口(B1)**:`Route::Content` 是新顶层路由,但 `Route::nav()` 复用 `NavSection::Download`
  ——导航轨仍五项,页头右上是「版本管理 | 内容库」一级切换(版本管理页也加了同一个切换,
  否则内容库只有 `ULL_START_ROUTE` 能到)
- **页内四条一级侧栏**(`pages/content/layout.rs` 四档几何:992/856/648 三个门槛都是
  「Σ栏宽 + 间距」算出来的):整合包 / Mod / 资源包 / 光影;标签栏 200(`Resource packs`
  比 M107 的版本类型名长,140 会切字,已登记偏离)
- **筛选**:关键词(草稿 + 显式「搜索」按钮,不逐字打网络)、来源、游戏版本、加载器
  (Fabric/Forge/NeoForge/Quilt)、排序;**分页**(「加载更多」)而不是无限滚动——快照只有一个槽、
  翻页要看得见、状态要可截图复现
- **四态**:加载 / 空 / 失败 / 有数据 + 「没有更多了」;筛选条在任何状态下都在(加载中藏筛选条
  等于把入口收走)
- **一键装入**:`Content::blockers()` 是纯函数——版本不匹配、加载器不匹配、没有目标实例、
  整合包缺管线**四条原因逐条**给出,按钮点之前就禁用;判定与画法分开(model/tests 直接钉)
- **整合包今天不可装**:`ContentKind::Modpack.dir()` 是 `None`,UI 禁用 + 说明,host 侧再挡
  一次(绕过 UI 也只会回一条 Failed)。真流程是 M111 的导入管线
- **零 UI 网络**:`presenter/host/addon.rs` + `addon/{sources,http,install}.rs`;无 header 的请求走
  core 的 `download_bytes`(信号量限流 + 健康度 + 退避全现成),带 `X-API-KEY` 的只有 CF 一条,
  自带 429/5xx 退避(优先吃 `Retry-After`,封顶 30s);密钥不进 DTO/日志/仓库
- 文案 41 条中英进 `crates/i18n`(新域接在装配链末尾 ⇒ `Key::ALL` 与金表只追加)
- 演示数据 `ContentHit → Hit::of` 与产品路径同源(格式化不会两套),`ULL_CONTENT_KIND` /
  `ULL_CONTENT_STATE` 供出图矩阵选类型与状态

取证:`DevDocs/artifacts/m109/`(10 张:四类内容 × 浅/深 + 搜索无结果 + 加载器不兼容禁用)

门禁:fmt ✓ / clippy `-D warnings`(三 crate)✓ / app-iced 450+6、presenter 170、i18n 24 全绿;
`cargo tree` 两条零 UI 门禁仍空;全部改动文件 ≤400 行。
This commit is contained in:
AskaEth committed 2026-11-05 03:20:21 +08:00
1 parent 5adac230fe
commit 6b161227d8
42 files changed
+4071 -25

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
//! 内容库文案(M109 `CONTENTDL`)。
//!
//! 一句话一个 key:带变量的句子用 `{name}`,页面只做 [`crate::Catalog::render`],不拼片段。
//!
//! ## 三条口径
//!
//! 1. **只说能做的事**:整合包那条写的是「要等导入管线」,不是「稍后自动支持」;
//! 2. **站名也进词表**:`Modrinth` / `CurseForge` 的**中文译文必须与英文不同**
//! (`catalog/tests.rs` 的 `chinese_catalog_is_actually_chinese` 会红),所以中文写成「Modrinth 源」;
//! 3. **禁用态要能解释自己**:`ContentIncompatible*` / `ContentModpackNotReady` 就是禁用按钮旁边那几句话——
//! 没有它们,禁用就等于「点了没反应」。
//!
//! 域文件的形状见 `crate::catalog` 的「域装配」一节。
macro_rules! content_entries {
($next:ident, $($rest:ident,)* { $($acc:tt)* }) => {
$next! { $($rest,)* {
$($acc)*
// --- content library page (M109) ---
ContentTitle => ("Content", "内容库"),
ContentSubtitle => (
"Modpacks, mods, resource packs and shaders",
"整合包、Mod、资源包与光影"
),
ContentSectionVersions => ("Version management", "版本管理"),
ContentSectionContent => ("Content library", "内容库"),
ContentKindModpack => ("Modpacks", "整合包"),
ContentKindMod => ("Mods", "Mod"),
ContentKindResourcepack => ("Resource packs", "资源包"),
ContentKindShader => ("Shaders", "光影"),
ContentSourceModrinth => ("Modrinth", "Modrinth 源"),
ContentSourceCurseforge => ("CurseForge", "CurseForge 源"),
ContentSortRelevance => ("Relevance", "相关度"),
ContentSortDownloads => ("Downloads", "下载量"),
ContentSortNewest => ("Newest", "最新发布"),
ContentSortUpdated => ("Recently updated", "最近更新"),
ContentSearchPlaceholder => ("Search content…", "搜索内容…"),
ContentSearch => ("Search", "搜索"),
ContentFilterVersion => ("Game version", "游戏版本"),
ContentFilterLoader => ("Loader", "加载器"),
ContentFilterAny => ("Any", "不限"),
ContentCount => ("{count} results", "{count} 个结果"),
ContentLoading => ("Loading content…", "正在加载内容…"),
ContentLoadingHint => (
"Results come from the network; nothing is cached offline yet.",
"结果来自网络,暂时没有离线缓存。"
),
ContentEmptyTitle => ("Nothing matched", "没有匹配的内容"),
ContentEmptyBody => (
"Try another keyword, or clear a filter.",
"换个关键词,或清掉一个筛选条件。"
),
ContentFailedTitle => ("Could not load content", "内容加载失败"),
ContentFailedBody => ("Check the network, then retry.", "检查网络后重试。"),
ContentRetry => ("Retry", "重试"),
ContentLoadMore => ("Load more", "加载更多"),
ContentNoMore => ("No more results", "没有更多了"),
ContentDetailEmpty => (
"Pick an item to see what it supports.",
"选一个项目看它支持什么。"
),
ContentDetailDownloads => ("{count} downloads", "下载量 {count}"),
ContentDetailUpdated => ("Updated {date}", "更新于 {date}"),
ContentDetailLoaders => ("Loaders: {list}", "加载器:{list}"),
ContentDetailVersions => ("Game versions: {list}", "游戏版本:{list}"),
ContentInstall => ("Install into instance", "装入实例"),
ContentTargetInstance => ("Target instance", "目标实例"),
ContentNoInstances => ("No instance to install into", "没有可装入的实例"),
ContentIncompatibleVersion => (
"Needs {version}; this instance runs {actual}",
"需要 {version},该实例运行的是 {actual}"
),
ContentIncompatibleLoader => ("Not built for {loader}", "不支持 {loader}"),
ContentModpackNotReady => (
"Modpack install comes with the M111 import pipeline",
"整合包安装要等 M111 的导入管线"
),
ContentSourceUnavailable => (
"Set ULL_CF_API_KEY to search CurseForge",
"要搜 CurseForge 请先设置 ULL_CF_API_KEY"
),
} }
};
}
pub(crate) use content_entries;