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:
AskaEth committed 2026-11-02 09:29:31 +08:00
1 parent 00f132d8ce
commit f159529936
69 files changed
+441 -129

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]);
}
}