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

+4 -4
View File
@@ -1,10 +1,10 @@
//! AI 页与覆盖层:会话消息、对话框(一次一条)、Toast。
use crate::dialogs as dlg;
use crate::dto::ChatRole as DtoChatRole;
use crate::state::AppState;
use crate::toast as toast_mod;
use crate::{ChatRole, DialogKind, UiChatMessage, UiDialog, UiToast};
use unlucky_presenter::dialogs as dlg;
use unlucky_presenter::dto::ChatRole as DtoChatRole;
use unlucky_presenter::state::AppState;
use unlucky_presenter::toast as toast_mod;
/// AI 会话消息。
pub fn chat(state: &AppState) -> Vec<UiChatMessage> {
+2 -2
View File
@@ -2,9 +2,9 @@
use slint::Color;
use crate::dto::{self, InstanceRuntime};
use crate::state::{AppState, TileCell};
use crate::{LoaderKind, RuntimePhase, UiTile};
use unlucky_presenter::dto::{self, InstanceRuntime};
use unlucky_presenter::state::{AppState, TileCell};
/// 头像/图标占位的底色池(Metro 强调色系,按 id 哈希稳定取色)。
const HUES: [u32; 8] = [
+2 -2
View File
@@ -1,8 +1,8 @@
//! 日志页:会话列表、日志行、D8 诊断卡片。
use crate::dto::{LogLevel as DtoLogLevel, LogOutcome as DtoLogOutcome};
use crate::state::AppState;
use crate::{LogLevel, LogOutcome, UiDiagnosis, UiLogLine, UiLogSession};
use unlucky_presenter::dto::{LogLevel as DtoLogLevel, LogOutcome as DtoLogOutcome};
use unlucky_presenter::state::AppState;
/// 日志会话列表。
pub fn log_sessions(state: &AppState) -> Vec<UiLogSession> {
+18 -17
View File
@@ -5,17 +5,17 @@
use slint::{ModelRc, SharedString, VecModel};
use crate::dto::{
AccountKind as DtoAccountKind, DeviceLoginPhase, InstallStage as DtoInstallStage,
JavaChainOutcome, NoticeLevel, SettingControl, VersionKind as DtoVersionKind,
};
use crate::state::AppState;
use crate::wizard::WizardStep;
use crate::{
AccountKind, InstallStage, JavaStepOutcome, NoticeKind, SettingControl as UiControl, UiAccount,
UiDeviceCode, UiInstallProgress, UiJavaRuntime, UiJavaStep, UiJavaVendor, UiNotice,
UiSettingGroup, UiSettingRow, UiVersion, UiWizard,
};
use unlucky_presenter::dto::{
AccountKind as DtoAccountKind, DeviceLoginPhase, InstallStage as DtoInstallStage,
JavaChainOutcome, NoticeLevel, SettingControl, VersionKind as DtoVersionKind,
};
use unlucky_presenter::state::AppState;
use unlucky_presenter::wizard::WizardStep;
fn strings(items: &[String]) -> ModelRc<SharedString> {
ModelRc::new(VecModel::from(
@@ -162,8 +162,8 @@ pub fn install(state: &AppState) -> UiInstallProgress {
bytes_label: if progress.bytes_total > 0 {
format!(
"{}/{}",
crate::dto::human_bytes(progress.bytes_done),
crate::dto::human_bytes(progress.bytes_total)
unlucky_presenter::dto::human_bytes(progress.bytes_done),
unlucky_presenter::dto::human_bytes(progress.bytes_total)
)
} else {
String::new()
@@ -236,7 +236,7 @@ pub fn wizard(state: &AppState) -> UiWizard {
let step = wizard.step;
let can_next = match step {
WizardStep::Version => !wizard.version.trim().is_empty(),
WizardStep::Name => crate::wizard::validate_name(&wizard.name).is_ok(),
WizardStep::Name => unlucky_presenter::wizard::validate_name(&wizard.name).is_ok(),
WizardStep::Loader | WizardStep::Confirm => true,
WizardStep::Progress => false,
};
@@ -248,7 +248,7 @@ pub fn wizard(state: &AppState) -> UiWizard {
step_total: 5,
version: wizard.version.as_str().into(),
loader: loader_index(wizard.loader),
loader_label: if wizard.loader == crate::dto::LoaderKind::Vanilla {
loader_label: if wizard.loader == unlucky_presenter::dto::LoaderKind::Vanilla {
"原版".to_string()
} else {
format!("{} {}", wizard.loader.label(), wizard.loader_version.trim())
@@ -265,14 +265,15 @@ pub fn wizard(state: &AppState) -> UiWizard {
}
/// 加载器在向导里的单选序号(与 `wizard.slint` 的 LoaderChoice 顺序一致)。
pub fn loader_index(loader: crate::dto::LoaderKind) -> i32 {
pub fn loader_index(loader: unlucky_presenter::dto::LoaderKind) -> i32 {
match loader {
crate::dto::LoaderKind::Vanilla => 0,
crate::dto::LoaderKind::Fabric => 1,
crate::dto::LoaderKind::Quilt => 2,
crate::dto::LoaderKind::Forge => 3,
crate::dto::LoaderKind::NeoForge => 4,
crate::dto::LoaderKind::OptiFine | crate::dto::LoaderKind::LiteLoader => 0,
unlucky_presenter::dto::LoaderKind::Vanilla => 0,
unlucky_presenter::dto::LoaderKind::Fabric => 1,
unlucky_presenter::dto::LoaderKind::Quilt => 2,
unlucky_presenter::dto::LoaderKind::Forge => 3,
unlucky_presenter::dto::LoaderKind::NeoForge => 4,
unlucky_presenter::dto::LoaderKind::OptiFine
| unlucky_presenter::dto::LoaderKind::LiteLoader => 0,
}
}
+5 -5
View File
@@ -1,12 +1,12 @@
//! conv/pages.rs 的单测(拆文件守 400 行上限)。
use super::*;
use crate::dto::{
use slint::Model;
use unlucky_presenter::dto::{
AccountView, DeviceLoginView, InstallProgressView, JavaPageView, JavaVendorView, NoticeView,
SettingGroupView, SettingRowView, VersionSummary,
};
use crate::wizard::WizardState;
use slint::Model;
use unlucky_presenter::wizard::WizardState;
#[test]
fn accounts_and_device_login_render() {
@@ -90,7 +90,7 @@ fn java_chain_and_vendors_map_outcomes() {
sources: vec!["BMCLAPI".to_string(), "GitHub".to_string()],
installed: true,
}],
chain: vec![crate::dto::JavaChainStep {
chain: vec![unlucky_presenter::dto::JavaChainStep {
vendor: "temurin".to_string(),
outcome: JavaChainOutcome::Hit,
detail: "21".to_string(),
@@ -161,7 +161,7 @@ fn wizard_versions_skip_ancient_ones() {
},
VersionSummary {
id: "rd-132211".to_string(),
kind: DtoVersionKind::Ancient(crate::dto::AncientKind::Classic),
kind: DtoVersionKind::Ancient(unlucky_presenter::dto::AncientKind::Classic),
released: "2009-05-13".to_string(),
installed: false,
},
+3 -3
View File
@@ -1,9 +1,9 @@
//! UI 偏好/路由枚举的双向映射(Slint 枚举 ↔ 状态层枚举)。
use crate::dto::{self, VersionCategory};
use crate::prefs;
use crate::router;
use crate::{BgFillKind, DownloadCategory, InstanceView, RouteKind, ThemeMode, TileSize};
use unlucky_presenter::dto::{self, VersionCategory};
use unlucky_presenter::prefs;
use unlucky_presenter::router;
/// 路由 → 导航高亮/页面选择。
pub fn route(route: &router::Route) -> RouteKind {
+6 -4
View File
@@ -1,8 +1,10 @@
//! 版本列表与下载分类映射(下载页)。
use crate::dto::{AncientKind, VersionCategory, VersionKind as DtoVersionKind, VersionSummary};
use crate::state::AppState;
use crate::{UiVersion, VersionKind};
use unlucky_presenter::dto::{
AncientKind, VersionCategory, VersionKind as DtoVersionKind, VersionSummary,
};
use unlucky_presenter::state::AppState;
/// 分类侧栏 → 该分类下的版本(远古版按子类细分)。
pub fn in_category(category: VersionCategory, kind: DtoVersionKind) -> bool {
@@ -58,8 +60,8 @@ fn version_kind(kind: DtoVersionKind) -> VersionKind {
#[cfg(test)]
mod tests {
use super::versions;
use crate::dto::{VersionKind, VersionSummary};
use crate::state::AppState;
use unlucky_presenter::dto::{VersionKind, VersionSummary};
use unlucky_presenter::state::AppState;
fn state_with_versions() -> AppState {
let mut state = AppState::new();