用户投诉原话:「我们的启动日志放在哪?…他找半天没找到崩溃日志」「只有一个安装日志」。 启动器运行日志(.ULL/logs/launcher.log) - presenter 构造 HostEnv 时就登记日志目录:启动早期缓冲的 200 行([probe]/[font]/[theme]) 当场刷盘,不再是「只有开过一局游戏才有日志」。 - app/** 里所有 eprintln! 诊断改走 app::diag::diag!(stderr + 文件双写,行首 [tag] 即 scope), 含 boot/boot::fallback/state/update 各线。 - boot 那一拍补一行后端事实(request/effective/fallback)。 - 滚动上限(1 MiB → launcher.1.log,只留一份)写进 README 与 docs/logs-location.md。 会话日志搬家:data/agent/sessions → logs - LauncherLayout: sessions_root()/game_logs() 新增,agent_sessions() 降级为只读兼容。 - host/logs 与 cli/commands/sessions 两个根合并(同 id 新根优先),event_path 回落旧家。 - 不搬不删;日志目录写一份 session-log-location.json 记录(moved=0, policy=read-only-compat)。 入口与文案 - 设置页「通用」新增「日志文件夹」行:路径 + 打开目录 + 复制路径(i18n 中英)。 - 日志页页头的「打开目录」不再是占位:走 M187 的 open_dir,失败文本带路径并送进剪贴板。 - README.md / README.zh-CN.md 新增「日志在哪」章节(含 crash-reports 只在游戏走到那一步才有), docs/logs-location.md 给出上限、双写判据、迁移策略与没验的部分。 守门(每条都有变异证明,见报告) - 旧家会话仍列得出/打不开 ⇒ 只读新根变红;新会话写 logs/game ⇒ 只读旧根、写回旧家都变红。 - launcher.log 滚动 ⇒ 干掉 rename 变红。 - 打开日志文件夹指向真实目录 ⇒ 换成 cache/ 或去掉 create_dir_all 都变红。 - 日志根随三模式变化 ⇒ 把 HostEnv::logs_dir 写死成便携落点变红。
132 lines
6.2 KiB
Rust
132 lines
6.2 KiB
Rust
//! App 侧的外观动作:**下发整窗 α / 写回配置 / 打取证日志**。
|
||
//!
|
||
//! 三件事各自只有一条路,且都从这里出去(`update` 只负责把它们按消息串起来):
|
||
//!
|
||
//! | 动作 | 唯一落点 | 为什么 |
|
||
//! |---|---|---|
|
||
//! | 下发整窗 α | [`App::apply_window_alpha`] → `app::window_alpha::dispatch` | 判据在 `platform`、FFI 在 `platform/win32`,这里只做「取 α → 交给执行侧 → 记日志」 |
|
||
//! | 写回配置 | [`App::persist_appearance`](8 个 UI 字段)+ `AppCommand::SetSetting`(另外**三个**只读键) | 每个键**只有一个写者**:`AppearancePrefs` 里没有 `window_opacity` / `palette_source` / `window_corner`,它们只能走 schema 指令 |
|
||
//! | 取证日志 | [`App::log_appearance`] | 一行里同时有请求值、生效值、下发字节与 FFI 结局 |
|
||
//!
|
||
//! ## 为什么「写回」要分两条路
|
||
//!
|
||
//! `SettingsBridge::persist` 收的是 `AppearancePrefs`(背景图四件 + 面板 α + 玻璃 + 强调色),
|
||
//! 它的实现是「先读文件当前值,再覆盖我们改的那几个字段」——所以用它写**它覆盖的键**最省事,
|
||
//! 也不会把用户的自定义强调色冲掉。而 `appearance.window_opacity` / `appearance.palette_source` /
|
||
//! `appearance.window_corner` 不在那个结构里(它们是「只读下层键」,见
|
||
//! `presenter::prefs::AppearanceBase` 的**四个**字段:整窗 α / 标题栏三态 / 配色来源 / 窗口圆角),
|
||
//! 写它们只能走 schema 白名单指令 `AppCommand::SetSetting`(真 host 用 `SettingStore::set` 落盘)。
|
||
//! 其中「标题栏三态」目前**没有 UI 写者**(D37 的接线未落地),所以走 `SetSetting` 的是**三个**键。
|
||
|
||
use crate::app::diag::diag;
|
||
use unlucky_presenter::dto::AppCommand;
|
||
|
||
use crate::pages::appearance::Change;
|
||
|
||
use super::state::App;
|
||
use super::window_alpha::{self, Dispatch};
|
||
|
||
impl App {
|
||
/// 把当前整窗 α 交给执行侧(补 `WS_EX_LAYERED` + `SetLayeredWindowAttributes`)并记一行日志。
|
||
///
|
||
/// 没有句柄时**也打日志**(`hwnd=none`):不能让「α 没生效」这件事只体现为「屏幕上没变化」。
|
||
pub(super) fn apply_window_alpha(&self, context: &str) {
|
||
let Some(hwnd) = self.hwnd else {
|
||
self.log_appearance(context, None);
|
||
return;
|
||
};
|
||
let alpha = crate::platform::effective_window_alpha(
|
||
self.appearance.backend,
|
||
f64::from(self.appearance.window_opacity),
|
||
);
|
||
let dispatch = window_alpha::dispatch(hwnd, alpha);
|
||
self.log_appearance(context, Some(&dispatch));
|
||
}
|
||
|
||
/// 取证日志:`[appearance] <状态行> layered=… outcome=…`。
|
||
///
|
||
/// 数值来自 [`crate::pages::appearance::State::log_line`](请求值 / 生效值 / 下发字节),
|
||
/// 尾巴来自执行侧。**这一行就是「透明度到底设成了多少」的证据**——RDP 上的观感不可信。
|
||
pub(super) fn log_appearance(&self, context: &str, dispatch: Option<&Dispatch>) {
|
||
let mut line = self.appearance.log_line(context);
|
||
if let Some(dispatch) = dispatch {
|
||
line.push(' ');
|
||
line.push_str(&dispatch.describe());
|
||
} else {
|
||
line.push_str(" layered=skipped outcome=no-window");
|
||
}
|
||
diag!("{line}");
|
||
}
|
||
|
||
/// 设置页改了一项外观。
|
||
pub(super) fn on_appearance(&mut self, change: Change) {
|
||
let needs_window = self.appearance.apply(change.clone());
|
||
|
||
// 配色来源 / 背景图可能变了 ⇒ `Ui` 可能要改用解析出的方案。
|
||
self.rebuild_ui();
|
||
self.view_dirty = true;
|
||
self.refresh_view();
|
||
|
||
match change.key() {
|
||
// 三个只读下层键(整窗 α / 配色来源 / 窗口圆角):走 schema 指令
|
||
// (`SettingsBridge` 的结构里没有它们;标题栏三态还没有 UI 写者,不在此列)。
|
||
Some(_)
|
||
if matches!(
|
||
change,
|
||
Change::WindowOpacity(_) | Change::PaletteSource(_) | Change::Corner(_)
|
||
) =>
|
||
{
|
||
if let (Some(key), Some(value)) = (change.key(), change.value()) {
|
||
self.dispatch(AppCommand::SetSetting {
|
||
key: key.to_owned(),
|
||
value,
|
||
});
|
||
}
|
||
}
|
||
// 其余键:走 bridge(它知道怎么保留强调色这类本 mission 不管的字段)。
|
||
Some(_) => self.persist_appearance(),
|
||
None => {}
|
||
}
|
||
|
||
if needs_window {
|
||
self.apply_window_alpha("change");
|
||
} else {
|
||
self.log_appearance("change", None);
|
||
}
|
||
// M116:圆角档位变了要**重新判定 + 重新下发**(与整窗 α 是两条独立的管线)。
|
||
self.apply_window_corner("change");
|
||
}
|
||
|
||
/// 重读配置后同步外观(热切:外部 CLI / AI 改完 1 秒内生效)。
|
||
pub(super) fn reload_appearance(&mut self) {
|
||
let (prefs, _theme) = self.bridge.load();
|
||
let base = self.bridge.load_base();
|
||
self.appearance.reload(&prefs, &base);
|
||
self.appearance_prefs = prefs;
|
||
self.rebuild_ui();
|
||
self.view_dirty = true;
|
||
self.apply_window_alpha("reload");
|
||
// M116:外部(CLI / AI)改了 `appearance.window_corner` 也要热生效。
|
||
self.apply_window_corner("reload");
|
||
}
|
||
|
||
/// 把外观写回 `launcher.json`(只改本 mission 管的字段,强调色/玻璃原样保留)。
|
||
fn persist_appearance(&mut self) {
|
||
let mut prefs = self.appearance_prefs.clone();
|
||
prefs.bg_image_enabled = self.appearance.bg_enabled;
|
||
prefs.bg_image = self.appearance.bg_path.clone();
|
||
prefs.bg_fill = self.appearance.bg_fill;
|
||
prefs.bg_dim = self.appearance.bg_dim;
|
||
prefs.panel_alpha = self.appearance.panel_alpha;
|
||
prefs.glass = self.appearance.glass;
|
||
|
||
self.bridge.persist(&prefs, self.theme_mode);
|
||
self.appearance_prefs = prefs;
|
||
|
||
match self.bridge.path() {
|
||
Some(path) => diag!("[settings] 外观已写回 {}", path.display()),
|
||
None => diag!("[settings] 外观未落盘(数据根解析不出来,内存态运行)"),
|
||
}
|
||
}
|
||
}
|