release: v0.2.5
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run

This commit is contained in:
AskaEth committed 2026-11-12 12:06:59 +08:00
commit 0522a41a52
1587 files changed
+323674

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
//! 一张截图的**预览**(M251 · SAVESHOTS,D54 第 2/6 条)。
//!
//! ## 像素为什么走 DTO
//!
//! D54 定「**不落盘缩略图**」:写 PNG 要编码器,而编码器只为一份磁盘缓存引入不划算。于是像素
//! 就直接躺在快照里跟着 IPC 面走,由壳层用 `iced::widget::image::Handle::from_rgba` 建句柄。
//! 代价是快照变大(60 张 × 160² × 4B ≈ 6MB),收益是零编码、零临时文件、零清理。
//!
//! ## 三种状态,一种都不许静默
//!
//! | 状态 | 什么时候 | 界面 |
//! |---|---|---|
//! | [`ScreenshotPreview::Ready`] | 解出来了 | 缩略图 |
//! | [`ScreenshotPreview::NotGenerated`] | 本次刷新超出 60 张上限(按 mtime 倒序只解前 60) | 「未生成预览」占位 |
//! | [`ScreenshotPreview::Failed`] | 超过 8 MiB / 读不出来 / 解不出来 | 占位 + **逐条**原因 |
//!
//! **不拼人读文案**(M161):这里只有结构与 [`ScreenshotThumbKind`],句子由 app 层按 i18n key 现求。
//!
//! ## 为什么失败也带 `detail`
//!
//! 「读不出来」有五种完全不同的原因(权限 / 被占用 / 文件已删 / 是目录 / 盘掉了),用户能自己
//! 处理的那几种全靠这句话。原文只进括号,不进主句(与 [`super::SavesFailureView`] 同一条口径)。
use serde::{Deserialize, Serialize};
/// 一张截图的预览状态。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScreenshotPreview {
/// 还没生成:本次刷新超出上限(按 mtime 倒序只解前 60 张,上限常量在
/// `crate::host::saves::thumbs::MAX_PER_REFRESH`——策略住在解码那一侧,DTO 只表达状态)。
NotGenerated,
/// 解出来了(RGBA8,行主序,`width × height × 4`)。
Ready(ThumbnailView),
/// 没解出来(逐条可见的原因 + 原文)。
Failed(ScreenshotThumbFailure),
}
impl ScreenshotPreview {
/// 这一拍有没有可画的像素。
#[must_use]
pub fn thumbnail(&self) -> Option<&ThumbnailView> {
match self {
Self::Ready(view) => Some(view),
Self::NotGenerated | Self::Failed(_) => None,
}
}
}
/// 一张缩略图的像素(**未落盘**,见模块文档)。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThumbnailView {
/// 宽(像素)。
pub width: u32,
/// 高(像素)。
pub height: u32,
/// RGBA8 字节,行主序,长度必须是 `width × height × 4`。
pub pixels: Vec<u8>,
}
impl ThumbnailView {
/// 像素数与宽高对得上吗(壳层建 `Handle` 之前**必须**成立:`from_rgba` 不校验长度,
/// 长度不对就是一张花屏图或者一次 panic 后的空窗)。
#[must_use]
pub fn is_consistent(&self) -> bool {
let expected = u64::from(self.width) * u64::from(self.height) * 4;
expected == self.pixels.len() as u64
}
}
/// 一张截图的预览为什么没出来(**每一种在 UI 上都有一句话**)。
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScreenshotThumbFailure {
/// 大类。
pub kind: ScreenshotThumbKind,
/// 底层原文(OS 错误 / 解码器原文);只进括号,不进主句。
pub detail: String,
}
/// 预览失败的大类。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ScreenshotThumbKind {
/// 单张超过解码上限(8 MiB):**不解码**,直接跳过(D54 第 5 条)。
TooLarge,
/// 读不出来(不存在 / 无权限 / 被占用)。
Unreadable,
/// 读到了但解不出来(坏 PNG / 不支持的格式)。
Decode,
}