Files
Unlucky-Launcher/crates/app-iced/src/pages/message.rs
T
AskaEth 1c8f6aa1b3 feat(m159): 自更新 GUI 客户端接线 + CLI update check/apply 同一内核
D50 的「检查 → 提示 → 可拒绝 → 下载校验 → 下一次启动生效」整环接线,GUI 与
CLI 共用 core 的一条内核(`update::Job`),并顺带修掉 M157 最后一轮复审的三处 P3。

内核(core::update)
- 新增 job.rs:`Job { source, target, staging, dest }` + `check` / `apply`(复查 →
  取包 → 长度/SHA256 双检 → 原子替换),`default_source`(GitHub,`ULL_UPDATE_DIR`
  可切本地目录夹具)、`process_dest`、`staging_dir`。
- `mod.rs` 导出 job 一族;`source.rs` 为 `Box<dyn UpdateSource>` 补 trait 转发
  (壳层要运行期选源,`UpdateChecker` 是泛型的)。

presenter(宿主通道)
- `dto/update.rs`:`UpdateView` / `UpdateOutcome`(Idle/Checking/UpToDate/Available/
  MissingArtifact/Failed 六态,**失败与最新不折叠**)/ `UpdateBanner` 优先级判据。
- `host/update.rs`:Factory(生产 GitHub / 单测本地目录)+ core 结论 → DTO 的唯一映射
  + 更新域偏好(`cache/update/prefs.json`)。
- `host/update_state.rs`:Runtime(会话状态机:begin_check/adopt/begin_apply/dismiss/
  set_auto_check)+ `CoreHost` 派发与后台线程。
- `AppCommand::{CheckUpdate, ApplyUpdate, DismissUpdate, SetUpdateAutoCheck}`、
  `CoreEvent::Update`、`AppState.update`;`refresh_all` 按开关派发静默检查。

app-iced
- 设置页新增第 5 组「更新」(当前版本 / 渠道 / 自动检查开关 / 立即检查 / 上次检查与
  结果 / 下载并安装 / 就位后立即重启),走 `settings::view` 的 `update` 形参 + 按下标
  分发(与 M106 语言行同一取舍)。
- 顶部提示条(每页可见):有新版本(版本号 + 体积 + 说明摘要,可安装/可忽略)、下载中、
  已就位(立即重启);静默检查失败**不**弹条(原因进设置页)。
- `pages::Message::Update(UpdateAction)` → `app/update/update_action.rs`;「立即重启」
  拉同参数的新进程后退出(截图通道不重启)。

CLI
- `ull update check` / `ull update apply [--dest <path>]`:复用同一 `Job`;四种结论分开
  报(`--json` 的 `status`),失败退出码 1(`update_check_failed` / `update_apply_failed`)。

i18n
- 新增 `catalog/update.rs`(30 条,链尾追加)+ 金表对应 30 行。

M157 复审 P3 三件
- `app/update/instances.rs` 模块文档改成「清脏的唯一时机是回程快照等于表单」。
- `pages/instances/demo.rs` 的 `demo_form` 注释改成现状(两个参数框早已是单行输入)。
- `pages/instances/launch_args.rs`:门限不满足时只灰 `opengl`/`vulkan`,`default` 仍可点
  (它是「不下发」的安全档);判据抽成 `backend_chip_enabled` 并补 3 条用例。浅色 + 灰化
  两张图:`C:/Temp/m159-shots/instances-launch-args-{light,gated}.png`。

顺带:harness 支持 `ULL_SETTINGS_GROUP=<下标>`(拍第 5 组的验收图需要它)。

验证:cargo fmt --check 绿;cargo clippy --workspace --all-targets -D warnings 绿;
cargo test --workspace --all-targets 51 个测试二进制全绿(唯一例外:core::addon::pack
一族要造 4 GiB 稀疏文件,本机磁盘 99% 满 ⇒ StorageFull,与本次改动无关);CLI 用本地
目录夹具跑通「发现 9.9.9 → 下载 → 校验 → 替换运行中的 exe → 备份留旧文件」,并验证
摘要不符时拒绝应用、目标文件零改动、暂存目录零残留。
2026-11-07 12:09:42 +08:00

