feat(presenter): 新建 unlucky-presenter,把 host/状态/DTO/设置桥从 app 迁出(M85 第 1 块)
D26(iced 重写)的关键一次性动作:UI 框架无关的宿主层独立成 crate, Slint 版(crates/app)与 iced 版(crates/app-iced)共用,Slint 退役后不再重写一遍。 - 新建 crates/presenter(package `unlucky-presenter` / lib `unlucky_presenter`), workspace 登记 + crate 文档头写清「这层为什么存在」、对外表面、UI 侧允许/禁止、 iced 接线骨架、以及哪些东西留在了 crates/app 及为什么。 - `git mv`(保留文件历史)迁入:host/(25 文件)、dto/(11 文件)、state.rs(+tests)、 prefs.rs、settings_bridge.rs、toast.rs、router.rs、dialogs.rs、wizard.rs、 mock.rs、mock_data.rs、backend.rs。模块路径与公开 API 名称不变(`host::install` 等), 内部实现零改动。 - 新增 `theme`:把原本混在 ui_bind/theme_runtime 里的**纯判据**(CONFIG_POLL_TICKS / SYSTEM_THEME_POLL_TICKS / TILE_REGENERATE_STEP / BackgroundKey / tile_viewport / system_theme_value / 两个 poll 判据)抽出来,附 5 个单测;app 侧改成调用它们。 - crates/app 改为消费者:删掉迁走的文件,`crate::host|state|dto|prefs|settings_bridge| toast|router|dialogs|wizard|mock|mock_data|backend` 全部改写为 `unlucky_presenter::*` (64 处),backend.rs 的 config_path/build_backend 提为 pub。 - crates/app-iced 只登记 unlucky-presenter 依赖,本 mission 不接线。 - prefs.rs 的 2 处 slint 文档引用改写;`grep -rn slint crates/presenter` 现在无输出。
This commit is contained in:
1 parent
00f132d8ce
commit
f159529936
69 files changed
+441
-129
No files matched your search
@@ -0,0 +1,162 @@
|
||||
//! `CoreHost::dispatch` 的指令实现(从 `host/mod.rs` 拆出,守 400 行上限)。
|
||||
//!
|
||||
//! 父模块的私有字段对子模块可见,所以这里直接读写 `self.env` / `self.tx` 等。
|
||||
|
||||
use crate::dto::{AppCommand, Backend, CoreEvent, CreateRequest, LoaderKind};
|
||||
use crate::toast::ToastKind;
|
||||
|
||||
use super::accounts::{self, LoginJob};
|
||||
use super::{game, install, java, launch, logs, settings, versions, CoreHost};
|
||||
|
||||
impl CoreHost {
|
||||
/// 下发一条用户意图(`Backend::dispatch` 的实现体)。
|
||||
pub(super) fn dispatch_command(&mut self, command: AppCommand) {
|
||||
self.dispatch_inner(command);
|
||||
}
|
||||
}
|
||||
impl CoreHost {
|
||||
/// 指令分发(见 `dispatch_command`)。
|
||||
fn dispatch_inner(&mut self, command: AppCommand) {
|
||||
match command {
|
||||
AppCommand::RefreshAll => self.refresh_all(),
|
||||
AppCommand::LaunchInstance(id) => self.start_launch(&id),
|
||||
AppCommand::StopInstance(id) => match self.running_pid(&id) {
|
||||
Some(pid) => match launch::stop(pid) {
|
||||
Ok(()) => self.toast(ToastKind::Info, "已请求停止游戏进程"),
|
||||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||||
},
|
||||
None => self.toast(ToastKind::Warn, "这个实例当前没有在跑"),
|
||||
},
|
||||
AppCommand::ListVersions(_) => versions::spawn_list(self.env.clone(), self.tx.clone()),
|
||||
AppCommand::InstallVersion(version) => {
|
||||
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
self.install_cancel = Some(std::sync::Arc::clone(&cancel));
|
||||
install::spawn_install(
|
||||
self.env.clone(),
|
||||
self.tx.clone(),
|
||||
CreateRequest {
|
||||
name: version.clone(),
|
||||
version,
|
||||
loader: LoaderKind::Vanilla,
|
||||
loader_version: String::new(),
|
||||
},
|
||||
cancel,
|
||||
);
|
||||
}
|
||||
AppCommand::CreateInstance(request) => {
|
||||
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
self.install_cancel = Some(std::sync::Arc::clone(&cancel));
|
||||
install::spawn_install(self.env.clone(), self.tx.clone(), request, cancel);
|
||||
}
|
||||
AppCommand::CancelInstall => {
|
||||
// 取消只在阶段边界生效(core 的下载循环没有取消钩子):这里先把面板收回,
|
||||
// 后台线程会在下一阶段开始前看到标记并收尾。
|
||||
if let Some(cancel) = &self.install_cancel {
|
||||
cancel.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
self.install = None;
|
||||
self.send(CoreEvent::InstallProgress(None));
|
||||
}
|
||||
AppCommand::SetIsolation { id, mode } => {
|
||||
match game::set_isolation(&self.env, &id, &mode) {
|
||||
Ok(()) => {
|
||||
self.toast(ToastKind::Success, "隔离档位已更新");
|
||||
self.refresh_instances();
|
||||
}
|
||||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||||
}
|
||||
}
|
||||
AppCommand::RefreshLogSessions => {
|
||||
self.send(CoreEvent::LogSessionsChanged(logs::sessions(&self.env)));
|
||||
}
|
||||
AppCommand::OpenLogSession(id) => self.open_session(&id),
|
||||
AppCommand::ImportCrashLog(path) => match logs::analyze_crash_file(&path) {
|
||||
Ok((label, report)) => {
|
||||
self.send(CoreEvent::DiagnosisReady(report));
|
||||
self.toast(ToastKind::Info, label);
|
||||
}
|
||||
Err(reason) => self.toast(ToastKind::Warn, reason),
|
||||
},
|
||||
AppCommand::AddOfflineAccount(name) => match accounts::add_offline(&self.env, &name) {
|
||||
Ok(display) => {
|
||||
self.toast(ToastKind::Success, format!("已添加离线账户 {display}"));
|
||||
self.refresh_accounts();
|
||||
}
|
||||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||||
},
|
||||
AppCommand::RemoveAccount(key) => match accounts::remove(&self.env, &key) {
|
||||
Ok(display) => {
|
||||
if self.active_account.as_deref() == Some(key.as_str()) {
|
||||
self.active_account = None;
|
||||
}
|
||||
self.toast(ToastKind::Info, format!("已删除账户 {display}"));
|
||||
self.refresh_accounts();
|
||||
}
|
||||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||||
},
|
||||
AppCommand::SwitchAccount(id) => {
|
||||
self.active_account = Some(id);
|
||||
self.refresh_accounts();
|
||||
self.toast(ToastKind::Info, "已切换启动账户");
|
||||
}
|
||||
AppCommand::StartDeviceLogin => {
|
||||
self.login = Some(LoginJob::new());
|
||||
let job = self.login.clone().unwrap_or_default();
|
||||
self.send(CoreEvent::DeviceLoginChanged(crate::dto::DeviceLoginView {
|
||||
phase: crate::dto::DeviceLoginPhase::Requesting,
|
||||
message: "正在向微软申请设备码…".to_string(),
|
||||
..crate::dto::DeviceLoginView::new()
|
||||
}));
|
||||
accounts::start_login(self.env.clone(), self.tx.clone(), job);
|
||||
}
|
||||
AppCommand::StartBrowserLogin => {
|
||||
self.login = Some(LoginJob::new());
|
||||
let job = self.login.clone().unwrap_or_default();
|
||||
self.send(CoreEvent::DeviceLoginChanged(crate::dto::DeviceLoginView {
|
||||
phase: crate::dto::DeviceLoginPhase::Requesting,
|
||||
message: "正在准备浏览器登录(本地回调端口)…".to_string(),
|
||||
..crate::dto::DeviceLoginView::new()
|
||||
}));
|
||||
accounts::start_browser_login(self.env.clone(), self.tx.clone(), job);
|
||||
}
|
||||
AppCommand::CancelDeviceLogin => {
|
||||
if let Some(job) = &self.login {
|
||||
job.cancel();
|
||||
}
|
||||
self.login = None;
|
||||
self.send(CoreEvent::DeviceLoginChanged(
|
||||
crate::dto::DeviceLoginView::new(),
|
||||
));
|
||||
}
|
||||
AppCommand::InstallJava(vendor) => {
|
||||
self.java_busy = true;
|
||||
self.java_message = format!("正在安装 Java({vendor})…");
|
||||
self.refresh_java();
|
||||
java::spawn_install(
|
||||
self.env.clone(),
|
||||
self.tx.clone(),
|
||||
unlucky_core::game::java::JdkVendor::parse(&vendor),
|
||||
);
|
||||
}
|
||||
AppCommand::SetSetting { key, value } => match settings::set(&self.env, &key, &value) {
|
||||
Ok(()) => {
|
||||
self.send(CoreEvent::SettingsChanged(settings::groups(&self.env)));
|
||||
self.java_chain.clear();
|
||||
self.refresh_java();
|
||||
}
|
||||
Err(reason) => self.toast(ToastKind::Error, reason),
|
||||
},
|
||||
AppCommand::DismissNotice(id) => {
|
||||
self.notices.retain(|notice| notice.id != id);
|
||||
self.send(CoreEvent::NoticesChanged(self.notices.clone()));
|
||||
}
|
||||
// AI 对话:agent 的 GUI host 未落地前用脚本回复(不接真实模型)
|
||||
other @ (AppCommand::AskAgent(_) | AppCommand::StopAgent | AppCommand::SetYolo(_)) => {
|
||||
self.chat.dispatch(other);
|
||||
for event in self.chat.drain_events() {
|
||||
self.events.push_back(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user