Files
Unlucky-Launcher/crates/app-iced/src/pages/content/packui/model/progress.rs
T
AskaEth 5a52f38f35 feat(content): 安装失败的两档分组与「只重试这些」(M191)
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 没有这一项,界面上也没有半成品入口。
2026-11-09 05:06:04 +08:00

126 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 阶段与进度:**把导入进度映射成下载页那块面板的入参**(不另画一套进度 UI)。
//!
//! ## 任务书第 5 条怎么落
//!
//! 「沿用既有安装面板组件与状态机,禁止再写第二套进度 UI」——
//!
//! 1. 导入的 `ImportStageView` / 计数先映射成面板的入参形状([`panel_progress`] →
//! `InstallProgressView` → [`InstallView`]);
//! 2. 画法交给 `pages::download::install_panel`(同一个函数、同一套几何,进度条 / 计数 /
//! 取消按钮都归它);
//! 3. 任务书要的「解包 / overrides / 补文件」三档由**一行步骤说明**([`stage_line`])承载。
//!
//! **已知重叠(如实写,别再说成修好了)**:面板自带的阶段词是**安装**语义
//! (`Resolving version` / `Downloading files` / `Building the version` / `Registering instance`),
//! 而 `install_panel_model` 永远会填它(M109 合流 M105 后 `InstallView` 不再对外导出,
//! 没法从外面塞一个自定义标签或清空)。所以屏幕上同时出现「Step 2/5 · Unpacking overrides」
//! 与「Building the version」:**粗/细两层,不是两句矛盾的话**,但 `Extract`/`Commit` 都映射到
//! `Installing`,那一档的用词偏「安装」。
//!
//! 要彻底解决得让面板接受调用方给的阶段词(已报 finding:把安装面板提成共享组件)。
use unlucky_i18n::{Catalog, Key};
use unlucky_presenter::dto::{
ImportProgressView, ImportStageView, InstallProgressView, InstallStage,
};
use crate::pages::download::Download;
/// 抛进面板的进度行。
#[derive(Debug, Clone, PartialEq)]
pub struct Running {
/// 包名(面板标题那一行)
pub pack: String,
/// 细档位说明(`步骤 2/5 · 解包覆盖内容`)
pub stage_line: String,
/// 交给下载页那块面板的进度(**唯一**的进度形状来源;`None` = 面板无话可说)
pub panel: Option<Download>,
}
/// 细档位说明(`步骤 2/5 · 解包覆盖内容`)。
#[must_use]
pub fn stage_line(stage: ImportStageView, catalog: Catalog) -> String {
let total = ImportStageView::ALL.len();
let index = ImportStageView::ALL
.iter()
.position(|item| *item == stage)
.map_or(1, |at| at + 1);
catalog.render(
Key::PackStageLine,
&[
("index", &index.to_string()),
("total", &total.to_string()),
("stage", catalog.text(stage_key(stage))),
],
)
}
/// 阶段 → 文案 key。
#[must_use]
pub const fn stage_key(stage: ImportStageView) -> Key {
match stage {
ImportStageView::Probe => Key::PackStageProbe,
ImportStageView::Extract => Key::PackStageExtract,
ImportStageView::Fetch => Key::PackStageFetch,
ImportStageView::Commit => Key::PackStageCommit,
ImportStageView::Register => Key::PackStageRegister,
}
}
/// 导入阶段 → 安装面板的粗阶段(**唯一映射处**)。
///
/// `Extract` / `Commit` 都落到 `Installing`:对那块面板而言它们都是「在写盘」,区别由
/// [`stage_line`] 说给用户。
#[must_use]
pub const fn panel_stage(stage: ImportStageView) -> InstallStage {
match stage {
ImportStageView::Probe => InstallStage::Planning,
ImportStageView::Extract | ImportStageView::Commit => InstallStage::Installing,
ImportStageView::Fetch => InstallStage::Downloading,
ImportStageView::Register => InstallStage::Registering,
}
}
/// 导入进度 → 下载页面板的入参。
///
/// `version` / `instance` 都填包名:面板第一行显示的就是「这次在装什么」,
/// 而导入的对象是包,不是某个版本 id。
#[must_use]
pub fn panel_progress(progress: &ImportProgressView) -> InstallProgressView {
InstallProgressView {
version: progress.pack.clone(),
instance: progress.pack.clone(),
loader: String::new(),
stage: panel_stage(progress.stage),
files_done: u64::from(progress.done),
files_total: u64::from(progress.total),
bytes_done: 0,
bytes_total: 0,
speed_bps: 0,
eta_secs: 0,
// 整合包导入的失败不产生内容库分类(它有自己的报告视图)。
failure_kind: None,
// 导入没有「只重试这些」这条路径(重试整个导入由导入区自己管)。
retry: Vec::new(),
message: progress.current.clone().unwrap_or_default(),
}
}
/// 进度事件 → 面板要的视图模型。
///
/// 面板入参走 `pages::download::install_panel_model`(M109 合流 M105 后提供的**唯一**入口):
/// 它把 `InstallProgressView` 转成面板要的 `Download`,终态(完成/取消)返回 `None`。
/// 导入的五个阶段都不是终态,所以正常路径下一定是 `Some`;`None` 只做防御(面板没了就只留步骤行)。
///
/// 面板自带那个粗阶段词是**安装**语义(`Resolving version` / `Downloading files` /
/// `Building the version` / `Registering instance`),所以细档位由 [`stage_line`] 说,
/// 二者是「粗/细」关系而不是两套说法——[`panel_stage`] 只挑语义最近的那一档。
pub(super) fn running_of(progress: &ImportProgressView, catalog: Catalog) -> Running {
let panel = panel_progress(progress);
Running {
pack: progress.pack.clone(),
stage_line: stage_line(progress.stage, catalog),
panel: crate::pages::download::install_panel_model(&panel, catalog),
}
}