165 lines
8.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 页面消息与动作表(M101 r2 从 `pages/mod.rs` **原样搬出**)。
//!
//! 为什么拆:合并 M100/M101 之后 `pages/mod.rs` 是 397 行,只剩 3 行余量,下一条线往枚举里加
//! 一个变体(含文档)就顶破 400 硬线。拆法与 M99 的 `pages/nav.rs` 同一种范式:**纯搬移**、
//! 一行不改、路径靠 `pages/mod.rs` 的 `pub use` 再导出保持不变(`pages::Message` / `pages::Action`)。
//!
//! `pages::Message` 是页面层的自有消息类型(**不** import `crate::app::Message`,组件契约 §2 的
//! 同一条纪律);app 层只做一次 `.map`。
use unlucky_i18n::LangChoice;
use unlucky_presenter::router::Route;
use super::appearance;
use super::content;
use super::download::{Category, Loader};
use super::settings;
/// 页面消息。
///
/// **故意不用 `crate::app::Message`**(组件库契约 §2 的同一条理由):页面不绑 app 的状态层,
/// 接线时由 app 层把这里的三类意图翻成 presenter 的 `AppCommand` / `Route`。
#[derive(Debug, Clone)]
pub enum Message {
/// 骨架期动作(三页的占位按钮 + 实例页上还没接后端的两个页头动作)。
Action(Action),
/// 路由跳转:导航轨、列表行、窄窗「返回」都走这一条(栈语义归 `presenter::router`)。
Navigate(Route),
/// 启动某实例(app 层翻成 `AppCommand::LaunchInstance`)。
Launch(String),
/// 停止某实例(app 层翻成 `AppCommand::StopInstance`)。
Stop(String),
/// 外观项改动(设置页「外观」分组 → `App` 的状态与落盘)。
///
/// 载荷是**用户请求值**(未钳位):钳位与 CPU 档降级在 `appearance::alpha` 里做,
/// 页面只表达意图(与组件库「不持有业务状态」同一条纪律)。
Appearance(appearance::Change),
/// 选中下载页的一个版本(M94):**页面内选中**,app 层只改视图模型,不 dispatch。
SelectVersion(String),
/// **确认安装**选中的版本(用当前安装选项)。
///
/// M105 起语义变了:以前是「立刻装原版」(`AppCommand::InstallVersion`),现在是
/// 「用选项区里选好的加载器 / 加载器版本 / 实例名开工」(`AppCommand::CreateInstance`)。
/// 载荷仍是版本 id,app 层拿它去草稿里取其余三项。
InstallVersion(String),
/// 改安装选项的加载器(M105;app 层改草稿,必要时派 `AppCommand::ListLoaderVersions`)。
SetLoader(Loader),
/// 改加载器版本(M105;空串 = 用官方最新)。
SetLoaderVersion(String),
/// 改实例名(M105;空串 = 用版本号)。
SetInstanceName(String),
/// 取消正在跑的安装(M94;`AppCommand::CancelInstall`)。
CancelInstall,
/// 切换下载页分类(M94;app 层翻成 `AppCommand::ListVersions`,`All` 只画现有快照)。
SetCategory(Category),
/// 搜索版本号(**M107**):查询词写进 `AppState.download_query`,页面走同一条重建路径过滤。
///
/// 为什么不是「页面自己存一份查询」:下载页是**无状态视图**(每帧由快照重建),页面持有的
/// 字段活不过下一次事件;查询词只有落在 presenter 的快照里才能跨帧存活,而
/// `Download::from_snapshot` 本来就读 `download_query` —— 复用这条路,过滤只有一份真相。
SetVersionQuery(String),
/// 清空搜索框(**M107**)。与 [`Self::SetVersionQuery`] 同一支的 `""` 特例,
/// 单独一个变体是为了让「清空」这个意图在日志与测试里一眼可辨。
ClearVersionQuery,
/// 选中第二级侧栏的一个主版本桶(**M107**;`None` = 全部版本)。
///
/// **纯视图态**(与 [`Self::SelectVersion`] 同一类):不派业务命令,app 层只改视图模型。
SelectMajor(Option<String>),
/// 折叠 / 展开第一级(版本类型)侧栏(**M107**)。纯视图态。
ToggleTypeRail,
/// 把现成文本塞进剪贴板(**M95**:诊断卡片的一行摘要,文本由页面造,app 只搬)。
CopySummary(String),
/// 设置页的一组消息(**M101 `SETTINGSMSG`**):设置页的**唯一**投递口。
///
/// 四条填页线(java / download / language+appearance / ai+about)各自在自己的文件里定义
/// 消息枚举,本枚举**不再**为设置页增加第二个变体——「加一支设置页消息」从此不碰本文件,
/// 也不碰 `app/update/page_messages.rs` 那条不许 `_ =>` 的 match。加变体与落点见
/// `pages/settings/message.rs`(`app/update/settings.rs` 对它做穷尽分发,漏接线报 E0004)。
Settings(settings::GroupMessage),
/// 换界面语言(**M106**:设置页 General 分组的语言行 → `App::set_language`)。
///
/// 载荷是**用户意图**(跟随系统 / 手选某一门语言),不是「生效语言」:生效语言由
/// `LangChoice::resolve` 按 D28 的顺序算出来,页面不参与计算。
///
/// 为什么不折进 [`Message::Settings`] 那个投递口:语言档位不是「某一组的状态」——它要**落盘**
/// 到 `ui.language` 并**重建整棵视图模型**(目录是 `pages::Model` 的一部分),而那条落点
/// (`app/update/settings/language_appearance.rs`)是 language+appearance 线的文件。
/// 代价是本文件多一个变体;收口方向写在 `settings/general.rs` 的文档里。
Language(LangChoice),
/// 内容库页的一组消息(**M109**):与设置页同一个范式(页面自己的枚举 + 一个投递口)。
///
/// 加一条内容库消息只动 `pages/content/message.rs`,不动本枚举的正文,也不动
/// `app/update/page_messages.rs` 那条不许 `_ =>` 的 match。
Content(content::Message),
/// 实例页「启动参数」区的消息(**M157**):编辑写进页面状态,保存才 dispatch。
Instances(super::instances::InstanceMessage),
/// 自更新动作(**M159**):提示条与设置页「更新」区共用一支。
///
/// 为什么是顶层变体而不是又一个 `GroupMessage`:更新动作是**跨页的一次性意图**
/// (检查 / 下载安装 / 忽略 / 立即重启),不是某一组的内部状态;设置页的「更新」组也不持有
/// 任何字段(快照在 presenter 里)。与 M106 的 [`Self::Language`] 同一取舍,偏离已登记在
/// `pages/settings/update.rs` 的模块文档里。
Update(UpdateAction),
/// 账户页消息(**M114**:三轨登录 / 列表动作 / 表单输入;app 层翻成 `AppCommand`)。
///
/// 与 `Settings` 同一种做法:账户页自己的消息枚举在 `pages/account.rs`,这里只加一支入口。
Account(crate::pages::account::Message),
}
/// 自更新的用户动作(M159)。落点是 `app/update/update_action.rs`。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpdateAction {
/// 立即检查(手动;会清掉「忽略这个版本」)
CheckNow,
/// 下载 → 校验 → 替换(提示条与设置页共用一个动作)
Apply,
/// 忽略这个版本(本次会话不再弹提示条;设置页仍可手动装)
Dismiss,
/// 立即重启(新版本已就位,重启后生效)
RestartNow,
/// 开关「启动时检查更新」
SetAutoCheck(bool),
}
/// 页面动作清单(骨架期够用就行;接线时按业务语义重命名/扩充)。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
/// 新建实例(**还没有后端**,见模块文档偏离 #8)。
NewInstance,
/// 导入(同上)。
Import,
/// 返回实例列表(窄窗整页切换)。**已由 `Message::Navigate(Route::Instances)` 取代**,
/// 这里保留枚举项是为了别把「返回」这件意图从动作表里抹掉(外壳回退路径仍可能用到)。
Back,
/// 启动(占位动作表里的「启动」;真启动走 `Message::Launch`)。
Play,
/// 更多。
More,
/// 复制实例。
Duplicate,
/// 打开实例目录。
OpenFolder,
/// 删除实例。
Delete,
/// 重新探测下载源。
RefreshSources,
/// 浏览版本列表。
BrowseVersions,
/// 导入整合包。
ImportModpack,
/// 检查下载源。
CheckSources,
/// 打开日志目录。
OpenLogs,
/// 复制诊断信息。
CopyDiagnostics,
/// 清空日志。
ClearLogs,
/// 重置当前设置分组。
ResetSection,
/// 保存设置。
SaveSettings,
/// 新增逐实例覆盖。
AddOverride,
}