From 2f491c31f92ca4a417a163c50eda0e2dab9e13d3 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 5 Nov 2026 11:48:44 +0800 Subject: [PATCH] =?UTF-8?q?refactor(app-iced):=20M120=20=E6=8A=8A=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E9=A1=B5=E7=9A=84=E5=B1=95=E7=A4=BA=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E4=BB=8E=20view.rs=20=E6=8B=86=E5=87=BA=E5=8E=BB=EF=BC=88?= =?UTF-8?q?=E7=BA=AF=E6=90=AC=E7=A7=BB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit view.rs 是「快照 → 视图模型」的唯一入口,实例页那一段(列表副文案 / 运行状态 / 元信息卡 / 时间戳)占了它近一半。M120 要在这批文案上接 i18n,先按功能拆成 view/instances.rs(account_page / logs / rail 是同一个先例),本次一字未改行为。 唯一的非搬移改动:view/tests.rs 显式 import InstanceRuntime / LaunchStage (它们原来是从 view.rs 的 use 里顺带借到的,拆分后不再可见)。 --- crates/app-iced/src/app/view.rs | 108 ++------------------- crates/app-iced/src/app/view/instances.rs | 110 ++++++++++++++++++++++ crates/app-iced/src/app/view/tests.rs | 5 +- 3 files changed, 124 insertions(+), 99 deletions(-) create mode 100644 crates/app-iced/src/app/view/instances.rs diff --git a/crates/app-iced/src/app/view.rs b/crates/app-iced/src/app/view.rs index 919c256..6017fcd 100644 --- a/crates/app-iced/src/app/view.rs +++ b/crates/app-iced/src/app/view.rs @@ -1,17 +1,19 @@ //! `view`:presenter 快照 → 页面视图模型 → 控件树。 //! //! 展示转换(快照字段 → 显示文本/网格)**全部在这里**,页面层只认 [`pages::Model`]。 +//! +//! 实例页那一段(`1.20.4 · Fabric`、运行状态、元信息卡)M120 搬进 [`instances`]:本文件 +//! 当时贴着 400 行硬线,而 i18n 还要在这个函数里接一批阶段标签。 use iced::{Element, Theme}; use unlucky_i18n::Lang; -use unlucky_presenter::dto::{InstanceRuntime, InstanceSummary, LaunchStage}; use unlucky_presenter::router::Route; use unlucky_presenter::state::AppState; use crate::app::components::interaction::Interaction; use crate::cli::Screen; -use crate::pages::{self, Detail, Meta, Row}; +use crate::pages::{self, Row}; use crate::shell::{self, agent, NavItem}; use super::message::{nav_item_of, Message}; @@ -19,9 +21,11 @@ use super::state::{log_first_frame, App}; use super::APP_TITLE; mod account_page; +mod instances; mod logs; mod rail; +use instances::{detail_of, runtime_status, supporting_line}; use logs::logs_model; impl App { @@ -140,7 +144,6 @@ pub(super) fn model( ) -> pages::Model { let route = state.route().clone(); let screen = pages::Screen::of(&route).unwrap_or(pages::Screen::Instances); - let rows: Vec = state .tiles() .into_iter() @@ -180,101 +183,10 @@ pub(super) fn model( } } -/// 列表行的副文案:`1.20.4 · Fabric 0.16.5`(原版只写「原版」,见 `LoaderInfo::badge`)。 -fn supporting_line(instance: &InstanceSummary) -> String { - format!("{} · {}", instance.mc_version, instance.loader.badge()) -} - -/// 运行状态(列表行的 overline 与详情页的状态行共用一份文案)。 -fn runtime_status(runtime: &InstanceRuntime) -> Option { - match runtime { - InstanceRuntime::Idle => None, - InstanceRuntime::Busy { stage, progress } => Some(format!( - "{} · {:.0}%", - stage_label(*stage), - progress.ratio() * 100.0 - )), - InstanceRuntime::Running { pid } => Some(format!("Running · pid {pid}")), - InstanceRuntime::Failed { reason } => Some(format!("Launch failed · {reason}")), - } -} - -/// 阶段文案(英文基线;i18n mission 接手后统一走词表)。 -/// -/// 不复用 `LaunchStage::label()`:那是中文,而本 crate 的界面文案在 i18n 之前是英文基线 -/// (见 `pages/instances/data.rs` 的同一条理由:干净 CI 上没有中文字体)。 -fn stage_label(stage: LaunchStage) -> &'static str { - match stage { - LaunchStage::Download => "Downloading", - LaunchStage::Verify => "Verifying", - LaunchStage::Launch => "Starting", - LaunchStage::Window => "Waiting for window", - } -} - -/// 详情列的内容(字段全部来自 [`InstanceSummary`])。 -fn detail_of(instance: &InstanceSummary) -> Detail { - // 视觉 P2-2(M90 r2):列表行与详情曾经「同物两名」(列表 `badge()` 出「原版」、详情硬编码 - // 「Vanilla」)。现在两边都走 `LoaderInfo::badge()` 这一个入口;等 i18n 落地后由词表统一, - // 届时两处会同时变——这正是要的。 - let loader = instance.loader.badge(); - - Detail { - id: instance.id.clone(), - name: instance.name.clone(), - subtitle: supporting_line(instance), - meta: vec![ - meta("VERSION", &instance.mc_version), - meta("LOADER", &loader), - meta("ISOLATION", &instance.isolation), - meta("VERSION ID", &instance.version_id), - meta( - "FILES", - if instance.version_present { - "Present on disk" - } else { - "Missing" - }, - ), - meta("LAST PLAYED", &last_played(instance.last_played.as_deref())), - ], - // 真快照没有「正文描述」这种字段(偏离 #9):演示数据才有正文段 - note: None, - status: runtime_status(&instance.runtime), - can_launch: matches!( - instance.runtime, - InstanceRuntime::Idle | InstanceRuntime::Failed { .. } - ), - can_stop: matches!( - instance.runtime, - InstanceRuntime::Busy { .. } | InstanceRuntime::Running { .. } - ), - } -} - -fn meta(label: &str, value: &str) -> Meta { - Meta { - label: label.to_string(), - value: value.to_string(), - } -} - -/// ISO-8601(`2026-10-29T19:22:00Z`)→ `2026-10-29 19:22 UTC`;`None` = 从未启动。 -/// -/// 只做切片不做时区换算:core 给的是 UTC,`Z` 就是它的时区;本地时区换算等 i18n/时间格式 mission。 -/// 非 ASCII 或长度不足时原样返回(不 panic、不猜)。 -pub(super) fn last_played(raw: Option<&str>) -> String { - let Some(text) = raw else { - return "Never".to_string(); - }; - match (text.get(0..10), text.get(11..16)) { - (Some(date), Some(time)) if date.starts_with(|c: char| c.is_ascii_digit()) => { - format!("{date} {time} UTC") - } - _ => text.to_string(), - } -} - #[cfg(test)] #[path = "view/tests.rs"] mod tests; + +/// 测试只借这一条(其余三条 `model()` 自己用)。 +#[cfg(test)] +use instances::last_played; diff --git a/crates/app-iced/src/app/view/instances.rs b/crates/app-iced/src/app/view/instances.rs new file mode 100644 index 0000000..605809f --- /dev/null +++ b/crates/app-iced/src/app/view/instances.rs @@ -0,0 +1,110 @@ +//! 实例页的**展示转换**(M120 从 `app/view.rs` 原样搬出:那边给 i18n 让出行数)。 +//! +//! | 函数 | 产物 | 谁读 | +//! |---|---|---| +//! | [`supporting_line`] | 列表行 / 详情副标题 `1.20.4 · Fabric 0.16.5` | [`crate::pages::Row`] / [`crate::pages::Detail`] | +//! | [`runtime_status`] | 运行状态行(列表 overline + 详情状态行共用一份) | 同上 | +//! | [`detail_of`] | 详情列的全部字段 | [`crate::pages::Detail`] | +//! | [`last_played`] | `2026-10-29 19:22 UTC` / `Never` | 元信息卡 `LAST PLAYED` | +//! +//! 这里只做「快照字段 → 要画什么」,不碰控件树。文案的语言由调用方传给 `view::model` 的 +//! `Lang` 决定(页面与这一段都从同一个 `Lang` 取目录,见 `pages::Model.lang`)。 + +use unlucky_presenter::dto::{InstanceRuntime, InstanceSummary, LaunchStage}; + +use crate::pages::{Detail, Meta}; + +/// 列表行的副文案:`1.20.4 · Fabric 0.16.5`(原版只写「原版」,见 `LoaderInfo::badge`)。 +pub(super) fn supporting_line(instance: &InstanceSummary) -> String { + format!("{} · {}", instance.mc_version, instance.loader.badge()) +} + +/// 运行状态(列表行的 overline 与详情页的状态行共用一份文案)。 +pub(super) fn runtime_status(runtime: &InstanceRuntime) -> Option { + match runtime { + InstanceRuntime::Idle => None, + InstanceRuntime::Busy { stage, progress } => Some(format!( + "{} · {:.0}%", + stage_label(*stage), + progress.ratio() * 100.0 + )), + InstanceRuntime::Running { pid } => Some(format!("Running · pid {pid}")), + InstanceRuntime::Failed { reason } => Some(format!("Launch failed · {reason}")), + } +} + +/// 阶段文案(英文基线;i18n mission 接手后统一走词表)。 +/// +/// 不复用 `LaunchStage::label()`:那是中文,而本 crate 的界面文案在 i18n 之前是英文基线 +/// (见 `pages/instances/data.rs` 的同一条理由:干净 CI 上没有中文字体)。 +fn stage_label(stage: LaunchStage) -> &'static str { + match stage { + LaunchStage::Download => "Downloading", + LaunchStage::Verify => "Verifying", + LaunchStage::Launch => "Starting", + LaunchStage::Window => "Waiting for window", + } +} + +/// 详情列的内容(字段全部来自 [`InstanceSummary`])。 +pub(super) fn detail_of(instance: &InstanceSummary) -> Detail { + // 视觉 P2-2(M90 r2):列表行与详情曾经「同物两名」(列表 `badge()` 出「原版」、详情硬编码 + // 「Vanilla」)。现在两边都走 `LoaderInfo::badge()` 这一个入口;等 i18n 落地后由词表统一, + // 届时两处会同时变——这正是要的。 + let loader = instance.loader.badge(); + + Detail { + id: instance.id.clone(), + name: instance.name.clone(), + subtitle: supporting_line(instance), + meta: vec![ + meta("VERSION", &instance.mc_version), + meta("LOADER", &loader), + meta("ISOLATION", &instance.isolation), + meta("VERSION ID", &instance.version_id), + meta( + "FILES", + if instance.version_present { + "Present on disk" + } else { + "Missing" + }, + ), + meta("LAST PLAYED", &last_played(instance.last_played.as_deref())), + ], + // 真快照没有「正文描述」这种字段(偏离 #9):演示数据才有正文段 + note: None, + status: runtime_status(&instance.runtime), + can_launch: matches!( + instance.runtime, + InstanceRuntime::Idle | InstanceRuntime::Failed { .. } + ), + can_stop: matches!( + instance.runtime, + InstanceRuntime::Busy { .. } | InstanceRuntime::Running { .. } + ), + } +} + +fn meta(label: &str, value: &str) -> Meta { + Meta { + label: label.to_string(), + value: value.to_string(), + } +} + +/// ISO-8601(`2026-10-29T19:22:00Z`)→ `2026-10-29 19:22 UTC`;`None` = 从未启动。 +/// +/// 只做切片不做时区换算:core 给的是 UTC,`Z` 就是它的时区;本地时区换算等 i18n/时间格式 mission。 +/// 非 ASCII 或长度不足时原样返回(不 panic、不猜)。 +pub(super) fn last_played(raw: Option<&str>) -> String { + let Some(text) = raw else { + return "Never".to_string(); + }; + match (text.get(0..10), text.get(11..16)) { + (Some(date), Some(time)) if date.starts_with(|c: char| c.is_ascii_digit()) => { + format!("{date} {time} UTC") + } + _ => text.to_string(), + } +} diff --git a/crates/app-iced/src/app/view/tests.rs b/crates/app-iced/src/app/view/tests.rs index 5ff06d7..4b6d874 100644 --- a/crates/app-iced/src/app/view/tests.rs +++ b/crates/app-iced/src/app/view/tests.rs @@ -1,7 +1,10 @@ //! `app/view.rs` 的守门测试(rebase 到 5adac23 之后 view.rs 贴 400 行上限,按「测试独立成文件」拆出)。 use super::*; -use unlucky_presenter::dto::{InstanceSummary, LoaderInfo, LoaderKind, Progress}; +// 拆分后 `view.rs` 不再 import 这三个(它们只被搬走的实例页转换用),用到的地方自己引 +use unlucky_presenter::dto::{ + InstanceRuntime, InstanceSummary, LaunchStage, LoaderInfo, LoaderKind, Progress, +}; use unlucky_presenter::mock_data::mock_instances; fn instance(runtime: InstanceRuntime) -> InstanceSummary {