diff --git a/crates/app-iced/src/pages/instances/saves/model.rs b/crates/app-iced/src/pages/instances/saves/model.rs index 1b3e28d..ede0ce1 100644 --- a/crates/app-iced/src/pages/instances/saves/model.rs +++ b/crates/app-iced/src/pages/instances/saves/model.rs @@ -196,9 +196,9 @@ pub fn from_state(state: &SavesState, catalog: Catalog) -> Saves { /// | 快照里的状态 | 框里 | 副文案 | /// |---|---|---| /// | `Ready`(像素自洽) | 真图 | `大小 · 时间` | -/// | `Ready`(像素长度不对) | 「解不出这张图」 | 同一句 + 原因(防御性:壳层不该拿到坏像素) | -/// | `NotGenerated` | 「未生成预览」 | 同一句(超预算没轮到) | -/// | `Failed` | 具名原因 | 原因 + OS/解码器原文(有的话) | +/// | `Ready`(像素长度不对) | 「解不出这张图」 | `大小 · 时间`(防御性:壳层不该拿到坏像素) | +/// | `NotGenerated` | 「未生成预览」 | `大小 · 时间`(超预算没轮到) | +/// | `Failed` | 具名原因(太大 / 读不出 / 解不出) | `大小 · 时间`(原文留在 DTO 里,不上墙) | fn shot_cell(shot: &ScreenshotView, texts: Texts) -> Shot { let stamp = rows::shot_detail(shot.bytes, shot.modified_ms, texts); match &shot.preview { @@ -212,31 +212,27 @@ fn shot_cell(shot: &ScreenshotView, texts: Texts) -> Shot { }, // 像素长度与宽高对不上:壳层不该拿到这种数据,但拿到了也不能画成花屏。 ScreenshotPreview::Ready(_) => { - missing(shot, texts.saves_preview_reason(ScreenshotThumbKind::Decode), "") + missing(shot, texts.saves_preview_reason(ScreenshotThumbKind::Decode), stamp) + } + ScreenshotPreview::NotGenerated => { + missing(shot, texts.saves_preview_none(), stamp) + } + ScreenshotPreview::Failed(failure) => { + missing(shot, texts.saves_preview_reason(failure.kind), stamp) } - ScreenshotPreview::NotGenerated => missing(shot, texts.saves_preview_none(), ""), - ScreenshotPreview::Failed(failure) => missing( - shot, - texts.saves_preview_reason(failure.kind), - failure.detail.as_str(), - ), } } -/// 「没有可画的像素」那一格:框里一句短话,格子下面把原因说全。 +/// 「没有可画的像素」那一格:**框里说为什么**(短句),**格子下面照旧是大小与时间**。 /// -/// 短话与副文案**分开**是刻意的:`160×90` 的框里塞不下 OS 原文,而原文正是用户能自己动手的 -/// 那一半信息(权限 / 被占用 / 文件没了),所以它去副文案那一行(会换行,不会丢)。 -/// **不靠字符串切分**去拆两句:切分遇到「原因本身带括号」就会切错(这条踩过)。 -fn missing(shot: &ScreenshotView, note: &str, detail: &str) -> Shot { - let caption = if detail.is_empty() { - note.to_string() - } else { - format!("{note}({detail})") - }; +/// 为什么原因不进副文案:`160px` 宽的格子里,"原因 + OS 原文"会摊成三行,把墙变得比图还吵; +/// 而 `ScreenshotThumbFailure::detail`(`9437184 > 8388608 字节` / `Access is denied.`)留在 +/// DTO 里,是排障用的原文,不是给墙看的正文。D54 第 6 条要的是「逐条、结构化、不是静默」—— +/// 那一句在框里,每条各说各的。 +fn missing(shot: &ScreenshotView, note: &str, stamp: String) -> Shot { Shot { name: shot.file.clone(), - detail: caption, + detail: stamp, preview: ShotPreview::Missing { note: note.to_string(), }, diff --git a/crates/app-iced/src/pages/instances/saves/wall.rs b/crates/app-iced/src/pages/instances/saves/wall.rs index dcdeaeb..11ec47f 100644 --- a/crates/app-iced/src/pages/instances/saves/wall.rs +++ b/crates/app-iced/src/pages/instances/saves/wall.rs @@ -23,11 +23,19 @@ //! 不拉伸(`ContentFit::Contain` 默认)。占位框反而钉死 `160×90`——它是"这里本该有一张图"的 //! 占位,尺寸要可预测,不能让一句话把它撑变形。 //! -//! ## 一行放几格 +//! ## 一行放几格(这条是**量出来的**,不是拍的) //! -//! [`PER_ROW`] = 3:`3 × 160 + 2 × 12 = 504px`。详情列在 1180 宽的窗口里内宽约 700,放 4 格 -//! (676px)会贴边;窄窗口(<840 时详情整页替换)只剩约 500 内宽,3 格是**同时能站住两档**的 -//! 那个数。改它之前先看 `DevDocs/artifacts/m251/` 的两张图。 +//! 详情列的宽度由 `pages::layout` 的 `FrameMetrics::column` 定,而它 `≤ measure_cap_px()` +//! (1180 窗宽下 = 424,见 `layout.rs` 的文档)。卡片还有左右内边距 ⇒ 实际内宽约 **370**。 +//! +//! 于是:**3 格 = 3 × 160 + 2 × 12 = 504px 装不下**(M251 第一次出图实测:第 3 格被挤成一条 +//! 竖条,`未生成预览` 四个字竖着排)。2 格 = `2 × 160 + 12 = 332px`,与同一列里元信息卡的 +//! 两列栅格(`META_CARD_MIN_WIDTH × 2 + META_GAP = 332`)**同宽**——这不是巧合,是同一列里 +//! 已经成立的节奏。 +//! +//! 代价写在明处:60 张 = 30 行,要滚很久。换 3 列只能把格子缩到 ~112px(图更小),D54 的 +//! `THUMBNAIL_MAX_EDGE = 160` 也就白解了;要真正做到「宽度自适应列数」得引一个栅格控件, +//! 那是新依赖,不在本 mission 的取舍里。 use iced::widget::{column, container, image, row}; use iced::{Alignment, Background, Element, Length}; @@ -49,8 +57,8 @@ const THUMB_WIDTH: f32 = 160.0; /// 占位框的高(16:9,与常见的截图宽高比一致)。 const THUMB_HEIGHT: f32 = 90.0; -/// 一行放几格(理由见模块文档)。 -const PER_ROW: usize = 3; +/// 一行放几格(= 同一列里元信息卡的两列栅格;理由见模块文档)。 +const PER_ROW: usize = 2; /// 整面墙(`saves.rs` 的 `section` 调;空列表不该走到这里)。 pub(super) fn grid<'a>(ui: &'a Ui, shots: &'a [Shot]) -> Element<'a, Message> { diff --git a/crates/app-iced/src/pages/instances/tests/saves.rs b/crates/app-iced/src/pages/instances/tests/saves.rs index d2aaaef..da09f64 100644 --- a/crates/app-iced/src/pages/instances/tests/saves.rs +++ b/crates/app-iced/src/pages/instances/tests/saves.rs @@ -315,15 +315,17 @@ fn every_preview_state_gets_its_own_caption() { .to_string() } ); - // 失败的副文案要带 OS/解码器原文(只进括号),没生成的那格只要一句短话。 - assert_eq!( - saves.screenshots[2].detail, - format!( - "{}(9437184 > 8388608 字节)", - texts.saves_preview_reason(ScreenshotThumbKind::TooLarge) - ) + // 副文案三格一样是「大小 · 时间」:原因在框里,不把墙变得比图还吵(原文留在 DTO 里)。 + assert!( + saves.screenshots[1].detail.starts_with("4.0 KB"), + "没生成的那格也要有大小与时间:{:?}", + saves.screenshots[1].detail + ); + assert!( + saves.screenshots[2].detail.starts_with("9.0 MB"), + "失败的那格同样保留元数据:{:?}", + saves.screenshots[2].detail ); - assert_eq!(saves.screenshots[1].detail, texts.saves_preview_none()); } /// 像素长度与宽高对不上时**不许**建句柄(`Handle::from_rgba` 不校验长度 ⇒ 画出来是花屏)。 diff --git a/crates/app-iced/src/pages/instances/text/saves.rs b/crates/app-iced/src/pages/instances/text/saves.rs index de313f5..2201abb 100644 --- a/crates/app-iced/src/pages/instances/text/saves.rs +++ b/crates/app-iced/src/pages/instances/text/saves.rs @@ -49,7 +49,10 @@ impl Texts { pub(in crate::pages::instances) fn saves_no_screenshots(self) -> &'static str { self.text(Key::InstancesSavesNoScreenshots) } - /// 「有图标」:只判 `icon.png` 存不存在(不解码,缩略图墙不在 U3 范围内)。 + /// 「有图标」:只判 `icon.png` 存不存在。 + /// + /// M251 之后**仍未解**世界图标——缩略图墙只覆盖 `screenshots/`(D54 的范围)。这行字眼 + /// 原来写的是「缩略图墙不在 U3 范围内」,那句话今天已经失真(墙做了),改成现状描述。 pub(in crate::pages::instances) fn saves_world_icon(self) -> &'static str { self.text(Key::InstancesSavesWorldIcon) }