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 执行器上移。
This commit is contained in:
AskaEth committed 2026-10-31 21:55:58 +08:00
1 parent bc41dfcf7d
commit ddc0577f83
64 files changed
+8047 -408

No files matched your search

+234
View File
@@ -0,0 +1,234 @@
//! Java 页 DTO:四家发行版、回退链可视化与本地运行时清单(D23/D13)。
//!
//! 回退链是「可观测性」的载体:安装失败时用户要能看见「Temurin 跳过 → Oracle 失败 →
//! Microsoft 命中」这条链,而不是只拿到一句「下载失败」。链的每一步由 host 从
//! `core::game::java::source::ChainLog` 翻译过来。
use serde::{Deserialize, Serialize};
/// 一家发行版的展示行。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JavaVendorView {
/// 稳定 id(`temurin` / `oracle` / `microsoft` / `zulu`)
pub id: String,
/// 展示名
pub display: String,
/// 优先级序号(1 起)
pub rank: u32,
/// 支持的 Java 大版本摘要(`21/17/11`)
pub majors: String,
/// 可用源(镜像 + 吞吐备注)
pub sources: Vec<String>,
/// 是否已装过这家(托管目录里有现成的)
pub installed: bool,
}
/// 回退链一步的结局。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JavaChainOutcome {
/// 还没尝试(安装前的计划链)
Pending,
/// 跳过(不支持目标大版本)
Skip,
/// 尝试过但失败
Failed,
/// 命中(下载/安装成功)
Hit,
}
impl JavaChainOutcome {
/// 徽标文字。
pub fn label(self) -> &'static str {
match self {
Self::Pending => "待尝试",
Self::Skip => "跳过",
Self::Failed => "失败",
Self::Hit => "命中",
}
}
/// 从 core 的 `VendorAttempt` 结局字符串解析。
pub fn parse(raw: &str) -> Self {
match raw {
"hit" => Self::Hit,
"failed" => Self::Failed,
"skip" | "skipped" => Self::Skip,
_ => Self::Pending,
}
}
}
/// 回退链一步。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JavaChainStep {
/// 发行版 id
pub vendor: String,
/// 结局
pub outcome: JavaChainOutcome,
/// 细节(跳过原因 / 失败摘要 / 命中的下载源)
pub detail: String,
}
/// 本地已发现的 Java 运行时。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JavaRuntimeView {
/// 可执行文件路径
pub binary: String,
/// 版本串(`17.0.9`)
pub version: String,
/// 大版本
pub major: u32,
/// 发行方
pub vendor: String,
/// 来源分类(system / managed / instance)
pub origin: String,
}
/// Java 页快照(一次事件整体替换)。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JavaPageView {
/// 地区判定(`domestic` / `global`)
pub region: String,
/// 优先级的来源(`region_default` / `java.vendorPriority`)
pub priority_source: String,
/// 生效顺序(`temurin → oracle → microsoft → zulu`)
pub order_text: String,
/// 四家展示行(按生效顺序)
pub vendors: Vec<JavaVendorView>,
/// 回退链(计划链或上一次安装的真实链)
pub chain: Vec<JavaChainStep>,
/// 本地运行时
pub runtimes: Vec<JavaRuntimeView>,
/// 托管目录
pub managed_roots: Vec<String>,
/// 是否有 Java 安装在跑
pub busy: bool,
/// 状态/错误文案
pub message: String,
}
impl Default for JavaPageView {
fn default() -> Self {
Self::new()
}
}
impl JavaPageView {
/// 空态(托管目录还没扫到东西)。
pub fn new() -> Self {
Self {
region: "global".to_string(),
priority_source: "region_default".to_string(),
order_text: String::new(),
vendors: Vec::new(),
chain: Vec::new(),
runtimes: Vec::new(),
managed_roots: Vec::new(),
busy: false,
message: String::new(),
}
}
/// 计划链:按生效顺序铺一排「待尝试」(安装前的可视化)。
pub fn planned_chain(vendors: &[JavaVendorView]) -> Vec<JavaChainStep> {
vendors
.iter()
.map(|vendor| JavaChainStep {
vendor: vendor.id.clone(),
outcome: JavaChainOutcome::Pending,
detail: String::new(),
})
.collect()
}
/// 命中的那一家的展示名(`None` = 链上还没命中)。
pub fn hit_vendor(&self) -> Option<&str> {
self.chain
.iter()
.find(|step| step.outcome == JavaChainOutcome::Hit)
.map(|step| step.vendor.as_str())
}
/// 已安装的大版本列表(去重升序)。
pub fn majors(&self) -> Vec<u32> {
let mut majors: Vec<u32> = self.runtimes.iter().map(|runtime| runtime.major).collect();
majors.sort_unstable();
majors.dedup();
majors
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vendor(id: &str, rank: u32) -> JavaVendorView {
JavaVendorView {
id: id.to_string(),
display: id.to_uppercase(),
rank,
majors: "21/17".to_string(),
sources: Vec::new(),
installed: false,
}
}
#[test]
fn planned_chain_is_all_pending_in_rank_order() {
let vendors = vec![vendor("temurin", 1), vendor("oracle", 2)];
let chain = JavaPageView::planned_chain(&vendors);
assert_eq!(chain.len(), 2);
assert!(chain
.iter()
.all(|step| step.outcome == JavaChainOutcome::Pending));
assert_eq!(chain[0].vendor, "temurin");
}
#[test]
fn hit_vendor_finds_the_hit_step() {
let mut page = JavaPageView::new();
page.chain = vec![
JavaChainStep {
vendor: "temurin".to_string(),
outcome: JavaChainOutcome::Skip,
detail: "不支持 8".to_string(),
},
JavaChainStep {
vendor: "oracle".to_string(),
outcome: JavaChainOutcome::Hit,
detail: "BMCLAPI".to_string(),
},
];
assert_eq!(page.hit_vendor(), Some("oracle"));
}
#[test]
fn outcome_parses_core_words() {
assert_eq!(JavaChainOutcome::parse("hit"), JavaChainOutcome::Hit);
assert_eq!(JavaChainOutcome::parse("failed"), JavaChainOutcome::Failed);
assert_eq!(JavaChainOutcome::parse("skip"), JavaChainOutcome::Skip);
assert_eq!(JavaChainOutcome::parse("?"), JavaChainOutcome::Pending);
}
#[test]
fn majors_are_deduped_and_sorted() {
let mut page = JavaPageView::new();
page.runtimes = vec![
JavaRuntimeView {
binary: "a".to_string(),
version: "17".to_string(),
major: 17,
vendor: "Temurin".to_string(),
origin: "managed".to_string(),
},
JavaRuntimeView {
binary: "b".to_string(),
version: "17".to_string(),
major: 17,
vendor: "Oracle".to_string(),
origin: "system".to_string(),
},
];
assert_eq!(page.majors(), vec![17]);
}
}