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 → 备份留旧文件」,并验证
摘要不符时拒绝应用、目标文件零改动、暂存目录零残留。
129 lines
5.2 KiB
Rust
129 lines
5.2 KiB
Rust
//! 实例页的**演示数据 → 视图模型**转换(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)
|
||
}
|