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` 现在无输出。
146 lines
6.3 KiB
Rust
146 lines
6.3 KiB
Rust
//! `HostTask` → `CoreEvent` 的翻译(从 `host/mod.rs` 拆出,守 400 行硬限)。
|
|
//!
|
|
//! 父模块的私有字段对子模块可见,所以这里直接读写 `self.tx` / `self.launches` 等。
|
|
|
|
use crate::dto::{AppEvent, CoreEvent, InstanceRuntime, LogOutcome};
|
|
use crate::toast::ToastKind;
|
|
|
|
use super::{logs, CoreHost, HostTask, LiveLaunch};
|
|
|
|
impl CoreHost {
|
|
/// 后台消息 → 事件。
|
|
pub(super) fn handle(&mut self, task: HostTask) {
|
|
match task {
|
|
HostTask::Versions(Ok(versions)) => self.send(CoreEvent::VersionListChanged(versions)),
|
|
HostTask::Versions(Err(reason)) => {
|
|
self.toast(ToastKind::Warn, format!("拉取版本清单失败:{reason}"));
|
|
}
|
|
HostTask::Install(progress) => {
|
|
self.install = Some(*progress.clone());
|
|
self.send(CoreEvent::InstallProgress(Some(*progress)));
|
|
}
|
|
HostTask::InstallDone { progress, result } => {
|
|
self.install_cancel = None;
|
|
self.install = Some(*progress.clone());
|
|
self.send(CoreEvent::InstallProgress(Some(*progress)));
|
|
match result {
|
|
Ok(_) => {
|
|
self.refresh_instances();
|
|
self.send(CoreEvent::LogSessionsChanged(logs::sessions(&self.env)));
|
|
}
|
|
Err(reason) => self.toast(ToastKind::Error, reason),
|
|
}
|
|
}
|
|
HostTask::Accounts(accounts) => {
|
|
self.accounts = accounts.clone();
|
|
self.send(CoreEvent::AccountsChanged(accounts));
|
|
self.refresh_instances();
|
|
}
|
|
HostTask::Login(view) => self.send(CoreEvent::DeviceLoginChanged(*view)),
|
|
HostTask::Java(page) => self.send(CoreEvent::JavaChanged(*page)),
|
|
HostTask::Logs {
|
|
sessions,
|
|
lines,
|
|
diagnosis,
|
|
} => {
|
|
self.send(CoreEvent::LogSessionsChanged(sessions));
|
|
self.send(CoreEvent::LogLinesChanged(lines));
|
|
if let Some(report) = diagnosis {
|
|
self.send(CoreEvent::DiagnosisReady(report));
|
|
}
|
|
}
|
|
HostTask::Sessions(sessions) => self.send(CoreEvent::LogSessionsChanged(sessions)),
|
|
HostTask::Tail(batch) => {
|
|
if batch.running {
|
|
self.send(CoreEvent::LogLinesChanged(batch.lines));
|
|
} else {
|
|
self.send(CoreEvent::LogAppended(*batch));
|
|
}
|
|
}
|
|
HostTask::LaunchStarted {
|
|
id,
|
|
session_id,
|
|
pid,
|
|
} => {
|
|
self.pending.remove(&id);
|
|
self.failed.remove(&id);
|
|
self.launches.retain(|live| live.instance != id);
|
|
self.launches.push(LiveLaunch {
|
|
instance: id.clone(),
|
|
session: session_id.clone(),
|
|
pid,
|
|
});
|
|
// 实时尾随一次只跟一场(下载页/日志页共用一个滚动窗口);
|
|
// 已有别的实例在尾随时不抢占——它的日志仍在 JSONL 里,收尾后可回放。
|
|
if self.tail.session().is_none() {
|
|
self.tail.attach(
|
|
self.env
|
|
.sessions_root()
|
|
.join("game")
|
|
.join(format!("{session_id}.jsonl")),
|
|
&session_id,
|
|
);
|
|
}
|
|
self.send(CoreEvent::InstanceRuntimeChanged {
|
|
id,
|
|
runtime: InstanceRuntime::Running { pid },
|
|
});
|
|
}
|
|
HostTask::Runtime { id, runtime } => {
|
|
self.send(CoreEvent::InstanceRuntimeChanged { id, runtime });
|
|
}
|
|
HostTask::LaunchEnd(finished) => {
|
|
// 收尾可能发生在 LaunchStarted 之前(build_request / spawn 失败只发
|
|
// LaunchEnd):pending 必须在这里也清掉,否则 in_flight 恒真、该实例
|
|
// 本会话内再也起不来。
|
|
self.pending.remove(&finished.id);
|
|
self.launches.retain(|live| live.instance != finished.id);
|
|
let runtime = if finished.outcome == LogOutcome::Ok {
|
|
self.failed.remove(&finished.id);
|
|
InstanceRuntime::Idle
|
|
} else {
|
|
// 记住失败原因:随后的 refresh_instances 会重建快照,
|
|
// 不记的话磁贴的「失败/重试」外观会被 Idle 盖掉。
|
|
self.failed
|
|
.insert(finished.id.clone(), finished.summary.clone());
|
|
InstanceRuntime::Failed {
|
|
reason: finished.summary.clone(),
|
|
}
|
|
};
|
|
self.send(CoreEvent::InstanceRuntimeChanged {
|
|
id: finished.id.clone(),
|
|
runtime,
|
|
});
|
|
self.send(CoreEvent::LaunchFinished((*finished).clone()));
|
|
// 只拆自己那场的尾随:并发时另一实例的实时日志不该被这次收尾掐掉。
|
|
if self.tail.session() == Some(finished.session.as_str()) {
|
|
self.tail.detach();
|
|
}
|
|
self.send(CoreEvent::LogSessionsChanged(logs::sessions(&self.env)));
|
|
self.refresh_instances();
|
|
}
|
|
HostTask::Crash(report) => {
|
|
self.send(CoreEvent::DiagnosisReady(*report));
|
|
}
|
|
HostTask::Notice(notice) => self.push_notice(*notice),
|
|
HostTask::Toast(toast) => self.events.push_back(AppEvent::Toast(*toast)),
|
|
}
|
|
}
|
|
|
|
/// 尾随运行中会话(每 `TAIL_POLL_TICKS` 拍一次)。
|
|
pub(super) fn poll_tail(&mut self) {
|
|
let Some((session, lines)) = self.tail.poll() else {
|
|
return;
|
|
};
|
|
if lines.is_empty() {
|
|
return;
|
|
}
|
|
let running = self.launches.iter().any(|live| live.session == session);
|
|
self.send(CoreEvent::LogAppended(crate::dto::LiveLogBatch {
|
|
session,
|
|
lines,
|
|
running,
|
|
}));
|
|
}
|
|
}
|