feat(dlflow): M105 安装选项区(四家加载器 + 官方版本列表 + 实例名)与取消安装真停

用户实跑反馈两条,本轮各自定位到根因并修掉:

1. 「下载页没有前置加载器选项」= 功能未做:`Message::InstallVersion` 直接派
   `AppCommand::InstallVersion`(= 只装原版),core 早在 M32/M35/M45 就有四家加载器的
   安装管线。现在选中版本之后面板先给「安装选项」区(原版 / Forge / NeoForge / Fabric /
   Quilt + 该加载器的官方版本列表 + 实例名),点「安装」才派
   `AppCommand::CreateInstance`。
   - 加载器版本列表复用 core 的 `LoaderSource::loader_versions`,经新增的
     `AppCommand::ListLoaderVersions` / `CoreEvent::LoaderVersions` 回流;
     缓存键是 `(加载器, 原版版本)`,切版本不会串列表;取不到给具名原因并**禁掉确认**。
   - 不兼容组合用静态规则在**选中那一刻**说清(NeoForge 需 1.20.1+、Fabric/Quilt 需
     1.14+、远古版四家都不支持),不让用户「选完才失败」,也不为不可能的组合作无谓联网。
   - 选项草稿(选中 + 加载器 + 加载器版本 + 实例名)住在 `App`,`refresh_view` 带进页面
     模型(与分类筛选同一做法;页面自己存不住跨事件的东西)。

2. 「点取消没反应」= 真 bug,两跳各断一半:
   - 第一跳(UI 侧):取消确实生效了,但**还在跑的后台线程继续发进度**,`InstallProgress(None)`
     刚把面板收回,下一条「下载中」立刻又点亮它 —— 用户看到的就是「点了没反应」。
     修法:`CoreHost` 加安装代次 `install_epoch`(开工与取消都 +1),`HostTask::Install` /
     `InstallDone` 带代次回来,代次不符一律丢。
   - 第二跳(core 侧):`execute` 没有任何取消入参,取消只停在段边界,传输根本没停。
     新增 `execute_with_cancel(…, cancel: Option<&AtomicBool>)`(既有 `execute` 原样包装,
     CLI 一字未改):取消后不再起新文件,在飞的那个与「取消 future」`select` 赛跑,取消赢就
     drop 掉下载 future(响应流关闭、`.ullpart` 由 `PartFile::Drop` 清掉),并且不写版本清单,
     返回新的 `InstallErrorKind::Cancelled`。
   - 另外:取消后**绝不登记实例**(文件段跑完但用户已取消的那一路也拦住);取消文案把
     「哪些留着、哪些一定没发生」说清(D21)。

实测(真网络装置 `host/cancel_tests.rs`,`#[ignore]` 守着,跑法见文件头):
取消前 files_done=2 / `.ullpart` 613 字节;取消后 20 s 进度事件 **0 条**、`.ullpart` **0 字节**
(-613)、版本清单未落盘、实例目录不存在。取消前是 +201466 字节(同一个装置量的)。

文案全部进 i18n(中英,9 条新 key + 金表同步)。
This commit is contained in:
AskaEth committed 2026-11-04 22:12:22 +08:00
1 parent d690d09b84
commit 30b1ff6f48
39 files changed
+2550 -518

No files matched your search

+42 -3
View File
@@ -19,7 +19,8 @@
//! ```
//!
//! 取值:`ready`(缺省)/ `loading` / `empty` / `selected` / `category-release` / `installing` /
//! `installing-late` / `installing-empty` / `failed` / `done` / `done-next`。
//! `installing-late` / `installing-empty` / `failed` / `done` / `done-next` /
//! **`options` / `options-fabric` / `options-incompat`**(M105 的安装选项区三态)。
//!
//! `category-release` / `done` / `done-next` 都**不手搓结果**,而是拿一份 `AppState` 跑
//! [`Download::from_snapshot`]:图证明的是产品路径的行为(分类本地过滤、装完后面板让回列表),
@@ -40,6 +41,7 @@ use unlucky_presenter::state::AppState;
use unlucky_i18n::Catalog;
use super::model::{Category, Download, InstallView, VersionRow};
use super::options::{InstallDraft, Loader};
/// 取证开关的名字([`demo`] 读它)。
pub const DEMO_STATE_ENV: &str = "ULL_DL_DEMO";
@@ -63,6 +65,17 @@ pub fn demo(catalog: Catalog) -> Download {
selected: Some("1.21.9".to_string()),
..ready(catalog)
},
// M105:安装选项区三态(未选加载器 / 已选 Fabric / 不兼容组合)。
// 三条都走**产品那条真转换**(`from_snapshot` + 草稿),图里看到的是页面真会画的东西。
Some("options") => options(catalog, "1.21.9", Loader::Vanilla, None),
Some("options-fabric") => options(
catalog,
"1.21.9",
Loader::Fabric,
Some(vec!["0.16.5", "0.16.4", "0.15.11"]),
),
// NeoForge 最早只到 1.20.1,远古版(列表里的 `rd-132211`)根本没有构建 ⇒ 选中即报原因
Some("options-incompat") => options(catalog, "rd-132211", Loader::NeoForge, None),
Some("selected") => Download {
selected: Some("1.21.9".to_string()),
..ready(catalog)
@@ -103,11 +116,36 @@ fn ready(catalog: Catalog) -> Download {
category: Category::of(&versions),
rows,
selected: None,
options: None,
install: None,
loaded: true,
}
}
/// 「用户选了版本、正在填安装选项」的演示(M105)。
///
/// 走真转换:`AppState`(版本列表 + 可选的加载器版本缓存)+ 草稿 → [`Download::from_snapshot`]。
/// 于是图上「默认取最新加载器版本」「不兼容给具名原因」「确认键禁用」都是产品行为,不是演示特权。
fn options(catalog: Catalog, version: &str, loader: Loader, cached: Option<Vec<&str>>) -> Download {
let mut state = AppState::new();
state.apply(AppEvent::Core(CoreEvent::VersionListChanged(
demo_versions(),
)));
if let Some(items) = cached {
state.apply(AppEvent::Core(CoreEvent::LoaderVersions {
kind: loader.kind(),
mc_version: version.to_string(),
result: Ok(items.into_iter().map(str::to_string).collect()),
}));
}
let draft = InstallDraft {
selected: Some(version.to_string()),
loader,
..InstallDraft::default()
};
Download::from_snapshot(&state, Category::All, &draft, catalog)
}
/// 演示用的版本表(`ready` 与 `finished` 共用;与 presenter 的 `mock_versions` 同形)。
fn demo_versions() -> Vec<VersionSummary> {
fn summary(id: &str, kind: VersionKind, released: &str, installed: bool) -> VersionSummary {
@@ -161,7 +199,8 @@ fn finished(catalog: Catalog, select_next: Option<&str>) -> Download {
},
))));
let mut model = Download::from_snapshot(&state, Category::All, catalog);
let mut model =
Download::from_snapshot(&state, Category::All, &InstallDraft::default(), catalog);
model.selected = select_next.map(str::to_string);
model
}
@@ -172,7 +211,7 @@ fn category(catalog: Catalog, wanted: Category) -> Download {
state.apply(AppEvent::Core(CoreEvent::VersionListChanged(
demo_versions(),
)));
Download::from_snapshot(&state, wanted, catalog)
Download::from_snapshot(&state, wanted, &InstallDraft::default(), catalog)
}
/// 一次安装的演示快照(文件计数 / 字节 / 速度 / 剩余时间都给真值,进度条画得出来)。