fix(download): M94 r2 —— 安装终态出口 + 分类真机过滤(复审 P1-1 / P2-1)

P1-1 安装一旦收尾就再也装不了第二个版本(终态面板没有出口)
- 页面侧:`Download::from_snapshot` 把 **Done / Cancelled** 当结束(`InstallView::finished`),
  面板让回列表 —— 装完一个版本可以直接选下一个继续装(图:r2-done / r2-done-next)。
  不清 `Failed`:失败态带着用户要的东西(停在哪个文件 + 去日志页)。
- 面板加真出口:失败态「Open logs」(实心)+「Dismiss」(文本 → `Message::CancelInstall`,
  host 侧对没在跑的任务是安全 no-op),修掉「失败后连重试都不可达」。
- 决策抽成纯函数 `install::panel_state()`(Hint / Ready / Install),`panel()` 只是它的画法,
  于是「能不能再装」可以被单测钉死,不用开图形环境。
- 两条新测试**不走 MockBackend**(它从不发 `InstallProgress`,终态永远跑不到 ⇒ 假绿):
  直接构造 `AppState` + `apply(CoreEvent::InstallProgress(..))`。

P2-1 分类 chip 在真 host 上是空控件
- `from_snapshot(state, category, catalog)` 收分类并按 `Category::matches` **本地过滤**
  (真 host 的 `ListVersions(_)` 丢掉分类参数、回全量清单);
- 分类不再从列表反推(`Category::of` 只留给演示首帧),`app/view.rs::model` 收分类、
  `refresh_view` 把上一份视图模型的分类带回去 ⇒ 事件回来 chip 不会跳回「全部」;
- `update.rs` 的 `SetCategory` 写分类后**立刻重建**(chip 与列表同一个谓词),再派真指令。

p3(顺带)
- 失败态与占位态共存:列表空/加载中时若安装还在跑,面板照样出现在底部(r2-installing-empty);
- 修 `install.rs` 的坏 rustdoc 链接;`install.rs` / `model.rs` 的测试模块外置到子文件守住 400 行。

测试文件:`pages/download/{model,install}/tests.rs`(新),`pages/download/tests.rs` 里的
`from_snapshot` 调用补分类参数。

实证(回退法):删掉 `.filter(|view| !view.finished())` → 两条终态测试红并点名
「Done 之后不许再占着安装面板」;删掉 `.filter(|category.matches(..))` → 两条分类测试红
(「只剩正式版」left=3 right=1)。放回修复 → 全绿。

门禁:fmt --check 干净、clippy -D warnings 干净、app-iced 280 + 集成 6 + i18n 23 全绿(-j 4)。
This commit is contained in:
tower authored and AskaEth committed 2026-11-02 21:36:28 +08:00
1 parent 9d6ee70301
commit 6453c76750
12 files changed
+432 -74

No files matched your search

