feat(launch): M157 实例级启动参数(五项回落 + graphicsBackend 三态)
core: 新增 game/launch_overrides(LaunchOverrides 五项 All-Option + 三级回落 + 文本补丁校验)与 launch/overrides(拼装落位:全局 java.extra_args 进 user.jvm、实例 jvmArgs 进 override_jvm、gameArgs 进 user.game、memory 进 -Xmx、backend 进 --graphicsBackend 并复用既有版本门槛告警);实例 schema 登记 launch.* 五键。 presenter: dto/LaunchArgsView+LaunchArgsPatch,主机侧读/写与门槛判定(backend_allowed),AppCommand::SetInstanceLaunchArgs。 cli: 新命令 instances launch-args(读/写/--clear),launch 装配链消费回落值。 app-iced: 实例详情页「启动参数」区(JVM/游戏参数、内存、Java、三态后端 chips + 灰化提示 + 保存/恢复),表单状态归 App、i18n 中英文案同步。
This commit is contained in:
1 parent
d68523b02a
commit
e2021645d7
56 files changed
+3776
-607
No files matched your search
@@ -178,6 +178,12 @@ pub struct App {
|
||||
pub(super) account_form: pages::account::Form,
|
||||
/// 账户页视图模型(与 `view` 同一条「状态变了才重建」的门控)。
|
||||
pub(super) account: pages::account::Page,
|
||||
/// 实例页「启动参数」区的表单(M157):五项 + 脏标记 + 绑定的实例 id。
|
||||
///
|
||||
/// 住在 `App` 的理由与 `download_draft` 完全一致:`refresh_view` 每次都整棵重建
|
||||
/// `pages::Model`,用户正在输入的内容存不到视图模型里。重播种点在
|
||||
/// [`App::refresh_view`](`LaunchForm::sync`,换实例必重播种、同实例的脏草稿不覆盖)。
|
||||
pub(super) launch_form: pages::LaunchForm,
|
||||
/// 皮肤/头像解码缓存(M114:同一张皮肤只解一次)。
|
||||
pub(super) avatars: pages::account::avatar::Cache,
|
||||
/// 上一次**真下发过**的圆角判定指纹(M116 r1 复审 P2-4:判定没变就不重下、不重记日志)。
|
||||
|
||||
@@ -124,6 +124,7 @@ impl App {
|
||||
agent_host: super::presenter::AgentHost::from_env(),
|
||||
// 下载页安装草稿(M105):出厂态 = 没选中、原版、两个空串
|
||||
download_draft: pages::InstallDraft::default(),
|
||||
launch_form: pages::LaunchForm::default(),
|
||||
install_cancelled_seen: 0,
|
||||
content: pages::content::PageState::default(),
|
||||
account_form: pages::account::Form::new(),
|
||||
|
||||
@@ -8,6 +8,8 @@ use unlucky_presenter::toast::ToastKind;
|
||||
|
||||
use crate::pages;
|
||||
|
||||
use unlucky_presenter::router::Route;
|
||||
|
||||
use super::App;
|
||||
|
||||
impl App {
|
||||
@@ -52,10 +54,35 @@ impl App {
|
||||
self.lang(),
|
||||
&self.content,
|
||||
);
|
||||
// M157:启动参数表单的重播种(换实例必重播种、同实例的脏草稿不覆盖)。
|
||||
self.sync_launch_form();
|
||||
// 账户页(M114):与 `view` 同一次门控重建(实现见 `view/account_page.rs`,
|
||||
// 皮肤解码走缓存,所以重复重建不重解图)。
|
||||
self.rebuild_account_page(width);
|
||||
self.view_dirty = false;
|
||||
self.refreshes += 1;
|
||||
}
|
||||
|
||||
/// 把详情页当前实例的启动参数快照灌进表单(唯一的重播种点,见 `pages/instances/state.rs`)。
|
||||
///
|
||||
/// 不在详情页(或实例已不在快照里)时解绑:下次进详情页会重新播种,
|
||||
/// 免得「A 的草稿被当成 B 的初值」。
|
||||
fn sync_launch_form(&mut self) {
|
||||
let Route::Detail(id) = self.state.route() else {
|
||||
self.launch_form.bound = None;
|
||||
return;
|
||||
};
|
||||
match self
|
||||
.state
|
||||
.instances
|
||||
.iter()
|
||||
.find(|instance| &instance.id == id)
|
||||
{
|
||||
Some(instance) => {
|
||||
let view = instance.launch.clone();
|
||||
self.launch_form.sync(&view);
|
||||
}
|
||||
None => self.launch_form.bound = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,13 +202,13 @@ fn the_settings_page_renders_the_real_appearance_group() {
|
||||
assert_eq!(app.view.screen, Screen::Settings);
|
||||
assert_eq!(app.appearance.selected_group, appearance::APPEARANCE_GROUP);
|
||||
|
||||
let _tree = pages::page_with_appearance(&app.ui, &app.view, &app.appearance);
|
||||
let _tree = pages::page_with_appearance(&app.ui, &app.view, &app.appearance, &app.launch_form);
|
||||
drop(_tree);
|
||||
|
||||
// 切分组:只改视图状态,不落盘(`Change::SelectGroup` 的 key() == None)。
|
||||
app.appearance.apply(Change::SelectGroup(0));
|
||||
assert_eq!(app.appearance.selected_group, 0);
|
||||
let _tree = pages::page_with_appearance(&app.ui, &app.view, &app.appearance);
|
||||
let _tree = pages::page_with_appearance(&app.ui, &app.view, &app.appearance, &app.launch_form);
|
||||
}
|
||||
|
||||
/// **P2-2 的守门(复审 r1)**:面板 α 只作用到允许半透明的层。
|
||||
|
||||
@@ -48,6 +48,7 @@ mod boot_env;
|
||||
mod clock;
|
||||
mod content;
|
||||
mod download;
|
||||
mod instances;
|
||||
mod page_messages;
|
||||
mod settings;
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
//! 实例页「启动参数」区的消息落点(M157)。
|
||||
//!
|
||||
//! 三件事:
|
||||
//! 1. **编辑**只改页面状态(`App::launch_form`)并置脏——不 dispatch,不打网络;
|
||||
//! 2. **保存**派一条 [`AppCommand::SetInstanceLaunchArgs`](整份五项补丁),回程由
|
||||
//! `InstancesChanged` 快照刷新页面(不做乐观回显,与 M90 的启动/停止同一条纪律);
|
||||
//! 3. **恢复跟随全局**只清表单(仍需用户点保存)——「点一下就写盘」会让误触直接生效。
|
||||
//!
|
||||
//! 脏标记的清理点在保存之后:`refresh_view` 的 `LaunchForm::sync` 看到「同实例 + 脏」不会覆盖,
|
||||
//! 所以保存后要显式清脏,让下一次快照(host 写完文件回来的那份)重播种。
|
||||
|
||||
use iced::Task;
|
||||
use unlucky_presenter::dto::{AppCommand, LaunchArgsPatch};
|
||||
|
||||
use crate::pages::InstanceMessage;
|
||||
|
||||
use super::super::message::Message;
|
||||
use super::super::state::App;
|
||||
|
||||
impl App {
|
||||
/// 启动参数区的一条消息。
|
||||
pub(super) fn on_instance_message(&mut self, message: InstanceMessage) -> Task<Message> {
|
||||
let Some(instance) = self.selected_instance_id() else {
|
||||
return Task::none();
|
||||
};
|
||||
if !self.launch_form.bound_to(&instance) {
|
||||
// 表单还没对着这个实例(首帧还没播种):不接编辑,免得改到别的实例上。
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
match message {
|
||||
InstanceMessage::Jvm(text) => {
|
||||
self.launch_form.jvm = text;
|
||||
self.launch_form.dirty = true;
|
||||
}
|
||||
InstanceMessage::Game(text) => {
|
||||
self.launch_form.game = text;
|
||||
self.launch_form.dirty = true;
|
||||
}
|
||||
InstanceMessage::Memory(text) => {
|
||||
self.launch_form.memory = text;
|
||||
self.launch_form.dirty = true;
|
||||
}
|
||||
InstanceMessage::Java(text) => {
|
||||
self.launch_form.java = text;
|
||||
self.launch_form.dirty = true;
|
||||
}
|
||||
InstanceMessage::Backend(id) => {
|
||||
self.launch_form.backend = id;
|
||||
self.launch_form.dirty = true;
|
||||
}
|
||||
InstanceMessage::Reset => {
|
||||
self.launch_form.reset();
|
||||
}
|
||||
InstanceMessage::Save => {
|
||||
let patch: LaunchArgsPatch = self.launch_form.patch();
|
||||
self.launch_form.dirty = false;
|
||||
self.dispatch(AppCommand::set_launch_args(&instance, patch));
|
||||
}
|
||||
}
|
||||
// 纯本地编辑:不重建视图模型(页面直接从表单画),只要一帧重绘即可——iced 在
|
||||
// 消息处理完之后本来就会画一帧,所以这里什么都不排。
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// 详情页当前选中的实例 id(不在详情页时为 `None`)。
|
||||
fn selected_instance_id(&self) -> Option<String> {
|
||||
match self.state.route() {
|
||||
unlucky_presenter::router::Route::Detail(id) => Some(id.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_presenter::dto::LaunchArgsView;
|
||||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||
|
||||
use crate::app::state::App;
|
||||
use crate::cli::Options;
|
||||
use crate::pages::{InstanceMessage, LaunchForm};
|
||||
|
||||
use crate::theme::Families;
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
fn app_with_form() -> App {
|
||||
let mut app = App::with_presenter(
|
||||
Options::default(),
|
||||
FAMILIES,
|
||||
SchemeFlavor::Light,
|
||||
Box::new(unlucky_presenter::mock::MockBackend::new()),
|
||||
SettingsBridge::memory_only(),
|
||||
);
|
||||
// 进详情页 + 直接放一份「已播种」的表单:本测试只验消息落点,
|
||||
// 不验播种(那在 `LaunchForm::sync` 的单测里)。
|
||||
app.state.navigate(unlucky_presenter::router::Route::Detail(
|
||||
INSTANCE.to_string(),
|
||||
));
|
||||
app.launch_form = LaunchForm::from_view(
|
||||
Some(INSTANCE),
|
||||
&LaunchArgsView {
|
||||
instance_id: INSTANCE.to_string(),
|
||||
..LaunchArgsView::default()
|
||||
},
|
||||
);
|
||||
app
|
||||
}
|
||||
|
||||
/// 测试用的实例 id(mock 后端的实例快照里有它)。
|
||||
const INSTANCE: &str = "survival";
|
||||
|
||||
/// 编辑 → 脏;恢复 → 五项清空仍需保存;保存前不发命令。
|
||||
#[test]
|
||||
fn editing_marks_dirty_and_reset_waits_for_save() {
|
||||
let mut app = app_with_form();
|
||||
let _ = app.on_instance_message(InstanceMessage::Memory("6144".to_string()));
|
||||
assert_eq!(app.launch_form.memory, "6144");
|
||||
assert!(app.launch_form.dirty);
|
||||
|
||||
let _ = app.on_instance_message(InstanceMessage::Backend("vulkan".to_string()));
|
||||
assert_eq!(app.launch_form.backend, "vulkan");
|
||||
|
||||
let _ = app.on_instance_message(InstanceMessage::Reset);
|
||||
assert_eq!(app.launch_form.memory, "");
|
||||
assert_eq!(app.launch_form.backend, "default");
|
||||
assert!(app.launch_form.dirty, "恢复之后还得点保存才落盘");
|
||||
}
|
||||
|
||||
/// 表单没绑定到当前实例时不接编辑(否则草稿会写到别的实例上)。
|
||||
#[test]
|
||||
fn edits_are_ignored_when_the_form_is_bound_elsewhere() {
|
||||
let mut app = app_with_form();
|
||||
app.launch_form = LaunchForm::from_view(
|
||||
Some("other"),
|
||||
&LaunchArgsView {
|
||||
instance_id: "other".to_string(),
|
||||
..LaunchArgsView::default()
|
||||
},
|
||||
);
|
||||
let _ = app.on_instance_message(InstanceMessage::Memory("9999".to_string()));
|
||||
assert_eq!(app.launch_form.memory, "");
|
||||
assert!(!app.launch_form.dirty);
|
||||
}
|
||||
|
||||
/// 保存之后脏标记清掉(否则下一次快照刷新不敢重播种,界面会一直显示旧值)。
|
||||
#[test]
|
||||
fn save_clears_the_dirty_flag() {
|
||||
let mut app = app_with_form();
|
||||
let _ = app.on_instance_message(InstanceMessage::Java("D:/jdk21/bin/java.exe".to_string()));
|
||||
assert!(app.launch_form.dirty);
|
||||
let _ = app.on_instance_message(InstanceMessage::Save);
|
||||
assert!(!app.launch_form.dirty);
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,8 @@ impl App {
|
||||
self.view_dirty = true;
|
||||
self.refresh_view();
|
||||
}
|
||||
// M157:实例页「启动参数」区(编辑写进页面状态;保存才派发命令)
|
||||
pages::Message::Instances(message) => return self.on_instance_message(message),
|
||||
// M114:账户页(三轨登录 / 列表动作 / 表单输入)
|
||||
pages::Message::Account(message) => return self.on_account(message),
|
||||
// 还没接后端的占位动作:只打日志(哪几个、为什么,见 pages 模块文档的偏离登记)
|
||||
|
||||
@@ -92,6 +92,8 @@ impl App {
|
||||
},
|
||||
// M106:设置页 General 组要画「当前档位 + 生效语言」。
|
||||
self.language(),
|
||||
// M157:实例页「启动参数」区的表单(编辑态归 `App`,页面只借一帧)。
|
||||
&self.launch_form,
|
||||
)
|
||||
.map(Message::Page),
|
||||
agent::docked(&self.ui, session, agent_catalog, self.width)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
use unlucky_presenter::dto::{InstanceRuntime, InstanceSummary, LaunchStage, LoaderKind};
|
||||
|
||||
use crate::pages::{Detail, Meta};
|
||||
use crate::pages::{Detail, LaunchFacts, Meta};
|
||||
|
||||
/// 列表行的副文案:`1.20.4 · Fabric 0.16.5`(原版只写「原版 / Vanilla」,见 [`loader_badge`])。
|
||||
pub(super) fn supporting_line(instance: &InstanceSummary, catalog: Catalog) -> String {
|
||||
@@ -135,6 +135,17 @@ pub(super) fn detail_of(instance: &InstanceSummary, catalog: Catalog) -> Detail
|
||||
instance.runtime,
|
||||
InstanceRuntime::Busy { .. } | InstanceRuntime::Running { .. }
|
||||
),
|
||||
// M157:启动参数区的**只读事实**(门槛 / 生效值 / 三态词表全部来自 presenter 快照,
|
||||
// 页面不复制任何一条判据;表单值在 `App::launch_form`)。
|
||||
launch: LaunchFacts {
|
||||
allowed: instance.launch.graphics_backend_allowed,
|
||||
mc_version: instance.launch.mc_version.clone(),
|
||||
min_version: instance.launch.graphics_backend_min_version.clone(),
|
||||
memory_effective_mb: instance.launch.memory_effective_mb,
|
||||
java_effective: instance.launch.java_effective.clone(),
|
||||
backend_effective: instance.launch.graphics_backend_effective.clone(),
|
||||
options: instance.launch.graphics_backend_options.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ fn instance(runtime: InstanceRuntime) -> InstanceSummary {
|
||||
isolation: "isolated".to_string(),
|
||||
version_id: "1.20.4".to_string(),
|
||||
version_present: true,
|
||||
launch: unlucky_presenter::dto::LaunchArgsView::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user