Files
Unlucky-Launcher/crates/app-iced/src/pages/instances/demo.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

129 lines
5.2 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.
//! 实例页的**演示数据 → 视图模型**转换(M84 的出图基线靠这份数据)。
//!
//! 只被 [`crate::pages::demo_model`] 用:产品路径的转换在 `app/view.rs`(presenter 快照 → 模型)。
//! 两份数据源喂同一个渲染函数,这是 [`crate::pages::model`] 存在的理由。
use unlucky_i18n::{Catalog, Key, Lang};
use unlucky_presenter::dto::LaunchArgsView;
use super::super::model::{Detail, Instances, LaunchFacts, Meta, Row};
use super::data::{DEMO, NOTE, SELECTED};
use super::state::LaunchForm;
/// 演示路径的目录:**恒英文**。
///
/// 为什么不是 `model.lang`:本函数只被 `pages::demo_model` 调用,而那条路(`pages_demo` harness /
/// 单测)走 `Model::new`,`lang` 恒为 `Lang::En`(harness 没有换语言的开关)。产品路径不读这里
/// ——它走 `app/view.rs` 的快照转换。等 harness 有 `--lang` 时把这一行换成参数即可。
fn catalog() -> Catalog {
Lang::En.catalog()
}
/// 元信息卡的标签(与 `app/view/instances.rs` 的 `detail_of` 同一批 key,只是这里恒英文)。
fn label(key: Key) -> &'static str {
catalog().text(key)
}
/// 把 M84 的演示条目 [`DEMO`] 转成视图模型(真页面走 `app/view.rs` 的 `AppState` 转换)。
pub(crate) fn demo() -> Instances {
let rows = DEMO
.iter()
.map(|entry| Row {
id: entry.name.to_string(),
name: entry.name.to_string(),
supporting: format!("{} · {}", entry.version, entry.loader),
status: None,
})
.collect();
let entry = &DEMO[SELECTED];
let detail = Detail {
id: entry.name.to_string(),
name: entry.name.to_string(),
subtitle: format!("{} · {}", entry.version, entry.loader),
meta: vec![
meta(label(Key::InstancesMetaVersion), entry.version),
meta(label(Key::InstancesMetaLoader), entry.loader),
meta(label(Key::InstancesMetaMemory), entry.memory),
meta(label(Key::InstancesMetaJava), entry.java),
meta(label(Key::InstancesMetaMods), entry.mods),
meta(label(Key::InstancesMetaLastPlayed), entry.last_played),
],
note: Some(NOTE.to_string()),
status: None,
can_launch: true,
can_stop: false,
launch: launch_facts(),
};
Instances {
rows,
detail: Some(detail),
}
}
fn meta(label: &str, value: &str) -> Meta {
Meta {
label: label.to_string(),
value: value.to_string(),
}
}
/// 启动参数区的演示事实(M157 的 D30 出图夹具)。
///
/// `ULL_LAUNCH_ARGS_FIXTURE=gated` 出「版本不够、三态灰化」那一屏;缺省(`open`)是
/// 「26.2 实例 + Vulkan 可选」的正常态。**这是出图夹具,不是产品逻辑**:产品路径的门槛
/// 来自 presenter 快照(core 的 `graphics_backend_allowed`),与这个环境变量无关。
pub(crate) fn launch_facts() -> LaunchFacts {
let gated = gated_fixture();
LaunchFacts {
allowed: !gated,
mc_version: if gated { "1.20.4" } else { "26.2-snapshot-2" }.to_string(),
min_version: "26.2-snapshot-2".to_string(),
memory_effective_mb: if gated { 2048 } else { 4096 },
java_effective: "C:/Java/temurin-21/bin/java.exe".to_string(),
backend_effective: if gated { "default" } else { "vulkan" }.to_string(),
options: ["default", "opengl", "vulkan"]
.iter()
.map(|id| (*id).to_string())
.collect(),
}
}
/// 演示用的表单初值(页面状态的所有者是 `App`;harness 没有 `App`,自己持有一份)。
///
/// 返回**拥有**的表单而不是 `&'static`:出图/单测各自持有一份即可(一帧借用一次),
/// 而 `static` 会让所有出图路径共享同一份可编辑状态(两个参数的控件现在都是单行
/// `field::text_field` + `String`,不再是早先的多行编辑器)。
pub(crate) fn demo_form() -> LaunchForm {
LaunchForm::from_view(Some(DEMO[SELECTED].name), &demo_view())
}
fn demo_view() -> LaunchArgsView {
let gated = gated_fixture();
LaunchArgsView {
instance_id: DEMO[SELECTED].name.to_string(),
jvm_args: "-XX:+UseG1GC".to_string(),
game_args: "--fullscreen".to_string(),
memory_mb: (!gated).then_some(4096),
memory_effective_mb: if gated { 2048 } else { 4096 },
java_path: String::new(),
java_effective: "C:/Java/temurin-21/bin/java.exe".to_string(),
graphics_backend: if gated { "default" } else { "vulkan" }.to_string(),
graphics_backend_effective: if gated { "default" } else { "vulkan" }.to_string(),
graphics_backend_allowed: !gated,
mc_version: if gated { "1.20.4" } else { "26.2-snapshot-2" }.to_string(),
graphics_backend_min_version: "26.2-snapshot-2".to_string(),
graphics_backend_options: ["default", "opengl", "vulkan"]
.iter()
.map(|id| (*id).to_string())
.collect(),
}
}
fn gated_fixture() -> bool {
std::env::var("ULL_LAUNCH_ARGS_FIXTURE")
.map(|value| value.trim().eq_ignore_ascii_case("gated"))
.unwrap_or(false)
}