//! 下载页在 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, }); } }