Files
Unlucky-Launcher/crates/app-iced/src/pages/model.rs
T
AskaEth 03741ecd2d fix(m161): 收更新键/按钮/矮窗口/启动错误/导入结果五笔债
① update.auto_check / update.channel 登记进 core setting schema(1.6.0)
   - 新增 setting/keys/update.rs(两键 + UPDATE_CHANNELS,只允许 stable);
     SettingSchema 加 with_all 供域键表批量登记。
   - 新增 setting/update_prefs.rs:cache/update/prefs.json → launcher.json 的一次性迁移。
     四条硬约束:源指纹闸门(幂等)、不丢 false、新值胜出、旧文件改名 .migrated 后不再被读;
     主配置不可写时不动旧文件。收据 config/update-prefs-migration.json。
   - presenter Runtime 建会话时迁一次并把结局留在 migration();prefs 字段换成
     auto_check + 每次派发从主配置读 channel(不再硬编码 Channel::Stable)。
     保留旧字段 auto_check 的真实来源(原有测试 cases 会断言)。
   - CLI config get/set/dump 先跑同一次迁移(幂等),输出里给 migration 段与收据路径。

② presenter/dto/events.rs 的「本次会话不再弹提示条」注释改成真实语义
   (记的是版本号:同版本压住、更高版本照弹、手动检查清掉)。

③ 检查/下载进行中按钮禁用:pages/update/busy.rs 的 busy/can_check/can_apply;
   设置页两颗按钮改画禁用(不是消失),app 层 on_update_action 再挡一道
   (忙碌时 CheckNow/Apply 不派发);新增两条用例(含「忙完恢回可点」)。

④ 内容库低高度:content_layout::MIN_BODY_HEIGHT=460 + BodyMode(按固定件+三整行算出来,
   用例守着算式);frame::body_height 从 Ui::metrics 算正文可用高,pages::Model 带 window_height。
   ≥460 列表自己滚;<460 主列整列一个滚动条、列表按自然高度铺开。

⑤ 启动错误文案本地化:presenter 删掉 launch_summary / 「启动失败:」前缀 / 五条中文阶段前缀,
   改发 LaunchFailureView{stage,detail};LaunchFinished 改带 exit_code/uptime_ms/failure,
   Toast 由 app 层按 i18n key 现求(与 install_cancelled 同构)。InstancesFailed 拆成
   InstancesFailedStage(列表单行)+ LaunchStageFailed(详情/Toast 全文)。

⑥ 导入结果块:ImportReportView 加 failure_stage(NotStarted/RolledBack/RollbackIncomplete,
   由 core 的 rollback 机器位推出);标题、图标、底色三处分档,预检失败不再说「已回滚」。

顺带:六处注释/文档漂移(update.rs 的分工表与偏好落点、pack 的「已整包回滚」、
build_request 的 Errors、settings/update 的按钮禁用说明等),
以及为守 400 行把 update/cli-config/core-keys/presenter-dto/presenter-launch/i18n-golden
里贴线或越线的文件按模块拆开(纯搬移,行为不变)。
2026-11-07 15:02:13 +08:00

