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

+5
View File
@@ -113,6 +113,7 @@ macro_rules! finish {
}
mod agent;
mod content;
mod core_text;
mod download;
mod font;
@@ -131,6 +132,7 @@ mod golden;
mod tests;
use agent::agent_entries;
use content::content_entries;
use core_text::core_text_entries;
use download::{download_entries, version_entries};
use font::font_entries;
@@ -167,6 +169,9 @@ nav_entries! {
// `general_entries` 已经在 main 上占了链尾,所以本域排在它们**之后**:
// `Key::ALL` 顺序 = 金表顺序 = 先 main 的全部域,再 version。
version_entries,
// M109:内容库页。与 M107 同一条纪律——本域排在 main 已占住的链尾**之后**,
// 于是 `Key::ALL` 顺序 = 金表顺序 = 先 main 的全部域,再 version,最后 content。
content_entries,
finish,
{}
}
+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;
+42
View File
@@ -176,4 +176,46 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
("VersionSearchHitsOne", "1 match", "1 个匹配"),
("VersionSearchElsewhereTitle", "Found in another type", "在别的类型里找到了"),
("VersionSearchElsewhereOne", "1 version matches, but it is in another type.", "有 1 个版本匹配,但它在别的类型里。"),
// --- M109 内容库页(同样追加在链尾:`Key::ALL` 顺序 = 金表顺序) ---
("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"),
];