用户实跑反馈两条,本轮各自定位到根因并修掉:
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 + 金表同步)。
155 lines
7.7 KiB
Rust
155 lines
7.7 KiB
Rust
//! 下载页在 app 层的那几支(M94;M105 补上安装选项)。
|
|
//!
|
|
//! 为什么单独一个文件:M93(外观三件套接线)合入后 `update.rs` 已经贴到 400 行,下载页再往里塞
|
|
//! 几个 arm 就顶破硬线;拆出来是既不动 M93 的代码、又能守住 ≤400 的办法(`app/update/**` 属本
|
|
//! mission scope)。
|
|
//!
|
|
//! ## 各支各自的纪律(M105 后的口径)
|
|
//!
|
|
//! | 消息 | 落点 | 为什么 |
|
|
//! |---|---|---|
|
|
//! | [`pages::Message::SelectVersion`] | 只改**草稿** | 选中是页面内状态,不是业务命令;顺手清掉加载器版本(换了原版版本,旧版本号几乎必然不适用) |
|
|
//! | [`pages::Message::SetLoader`] | 改草稿 + 必要时派 `ListLoaderVersions` | 加载器版本列表来自官方元数据,页面不能自己编 |
|
|
//! | [`pages::Message::SetLoaderVersion`] / `SetInstanceName` | 只改草稿 | 纯输入 |
|
|
//! | [`pages::Message::InstallVersion`] | `AppCommand::CreateInstance(草稿)` | **确认才开工**(带加载器 / 加载器版本 / 实例名) |
|
|
//! | [`pages::Message::CancelInstall`] | `AppCommand::CancelInstall` | 见 `host/dispatch.rs`:真停 + 作废旧代次 |
|
|
//! | [`pages::Message::SetCategory`] | 写分类 → 重建 → `AppCommand::ListVersions` | 见下 |
|
|
//!
|
|
//! [「刷新下载源」](pages::Action::RefreshSources) 也在这里:它不是占位动作,具体分类 → 重拉该分类、
|
|
//! 「全部」→ 让 host 重扫一遍(`RefreshAll`)。
|
|
//!
|
|
//! ## 分类为什么既写视图模型、又派指令
|
|
//!
|
|
//! 真 host 的 `AppCommand::ListVersions(_)` 会丢掉分类参数、回全量清单(复审 P2-1),所以**过滤在
|
|
//! 本页做**:写进视图模型 → 立刻 `refresh_view()`(它会把 category 带回去、按同一谓词过滤行)→
|
|
//! 再派指令。这不是「乐观回显」:写的是**过滤条件本身**,chip 与列表用同一个
|
|
//! [`Category::matches`](crate::pages::Category),于是不可能出现「chip 说正式版、列表是全量」的两份真相。
|
|
//!
|
|
//! ## 草稿为什么住在这里而不是页面模型里
|
|
//!
|
|
//! `refresh_view()` 每次事件都把 `self.view` 整棵重建(连安装进度都算),页面存不住「用户正在填、
|
|
//! 还没确认」的东西。于是草稿放 `App::download_draft`,与分类筛选同一个做法:
|
|
//! 改草稿 → `view_dirty = true` → `refresh_view()`。
|
|
|
|
use unlucky_presenter::dto::CreateRequest;
|
|
|
|
use super::*;
|
|
|
|
impl App {
|
|
/// 下载页的消息。**处理了返回 `true`**(`on_page` 里就不用再写一遍这些 arm)。
|
|
pub(super) fn on_download(&mut self, message: &pages::Message) -> bool {
|
|
match message {
|
|
pages::Message::SelectVersion(id) => {
|
|
if self.download_draft.selected.as_deref() != Some(id.as_str()) {
|
|
// 换了原版版本:加载器版本是**按 (加载器, 原版版本)** 取的(见
|
|
// `CoreEvent::LoaderVersions` 的缓存键),旧的版本号留着只会误导。
|
|
self.download_draft.loader_version.clear();
|
|
}
|
|
self.download_draft.selected = Some(id.clone());
|
|
self.mark_download_dirty();
|
|
// 选了加载器版本之后换原版版本,列表要跟着换——已经查过的直接从快照读,不重复派。
|
|
self.ensure_loader_versions();
|
|
}
|
|
pages::Message::SetLoader(loader) => {
|
|
self.download_draft.loader = *loader;
|
|
self.download_draft.loader_version.clear();
|
|
self.mark_download_dirty();
|
|
self.ensure_loader_versions();
|
|
}
|
|
pages::Message::SetLoaderVersion(version) => {
|
|
self.download_draft.loader_version = version.clone();
|
|
self.mark_download_dirty();
|
|
}
|
|
pages::Message::SetInstanceName(name) => {
|
|
self.download_draft.instance_name = name.clone();
|
|
self.mark_download_dirty();
|
|
}
|
|
pages::Message::InstallVersion(id) => {
|
|
// 防守:按钮带的是**页面上那一行**的 id,草稿里的选中项理论上与它一致;
|
|
// 万一不一致(例如列表刚换过、用户手快),不能让 A 版本的加载器版本落到 B 版本上。
|
|
if self.download_draft.selected.as_deref() != Some(id.as_str()) {
|
|
self.download_draft.selected = Some(id.clone());
|
|
self.download_draft.loader_version.clear();
|
|
}
|
|
let request = self.install_request(id);
|
|
self.dispatch(AppCommand::CreateInstance(request));
|
|
}
|
|
pages::Message::CancelInstall => self.dispatch(AppCommand::CancelInstall),
|
|
pages::Message::SetCategory(category) => {
|
|
self.view.download.category = *category;
|
|
self.view_dirty = true;
|
|
self.refresh_view();
|
|
if let Some(kind) = category.command() {
|
|
self.dispatch(AppCommand::ListVersions(kind));
|
|
}
|
|
}
|
|
pages::Message::Action(pages::Action::RefreshSources) => {
|
|
match self.view.download.category.command() {
|
|
Some(kind) => self.dispatch(AppCommand::ListVersions(kind)),
|
|
None => self.dispatch(AppCommand::RefreshAll),
|
|
}
|
|
}
|
|
_ => return false,
|
|
}
|
|
true
|
|
}
|
|
|
|
/// 改草稿之后统一走这一条:置脏 + 立刻重建(不等下一拍,点了就有反应)。
|
|
fn mark_download_dirty(&mut self) {
|
|
self.view_dirty = true;
|
|
self.refresh_view();
|
|
}
|
|
|
|
/// 确认安装时的 `CreateRequest`(草稿 → 指令载荷)。
|
|
///
|
|
/// 实例名留空就让 core 用版本号(`CreateRequest::instance_name` 的既有口径),
|
|
/// 所以这里**不**自己拼一个默认名——两处各拼一份迟早会漂。
|
|
fn install_request(&self, id: &str) -> CreateRequest {
|
|
let draft = &self.download_draft;
|
|
CreateRequest {
|
|
name: draft.instance_name.trim().to_string(),
|
|
version: id.to_string(),
|
|
loader: draft.loader.kind(),
|
|
loader_version: draft.loader_version.trim().to_string(),
|
|
}
|
|
}
|
|
|
|
/// 当前草稿需要加载器版本列表、而快照里还没有时,派一次查询。
|
|
///
|
|
/// 判据是 `state.loader_versions_for(...)` 的 `None`(= 没查过):查过的(成功或失败)都不再重复派,
|
|
/// 失败要重试走 `RefreshSources` 那条路,页面上的红色原因行不会因为一次点击就消失。
|
|
fn ensure_loader_versions(&mut self) {
|
|
let loader = self.download_draft.loader;
|
|
if loader == pages::Loader::Vanilla {
|
|
return;
|
|
}
|
|
let Some(version) = self.download_draft.selected.clone() else {
|
|
return;
|
|
};
|
|
// 静态就不支持的组合不联网(否则只会换来一条「取不到」的假原因;真正的原因是
|
|
// 「这个加载器没有这个版本的构建」,页面已经用静态规则说清了)。
|
|
let kind = self
|
|
.state
|
|
.versions
|
|
.iter()
|
|
.find(|summary| summary.id == version)
|
|
.map(|summary| summary.kind);
|
|
if let Some(kind) = kind {
|
|
if loader.supports(&version, kind).is_err() {
|
|
return;
|
|
}
|
|
}
|
|
if self
|
|
.state
|
|
.loader_versions_for(loader.kind(), &version)
|
|
.is_some()
|
|
{
|
|
return;
|
|
}
|
|
self.dispatch(AppCommand::ListLoaderVersions {
|
|
kind: loader.kind(),
|
|
mc_version: version,
|
|
});
|
|
}
|
|
}
|