Files
Unlucky-Launcher/crates/app/src/callbacks.rs
T
AskaEth ddc0577f83 feat(app): GUI 全量接线,五页改吃 core 真实数据(M43)
app 从「mock 骨架」切到「真实 core host」:新增 src/host 装配层(env/job/
game/launch/identity/versions/install/meter/java/accounts/logs/settings/engine/
dispatch),是 app 里除 settings_bridge 外唯一 use unlucky_core 的地方,出口只有
crate::dto 的 DTO/事件(三通道纪律,AGENTS §6)。

页面接线:
- 实例页:profiles+版本 JSON 继承链+会话索引 → 磁贴(真实版本/加载器徽标/最后启动/
  隔离档位/载荷缺失);启动按钮走 LaunchRequest 装配 → Launcher::spawn → JSONL
  尾随 → 运行/失败回写;M30 ModeChangeWarning 进横幅通知条。
- 新建实例向导:版本 → 加载器 → 名字 → 确认 → 真实安装进度;Fabric/Quilt 可装,
  Forge/NeoForge 因需要 installer processors(仅 CLI 有)显式拒装并给出 CLI 命令。
- 下载页:远端 manifest(12h 缓存)+ 内嵌 unlisted → 分类(正式/预览/远古四子类);
  安装进度(文件数/字节/速度/ETA,速度由相邻采样差分)+ 阶段边界可取消;
  Java 分组:四家发行版 + 回退链可视化(计划链/真实 ChainLog)+ 本地运行时。
- 账户页:三轨账户列表(只读公开字段)+ 离线新增 + 删除/切换 + 微软设备码登录
  (用户码/链接/轮询状态;取消后丢弃结果)。凭据只在 host/identity 与 host/accounts
  出现,DTO 无 token 字段(单测钉住)。
- 设置页:新增 download/java/data/ai/mcp 五组,行由 core::setting schema 键生成,
  写回走 SettingStore::set(与 ull config set / AI set_config 同源 launcher.json)。
- 日志页:会话索引 → 列表;事件 JSONL → 日志行(等级高亮)+ D8 规则引擎诊断卡片;
  启动中实时尾随(600ms/拍,只读新增字节);崩溃日志导入分析(hs_err/crash/latest)。

架构与守线:
- 新页面状态走 ui/page_state.slint 的 PageState global(main.slint 再导出),
  Rust 侧 page_sync.rs 灌入、callbacks_pages.rs 接线;避免属性逐层转发顶破 400 行。
- DTO 全部 Serialize+Deserialize(IPC 契约),新增 accounts/java/settings/install/
  notices 五个子模块;事件扩展 LogAppended/LaunchFinished/AccountsChanged/
  DeviceLoginChanged/JavaChanged/InstallProgress/SettingsChanged/NoticesChanged。
- 截图通道扩展:--data-dir(钉数据根且 settings 同源)/--mock/--screenshot-page/
  --screenshot-wizard/--screenshot-session/--screenshot-install/--screenshot-launch
  (真实中间态,取图后自动 stop 防孤儿进程)。

已知边界:GUI 账户「当前启动账户」仅内存态(core schema 无 auth.* 键,app 不自建键);
Forge/NeoForge GUI 安装待 processors 执行器上移。
2026-10-31 21:55:58 +08:00

