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:
1 parent
d690d09b84
commit
30b1ff6f48
39 files changed
+2550
-518
No files matched your search
@@ -105,6 +105,7 @@ pub(super) fn model(
|
||||
width: f32,
|
||||
forced: Option<crate::cli::ForcedState>,
|
||||
category: pages::Category,
|
||||
draft: &pages::InstallDraft,
|
||||
) -> pages::Model {
|
||||
let route = state.route().clone();
|
||||
let screen = pages::Screen::of(&route).unwrap_or(pages::Screen::Instances);
|
||||
@@ -137,7 +138,12 @@ pub(super) fn model(
|
||||
window_width: width,
|
||||
active: route.nav(),
|
||||
instances: pages::Instances { rows, detail },
|
||||
download: pages::Download::from_snapshot(state, category, unlucky_i18n::Lang::En.catalog()),
|
||||
download: pages::Download::from_snapshot(
|
||||
state,
|
||||
category,
|
||||
draft,
|
||||
unlucky_i18n::Lang::En.catalog(),
|
||||
),
|
||||
logs: logs_model(state).with_status(lang),
|
||||
lang,
|
||||
forced: forced.map(|state| match state {
|
||||
@@ -274,10 +280,21 @@ mod tests {
|
||||
/// 与「取证密闭」正好相反,而且图看起来完全正常);② 加了变体却映射到别的态这种复制粘贴错。
|
||||
/// 抓不住:`shell::overridden`(标题栏 / 导航轨按钮走的**另一处** match,断言在 `shell/tests.rs`),
|
||||
/// 也抓不住不消费 `model.forced` 的裸 `iced::button`(设置页「外观」区就是)。
|
||||
/// 测试用的薄包装:默认宽度 / 默认分类 / 空草稿。
|
||||
fn at(state: &AppState, forced: Option<crate::cli::ForcedState>) -> pages::Model {
|
||||
model(
|
||||
state,
|
||||
1180.0,
|
||||
forced,
|
||||
pages::Category::All,
|
||||
&pages::InstallDraft::default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_forced_state_maps_to_the_expected_interaction() {
|
||||
let state = AppState::new();
|
||||
let forced = |forced| model(&state, 1180.0, Some(forced), pages::Category::All).forced;
|
||||
let forced = |forced| at(&state, Some(forced)).forced;
|
||||
|
||||
assert_eq!(
|
||||
forced(crate::cli::ForcedState::Rest),
|
||||
@@ -292,10 +309,7 @@ mod tests {
|
||||
forced(crate::cli::ForcedState::Pressed),
|
||||
Some(Interaction::Pressed)
|
||||
);
|
||||
assert_eq!(
|
||||
model(&state, 1180.0, None, pages::Category::All).forced,
|
||||
None
|
||||
);
|
||||
assert_eq!(at(&state, None).forced, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -306,7 +320,7 @@ mod tests {
|
||||
));
|
||||
state.navigate(Route::Detail("survival".to_string()));
|
||||
|
||||
let model = model(&state, 1180.0, None, pages::Category::All);
|
||||
let model = at(&state, None);
|
||||
assert_eq!(model.screen, pages::Screen::Instances);
|
||||
assert_eq!(model.active, Some(pages::Screen::Instances.nav()));
|
||||
assert_eq!(model.window_width, 1180.0);
|
||||
@@ -337,7 +351,7 @@ mod tests {
|
||||
));
|
||||
state.navigate(Route::Detail("survival".to_string()));
|
||||
|
||||
let model = model(&state, 1180.0, None, pages::Category::All);
|
||||
let model = at(&state, None);
|
||||
assert_eq!(
|
||||
model.instances.rows[0].status.as_deref(),
|
||||
Some("Downloading · 50%")
|
||||
@@ -375,7 +389,7 @@ mod tests {
|
||||
fn the_agent_route_has_no_page() {
|
||||
let mut state = AppState::new();
|
||||
state.navigate(Route::Agent);
|
||||
let model = model(&state, 1180.0, None, pages::Category::All);
|
||||
let model = at(&state, None);
|
||||
// Agent 没有页面:模型退回实例页的空壳,view 会走外壳回退(`None` 分支)
|
||||
assert_eq!(pages::Screen::of(state.route()), None);
|
||||
assert!(model.instances.detail.is_none());
|
||||
|
||||
Reference in new issue
Block a user