164 lines
6.7 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.
//! 视图模型:**presenter 快照 → 页面要画的东西**的唯一转换产物。
//!
//! ## 为什么页面不直接吃 presenter 的快照
//!
//! 页面层(`pages/*`)要能用**两套数据源**喂同一个渲染函数:
//!
//! | 数据源 | 谁转 | 用途 |
//! |---|---|---|
//! | presenter 的 `AppState` 快照 | `app/view.rs` 的自由函数 `model(&AppState, width, forced)` | 产品运行时的真页面 |
//! | `pages/instances/data.rs` 的演示条目 | [`crate::pages::demo`] | M84 骨架图的可复现基线、`pages_demo` example |
//!
//! 如果页面直接读 `InstanceSummary`,演示数据就得先伪造成 `InstanceSummary`(含 core 语义字段),
//! 基线图会跟着业务 DTO 的每次改动漂。所以中间加一层**纯展示字段**的模型:它只含「要画什么」。
//!
//! 文本都是 `String`/`&'static str`,没有框架类型——这一层可以纯单测(见 `pages/tests.rs`)。
use unlucky_i18n::Lang;
use unlucky_presenter::router::NavSection;
use crate::app::components::interaction::Interaction;
use super::content::Content;
use super::logs::Logs;
use super::update::Update;
use super::{Download, Screen};
/// 实例列表的一行(`ListItem` 的槽位:overline = 运行状态,headline = 名字,supporting = 版本)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
/// 实例 id(点行 → `Route::Detail(id)`)
pub id: String,
/// 显示名
pub name: String,
/// 副文案(`1.20.4 · Fabric 0.16.5`)
pub supporting: String,
/// 运行状态(`None` = 空闲,不占 overline 槽位)
pub status: Option<String>,
}
/// 元信息卡:标签 + 值(`VERSION` / `Fabric 0.16.5`)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Meta {
/// 卡片上排的标签(`LabelMedium`)
pub label: String,
/// 卡片下排的值(`BodyMedium`)
pub value: String,
}
/// 详情列要画的东西。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Detail {
/// 实例 id(启动/停止命令要用它)
pub id: String,
/// 标题(`TitleLarge`)
pub name: String,
/// 标题下的一行(`BodySmall`)
pub subtitle: String,
/// 元信息网格(顺序 = 展示顺序;列数仍由 `layout::meta_columns` 决定)
pub meta: Vec<Meta>,
/// 正文档(演示数据有;真实快照没有对应字段时为 `None`)
pub note: Option<String>,
/// 运行状态行(`None` = 空闲,不占版面)
pub status: Option<String>,
/// 「Play」可用
pub can_launch: bool,
/// 「Stop」可用
pub can_stop: bool,
/// 启动参数区的只读事实(M157)
pub launch: LaunchFacts,
}
/// 启动参数区(M157)的**只读事实**:门槛、生效值、三态词表。
///
/// 可编辑的值不在这里——它们在页面状态 [`super::instances::LaunchForm`](由 `App` 持有)。
/// 这样「每来一条事件就重建视图模型」不会把用户正在输入的内容冲掉。
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct LaunchFacts {
/// 这个版本能不能下发 `--graphicsBackend`(core 的版本门槛判定)。
pub allowed: bool,
/// 判定依据的游戏版本(提示文案里给出)。
pub mc_version: String,
/// 门槛的最低版本(`26.2-snapshot-2`;提示与测试共用一份来源)。
pub min_version: String,
/// 生效最大堆(三级回落之后)。
pub memory_effective_mb: u32,
/// 生效 Java(空 = 自动挑)。
pub java_effective: String,
/// 生效图形后端。
pub backend_effective: String,
/// 三态词表(`default` / `opengl` / `vulkan`)。
pub options: Vec<String>,
}
/// 实例页要画的东西。
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Instances {
/// 列表列(顺序稳定 = 实例列表顺序)
pub rows: Vec<Row>,
/// 当前选中的实例详情(`None` = 没选中 → 空态)
pub detail: Option<Detail>,
}
/// 一屏的视图模型。
#[derive(Debug, Clone, PartialEq)]
pub struct Model {
/// 当前路由落在哪个页面上
pub screen: Screen,
/// 窗口宽度(**含外壳**:布局档位判据就是它)
pub window_width: f32,
/// 窗口高度(**含外壳**)。
///
/// 只有「矮窗口要换版面」这一类判据用它(M161:内容库在低高度下不能把列表压扁)——
/// 宽度档位仍只看 [`Self::window_width`]。
pub window_height: f32,
/// 导航轨高亮项(`None` = 没有高亮,如 AI 独立页)
pub active: Option<NavSection>,
/// 实例页的数据(其它三页还是占位视觉,不读)
pub instances: Instances,
/// 下载页的数据(M94:版本列表 / 选中 / 安装进度,全部来自 presenter 快照)
pub download: Download,
/// 日志页的数据(M95:presenter 快照 → 日志行 + 诊断结论,见 [`super::logs`])。
pub logs: Logs,
/// 内容库页的数据(M109:检索条件 / 命中项 / 装入闸门,见 [`super::content`])。
pub content: Content,
/// 自更新的展示状态(M159:设置页「更新」区 + 顶部提示条,见 [`super::update`])。
pub update: Update,
/// 界面语言(M106):页面文案走 i18n key,语言由 app 层决定后灌进来。
///
/// 谁填:`app/state.rs` 的 `App::refresh_view`(`LangState::lang()` → `view::model(..)`),
/// 即「用户档位 / 系统 locale → 生效语言」这条链的结论。**不是**基线值:M95 以前这里是
/// `Lang::En` 占位,M106 之后中文系统上灌进来的是 `Lang::ZhCn`。
pub lang: Lang,
/// 取证用的强制交互态(`--force-state`):`Some` 时页面里的控件按该态画。
///
/// M90 r2(P3-1)之前这个开关只作用到 `shell` 回退路径,产品页是**静默失效**的;
/// 现在由 `app/view.rs` 把 `cli::ForcedState` 翻成 `Interaction` 灌进模型。
pub forced: Option<Interaction>,
}
impl Model {
/// 空模型(只有外壳参数,页面数据全空)——窄窗退化与空态的构造入口。
pub fn new(
screen: Screen,
window_width: f32,
window_height: f32,
active: Option<NavSection>,
) -> Self {
Self {
screen,
window_width,
window_height,
active,
instances: Instances::default(),
download: Download::loading(),
logs: Logs::default(),
// 演示模型也从这个空壳起步(`demo_model` 再按页面填数据)
content: super::content::demo(unlucky_presenter::dto::ContentKind::Mod, Lang::En),
update: Update::default(),
lang: Lang::En,
forced: None,
}
}
}