+78 -15
View File
@@ -18,13 +18,25 @@
//! --screen=download --screenshot=out.png --screenshot-theme=light --software-renderer
//! ```
//!
//! 取值:`ready`(缺省)/ `loading` / `empty` / `selected` / `installing` / `installing-late` / `failed`。
//! 取值:`ready`(缺省)/ `loading` / `empty` / `selected` / `category-release` / `installing` /
//! `installing-late` / `installing-empty` / `failed` / `done` / `done-next`。
//!
//! `category-release` / `done` / `done-next` 都**不手搓结果**,而是拿一份 `AppState` 跑
//! [`Download::from_snapshot`]:图证明的是产品路径的行为(分类本地过滤、装完后面板让回列表),
//! 不是演示的特权。
//!
//! `done` / `done-next` 是「**刚装完**」的两个采样点:它们不手搓模型,而是拿一份
//! `AppState`(版本列表 + `InstallStage::Done`)跑 [`Download::from_snapshot`],所以图上看到的是
//! 产品路径的真行为(复审 p2:`Done` 之后面板必须让回列表,否则第二个版本装不了)。
//! **只有 harness 与演示走这里**:产品路径由 `app/view.rs` 从 presenter 快照建模型。
use unlucky_presenter::dto::{
AncientKind, InstallProgressView, InstallStage, VersionKind, VersionSummary,
};
use unlucky_presenter::dto::{AppEvent, CoreEvent};
use unlucky_presenter::state::AppState;
use unlucky_i18n::Catalog;
use super::model::{Category, Download, InstallView, VersionRow};
@@ -60,6 +72,17 @@ pub fn demo(catalog: Catalog) -> Download {
selected: Some("1.21.9".to_string()),
..ready(catalog)
},
// 装完之后:走**产品那条真转换**(见 `finished`),图里看到的就是页面真会画的东西
Some("done") => finished(catalog, None),
// 装完之后再点下一个版本:面板必须重新给出实心「安装」
Some("done-next") => finished(catalog, Some("1.21.9-pre3")),
// 安装进行中、但列表被过滤空:面板要跟着占位态一起在(复审 p3 的边角)
// 分类筛选(本页本地过滤:真 host 丢弃 ListVersions 的分类参数)——混合清单里只留正式版
Some("category-release") => category(catalog, Category::Release),
Some("installing-empty") => Download {
install: Some(install(InstallStage::Downloading, 0.42, catalog)),
..Download::loading()
},
_ => ready(catalog),
}
}
@@ -69,6 +92,24 @@ pub fn demo(catalog: Catalog) -> Download {
/// 不复用 `mock_data::mock_versions()`:演示数据是**出图的基线**,得比 mock 稳定
/// (mock 随 P3 的假数据一起漂)。
fn ready(catalog: Catalog) -> Download {
let versions = demo_versions();
let rows: Vec<VersionRow> = versions
.iter()
.map(|version| VersionRow::of(version, catalog))
.collect();
Download {
// 混合分类 → 没有单个 chip 该亮(这正是 `Category::of` 的反推结果)
category: Category::of(&versions),
rows,
selected: None,
install: None,
loaded: true,
}
}
/// 演示用的版本表(`ready` 与 `finished` 共用;与 presenter 的 `mock_versions` 同形)。
fn demo_versions() -> Vec<VersionSummary> {
fn summary(id: &str, kind: VersionKind, released: &str, installed: bool) -> VersionSummary {
VersionSummary {
id: id.to_string(),
@@ -78,7 +119,7 @@ fn ready(catalog: Catalog) -> Download {
}
}
let versions = vec![
vec![
summary("1.21.9", VersionKind::Release, "2026-09-20", false),
summary("1.21.8", VersionKind::Release, "2026-07-11", true),
summary("1.20.4", VersionKind::Release, "2023-12-07", true),
@@ -95,21 +136,43 @@ fn ready(catalog: Catalog) -> Download {
"2010-08-11",
false,
),
];
]
}
let rows: Vec<VersionRow> = versions
.iter()
.map(|version| VersionRow::of(version, catalog))
.collect();
/// 「刚装完一个版本」的演示:**走产品那条真转换**(`AppState` → `Download::from_snapshot`)。
///
/// 为什么不用手搓模型:本轮复审的 bug 正是「`Done` 之后 `install` 从不清」——手搓一个
/// `install: None` 的模型会让图**假绿**(图上好看,真路径照样卡死)。这里真的走一遍快照转换,
/// 于是图能证明「装完之后面板让回列表」是产品路径的行为,不是演示的特权。
///
/// `select_next`:演「用户接着点了下一个版本」,面板应当重新给出实心「安装」。
fn finished(catalog: Catalog, select_next: Option<&str>) -> Download {
let mut state = AppState::new();
state.apply(AppEvent::Core(CoreEvent::VersionListChanged(
demo_versions(),
)));
state.apply(AppEvent::Core(CoreEvent::InstallProgress(Some(
InstallProgressView {
version: "1.21.8".to_string(),
stage: InstallStage::Done,
files_done: 512,
files_total: 512,
..InstallProgressView::new()
},
))));
Download {
// 混合分类 → 没有单个 chip 该亮(这正是 `Category::of` 的反推结果)
category: Category::of(&versions),
rows,
selected: None,
install: None,
loaded: true,
}
let mut model = Download::from_snapshot(&state, Category::All, catalog);
model.selected = select_next.map(str::to_string);
model
}
/// 「用户点了某个分类」的演示:走真转换(`from_snapshot` 的分类过滤),不手搓过滤结果。
fn category(catalog: Catalog, wanted: Category) -> Download {
let mut state = AppState::new();
state.apply(AppEvent::Core(CoreEvent::VersionListChanged(
demo_versions(),
)));
Download::from_snapshot(&state, wanted, catalog)
}
/// 一次安装的演示快照(文件计数 / 字节 / 速度 / 剩余时间都给真值,进度条画得出来)。