M186 把「可只重试失败项」算到了 core/presenter(DependencyPlan::retry_request()), 但 DTO 没字段、界面没按钮,用户拿不到。这一轮补最后一公里: - DTO:ContentFailureKind 加 Incompatible 一档(不再借 Unsupported,与「缺密钥」分开); InstallProgressView 加 retry: Vec<InstallFailureView>(project + 档位 + core 原话), 档位是专门的 InstallFailureKind(Incompatible / Unreachable,穷尽映射 core 的 FailureKind); ContentInstallRequest 加 retry: bool(重试路径唯一读的字段是 instance)。 - presenter:plan.rs 把上一轮失败的计划按 host+实例记住(与 addon/pack.rs 同一手法, host/mod.rs 不在 scope);retry_request()/retry_plan() 就是「只重试这些」—— 调 core 的 retry_request()、只请求失败的那几个项目,present 重新扫目录。 install.rs 的重试入口没有记录时回一条失败,不退化成整单重装;成功则 forget + 清名单。 - 界面:内容库页插入按档分组的失败面板(两段文案、各自条目)+「只重试这 N 个」实心按钮; 判据只有一条且在模型里(stage==Failed && 名单非空),全成功时连同按钮一起不渲染。 - i18n:新增 6 条中英文案(不兼容 / 取不到 / 只重试这 N 个 / 标题 / 提示 / 关闭),金表同步。 守门(每条都做过变异验证,见报告): ① 把 retry_request() 换成根项目请求 ⇒ retrying_asks_for_the_failed_projects_only 红(roots 变 ["sodium"]); ② 两档合并 ⇒ the_retry_list_keeps_the_two_tiers_apart / the_two_tiers_are_two_groups_with_two_labels 红; ③ 恒渲染 ⇒ a_fully_successful_install_has_no_retry_surface_at_all 红。 数据包(datapack)本轮仍未做:ContentKind 没有这一项,界面上也没有半成品入口。
114 lines
5.7 KiB
Rust
114 lines
5.7 KiB
Rust
//! # 内容库页(M109 `CONTENTDL`):整合包 / Mod / 资源包 / 光影
|
||
//!
|
||
//! 用户令:「下载页只负责下载整合包和Mod/光影」。于是版本管理页(M107/M108 改名)只做版本,
|
||
//! **这一页**做内容:四条内容类型(页内一级侧栏)+ 关键词/来源/版本/加载器/排序筛选 +
|
||
//! 结果列表与详情面板 + **一键装入指定实例**。
|
||
//!
|
||
//! ## 入口(B1 裁定:不新增导航项)
|
||
//!
|
||
//! `Route::Content` 是新顶层路由,但 `Route::nav()` 返回 `NavSection::Download`:导航轨仍是
|
||
//! 五项,页头右上是「版本管理 | 内容库」一级切换([`parts::section_switcher`])。
|
||
//! 为什么不是第六个图标:轨宽 88 塞六项会挤,且用户要的是「同一入口下两块内容」。
|
||
//!
|
||
//! ## 分页 vs 无限滚动:**选分页**
|
||
//!
|
||
//! 三条理由,都是这一页的实际约束:
|
||
//!
|
||
//! 1. **快照只有一个槽**(`AppState::content.results`):无限滚动要求「滚到底就自动续」,而
|
||
//! 自动续在没有虚拟化列表的 iced 里会一次挂上几百个控件;
|
||
//! 2. **翻页要有明确出口**:站点限流 / 失败重试都发生在「下一批」上,用户得能看见自己正在
|
||
//! 等第 3 页,而不是画面莫名一卡;
|
||
//! 3. **可截图复现**:分页的每一次状态都是一份确定的快照,D30 的矩阵能钉住「还有更多 / 没有更多了」。
|
||
//!
|
||
//! 所以尾部是一个「加载更多」按钮 + 一句「没有更多了」([`parts::more`]),追加逻辑在
|
||
//! presenter 的 `AppState::apply_content`(同条件换页 ⇒ 追加;换条件 ⇒ 整批替换;迟到 ⇒ 丢掉)。
|
||
//!
|
||
//! ## 四态(都必须能画出来)
|
||
//!
|
||
//! | 态 | 判据 | 画什么 |
|
||
//! |---|---|---|
|
||
//! | 加载 | `pending` 或还没有结果 | 一句「正在加载」+ 说明 + 重试 |
|
||
//! | 空 | 拉到了、零命中 | 真 `EmptyState` + 重试 CTA |
|
||
//! | 失败 | `failed` 有原因 | `Error` 图标 + 原因原文 + 重试(实心) |
|
||
//! | 有数据 | 有命中项 | 计数头 + 列表 +(还有更多 ⇒ 加载更多 / 否则 ⇒ 没有更多了) |
|
||
//!
|
||
//! ## 不在这里做的事
|
||
//!
|
||
//! - **不发 HTTP**:检索与装入都在 presenter 的 `host/addon`(任务书第 4 条)。
|
||
//! - **不做乐观回显**:点「装入」不改本地状态,进度只由 `InstallProgress` 事件回流。
|
||
//! - **不自己画安装面板**:复用 `pages::download::install_panel`(同一个函数、同一套文案)。
|
||
//!
|
||
//! ## 偏离登记
|
||
//!
|
||
//! | # | 偏离 | 为什么 | 何时收口 |
|
||
//! |---|---|---|---|
|
||
//! | 1 | 四种内容类型借用现有字形(`Folder` / `Plus` / `Restore` / `Expand`,见 `parts::kind_icon`) | 设计令牌的 28 枚图标里没有这四类内容,且**不许**借用导航那五枚(同屏撞形)、不许用填充字形(门禁) | 图标集补「整合包 / Mod / 资源包 / 光影」时 |
|
||
//! | 2 | 本页自带一份栏几何(`content/layout.rs`) | M107 的 `pages/layout/rails.rs` 未并入本分支的 base | M107 合并后改为复用、删掉本文件 |
|
||
//! | 3 | 整合包的「装入」是禁用态 | core 没有远程整合包的安装管线 | 导入管线落地后接同一入口 |
|
||
//! | 5 | CurseForge 的「装入」同样是禁用态(`Blocker::SourceNotWired`) | 检索通了,**文件端点**还没接(CF 的文件列表要带 key) | 接上 CF 文件端点后删掉这条原因 |
|
||
//! | 4 | 安装进度只有阶段级(无字节条) | presenter 依赖表里没有 tokio/futures,拿不到 `ProgressEmitter` 的接收端 | presenter 补依赖后换成字节级 |
|
||
//!
|
||
//! ## 安装失败之后:两档分组 + 「只重试这些」(M191)
|
||
//!
|
||
//! 装入失败且**有可重试项**时,页底那块失败面板换成 [`parts::retry_panel`]:按
|
||
//! `InstallFailureKind` 分两段(不兼容 / 取不到,各有自己的词表文案),并给一个「只重试这 N 个」
|
||
//! 的实心按钮。按钮发 [`Message::RetryFailed`],app 层翻成 `AppCommand::InstallContent{retry: true}`,
|
||
//! host 拿上一轮的计划调 `DependencyPlan::retry_request()` ⇒ **只请求失败的那几个项目**。
|
||
//!
|
||
//! 判据只有一条且在模型里(`RetryPanel::of`):`stage == Failed && !retry.is_empty()`。
|
||
//! 全成功、没有失败项、还在跑,三种情况都得到 `None`——**不渲染**,不是画一个灰按钮。
|
||
|
||
mod demo;
|
||
mod layout;
|
||
mod message;
|
||
mod model;
|
||
// M113: import/export —— 导入/导出区的界面(新代码全在 `packui*`)
|
||
mod packui;
|
||
mod parts;
|
||
mod view;
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|
||
|
||
pub use message::Message;
|
||
pub use model::{
|
||
loader_name, Blocker, Content, ContentQueryView, Hit, PageState, RetryGroup, RetryPanel,
|
||
Status, Target,
|
||
};
|
||
// M113: import/export
|
||
pub use packui::{request_export, Pack, PackMessage, PackPageState};
|
||
|
||
use iced::Element;
|
||
|
||
use unlucky_i18n::Lang;
|
||
|
||
pub use unlucky_presenter::dto::ContentKind;
|
||
|
||
use crate::pages::layout::FrameMetrics;
|
||
use crate::pages::Message as PageMessage;
|
||
use crate::theme::Ui;
|
||
|
||
/// 整页入口(`pages/mod.rs` 的 `render` 调它;页面层不认识 presenter 的 DTO)。
|
||
pub fn view<'a>(
|
||
ui: &'a Ui,
|
||
metrics: FrameMetrics,
|
||
model: &'a Content,
|
||
lang: Lang,
|
||
window_height: f32,
|
||
) -> Element<'a, PageMessage> {
|
||
view::view(ui, metrics, model, lang, window_height)
|
||
}
|
||
|
||
/// 演示数据(`pages_demo` 出图与单测用;产品路径走 `Content::from_snapshot`)。
|
||
pub fn demo(kind: ContentKind, lang: Lang) -> Content {
|
||
demo::demo(kind, lang)
|
||
}
|
||
|
||
/// 页头那条一级切换(版本管理页也要用同一个函数:两页共用一个入口才不会各画一套)。
|
||
pub(crate) fn section_switcher<'a>(
|
||
ui: &'a Ui,
|
||
catalog: unlucky_i18n::Catalog,
|
||
) -> Element<'a, PageMessage> {
|
||
parts::section_switcher(ui, catalog, true)
|
||
}
|