353 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 回调接线:把 `ui/main.slint` 暴露的每个回调翻译成「状态变更 + AppCommand + 同步」。
//!
//! 逐条列出而不做通用转发,是为了让「用户能做的每一件事」在这里一目了然;
//! 新增交互时只改这一处 + `.slint` 的 callback 声明。
use std::rc::Weak;
use slint::ComponentHandle;
use crate::conv;
use crate::dto::AppCommand;
use crate::router::Route;
use crate::toast::ToastKind;
use crate::ui_bind::Inner;
/// 接全部回调;闭包持弱引用,避免 `MainWindow` 与闭包互相持有导致窗口永不释放。
pub(crate) fn wire(weak: Weak<Inner>) {
let Some(inner) = weak.upgrade() else { return };
// 只在注册期间持有强引用;闭包内一律持弱引用(否则窗口与闭包互相持有、永不释放)。
let ui = &inner.ui;
wire_window(ui, &weak);
wire_navigation(ui, &weak);
wire_instances(ui, &weak);
wire_download(ui, &weak);
wire_logs(ui, &weak);
wire_settings(ui, &weak);
wire_overlays(ui, &weak);
}
fn wire_window(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_toggle_maximize(move || {
let Some(this) = weak.upgrade() else { return };
let window = this.ui.window();
window.set_maximized(!window.is_maximized());
});
}
{
let weak = weak.clone();
ui.on_minimize(move || {
if let Some(this) = weak.upgrade() {
this.ui.window().set_minimized(true);
}
});
}
{
let weak = weak.clone();
ui.on_request_close(move || {
if let Some(this) = weak.upgrade() {
let _ = this.ui.hide();
}
let _ = slint::quit_event_loop();
});
}
}
fn wire_navigation(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_navigate(move |route| {
let Some(this) = weak.upgrade() else { return };
if let Some(route) = conv::route_from(route) {
this.state.borrow_mut().navigate(route);
this.sync();
}
});
}
{
let weak = weak.clone();
ui.on_toggle_nav(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().toggle_nav();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_toggle_ai(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().toggle_ai();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_expand_ai(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().navigate(Route::Agent);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_dock_ai(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().back();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_toggle_yolo(move || {
let Some(this) = weak.upgrade() else { return };
let enabled = {
let mut state = this.state.borrow_mut();
state.ai.yolo = !state.ai.yolo;
state.ai.yolo
};
this.dispatch(AppCommand::SetYolo(enabled));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_ai_send(move |text| {
let Some(this) = weak.upgrade() else { return };
let text = text.to_string();
if text.trim().is_empty() {
return;
}
// 不做本地回显:user 消息由 host 的 MessageAppended 事件带回(契约 §3)
this.state.borrow_mut().ai.draft.clear();
this.ui.set_ai_draft("".into());
this.ask_agent(&text);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_ai_stop(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().ai.finish_stream();
this.dispatch(AppCommand::StopAgent);
this.sync();
});
}
}
fn wire_instances(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_instance_launch(move |id| {
let Some(this) = weak.upgrade() else { return };
this.dispatch(AppCommand::LaunchInstance(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_configure(move |id| {
let Some(this) = weak.upgrade() else { return };
// 齿轮键进实例详情(G4 占位页),实例级设置后续挂在该页 tab 上
this.state
.borrow_mut()
.navigate(Route::Detail(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_stop(move |id| {
let Some(this) = weak.upgrade() else { return };
this.dispatch(AppCommand::StopInstance(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_open(move |id| {
let Some(this) = weak.upgrade() else { return };
this.state
.borrow_mut()
.navigate(Route::Detail(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_new_instance(move || {
let Some(this) = weak.upgrade() else { return };
// G5:打开向导(版本 → 加载器 → 名字 → 确认 → 安装进度)
this.state.borrow_mut().wizard = crate::wizard::WizardState::open();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_view_changed(move |view| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().prefs.instance_view = conv::instance_view_from(view);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_tile_size_changed(move |size| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().prefs.tile_size = conv::tile_size_from(size);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_instance_query_changed(move |query| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().instance_query = query.to_string();
this.sync();
});
}
}
fn wire_download(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_download_category_changed(move |category| {
let Some(this) = weak.upgrade() else { return };
let category = conv::version_category(category);
this.state.borrow_mut().prefs.download_category = category;
this.dispatch(AppCommand::ListVersions(category));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_download_install(move |id| {
let Some(this) = weak.upgrade() else { return };
this.dispatch(AppCommand::InstallVersion(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_installed_only_changed(move |only| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().installed_only = only;
this.sync();
});
}
{
let weak = weak.clone();
ui.on_download_query_changed(move |query| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().download_query = query.to_string();
this.sync();
});
}
{
let weak = weak.clone();
ui.on_ancient_help(move || {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().toasts.push(
ToastKind::Info,
"远古版(classic/alpha/beta)需旧版 Java 与旧启动参数;完整说明页 P1 接入",
);
this.sync();
});
}
}
fn wire_logs(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_log_select(move |id| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().selected_session = Some(id.to_string());
this.dispatch(AppCommand::OpenLogSession(id.to_string()));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_log_ask_ai(move |id| {
let Some(this) = weak.upgrade() else { return };
let question = format!("帮我看下会话 {id} 为什么崩溃");
this.state.borrow_mut().navigate(Route::Agent);
this.ask_agent(&question);
this.sync();
});
}
{
let weak = weak.clone();
ui.on_log_apply_fix(move || {
let Some(this) = weak.upgrade() else { return };
this.state
.borrow_mut()
.toasts
.push(ToastKind::Warn, "一键修复在 P1 接 diagnose 的 fix_action");
this.sync();
});
}
}
fn wire_settings(ui: &crate::MainWindow, weak: &Weak<Inner>) {
// 主题三态 + 外观四件在 appearance_bind.rs(拆文件守 400 行)
crate::appearance_bind::wire(ui, weak);
{
let weak = weak.clone();
ui.on_yolo_changed(move |enabled| {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().ai.yolo = enabled;
this.dispatch(AppCommand::SetYolo(enabled));
this.sync();
});
}
{
let weak = weak.clone();
ui.on_mcp_changed(move |_enabled| {
let Some(this) = weak.upgrade() else { return };
this.state
.borrow_mut()
.toasts
.push(ToastKind::Info, "MCP server 开关在 P1 接 agent");
this.sync();
});
}
// P0 只保留入口:设置分组与 AI 端点暂不落状态(P1 接 setting schema)。
ui.on_settings_section_changed(|_section| {});
ui.on_ai_endpoint_changed(|_index| {});
}
fn wire_overlays(ui: &crate::MainWindow, weak: &Weak<Inner>) {
{
let weak = weak.clone();
ui.on_dialog_primary(move || close_current_dialog(&weak));
}
{
let weak = weak.clone();
ui.on_dialog_secondary(move || close_current_dialog(&weak));
}
{
let weak = weak.clone();
ui.on_dialog_dismissed(move || close_current_dialog(&weak));
}
{
let weak = weak.clone();
ui.on_toast_dismissed(move |id| {
let Some(this) = weak.upgrade() else { return };
if let Ok(id) = id.parse::<u64>() {
this.state.borrow_mut().toasts.dismiss(id);
this.sync();
}
});
}
}
fn close_current_dialog(weak: &Weak<Inner>) {
let Some(this) = weak.upgrade() else { return };
this.state.borrow_mut().dialogs.close_current();
this.sync();
}