feat(saves): 实例存档 / 截图的只读列表与打开文件夹(M235 STOREMVP)

用户诉求是 PCL 差距四项里的「存档截图 / 导入」,M230 审计确认三层全缺。塔裁决 U3
把这一版钉成「只读列表 + 打开文件夹」,不做缩略图墙(缩略图要引图片解码依赖,而
AGENTS §2 只允许 palette-extract 引 image 类依赖;截图/导入产物形态也没定型)。

core
- `game::saves`(新):只读列 `<run_dir>/saves`(世界目录)与 `<run_dir>/screenshots`
  (图片文件)。只调 symlink_metadata / read_dir / metadata,不建目录、不写文件、不读
  文件内容(`icon.png` 只判存在)。目录不存在 ⇒ 空列表不是错误;存在但不可用(符号
  链接 / 不是目录 / 读不了)⇒ 具名错误。条目级异常逐条登记原因,不静默丢。
- `platform::opener`(新):`open_dir` 是**目录**打开器的唯一收敛点(explorer / open /
  xdg-open),内部复用 `child::quiet`,cfg 分支只此一处。塔明确要求按 AGENTS §6 放
  platform 而不是业务模块,并要一条可执行判据防退化
  (`the_launcher_literals_live_here_only` 扫 `crates/core/src`:除本文件外不许出现
  `"explorer"` / `"xdg-open"` 字面量)。

presenter
- `dto::saves`:SavesCommand(List / OpenFolder)、SavesListView、SavesFailureView
  (结构 + OS 原文,不拼中文)、`SavesListView` 的计数助手。
- `host::saves`:走 `job::spawn`,UI 只收快照;运行目录仍是
  `isolation::derive_game_dir` 的同一个答案(与启动 / mod 列表同源,**不写盘**)。
  「打开文件夹」打开哪个目录由 host 定:`saves/` → `screenshots/` → 运行目录。
  失败分三类(InstanceUnavailable / DirUnusable / OpenFailed),可重试。
- `state::saves`:快照整体替换、迟到结果丢弃、失败保留绑定(重试才有意义)。
- 接线:CoreEvent::Saves / AppCommand::Saves / HostTask::Saves。

app-iced
- `pages::instances::saves`:详情页 SAVES 卡片(MODS 之后、启动参数之前;**不套第二层
  滚动条**,整列那一条是唯一入口)。世界行带 UTC 时间与「有图标」,截图行带大小与时间;
  第一行固定画「这一版只读」(U3 边界让用户看得见)。读盘失败画一条红字 + 「重试」;
  打开文件夹失败**不画**重试(那个动作的重试就是再点一次按钮)。
- 接线:Message::{ReloadSaves, OpenSavesFolder}、`sync_saves`(与 `sync_mods` 同一判据)、
  Model::saves、`saves_shot` 出图夹具(ULL_SAVES_FIXTURE=1)。

i18n
- `InstancesSaves*` 20 条中英成对,catalog 与金表在**同一条锚点**后追加、逐字节一致。

证据
- core 只读判据:`game::saves::tests` 在真盘建世界/截图,调用前后各拍一次
  (相对路径, 是否目录, 字节数, mtime)指纹并逐条比对;另有用例钉「目录缺失不建目录」。
- presenter:`host::saves::tests` 真盘端到端(custom 档位派生运行目录 → 快照)。
- GUI:`--software-renderer` 1180×760 浅/深各一张 + 1180×1280 两张「整块卡片可见」
  (1180×760 下 SAVES 卡片位于折叠线附近,只能看到卡头;第二对用 ULL_WINDOW_SIZE
  把窗口拉高,口径与 M98 的矩阵一致),落 `DevDocs/artifacts/M235/`。

没做的(U3 边界内,见 handoff):缩略图墙、导入、删除/重命名、`level.dat` 的
LevelName 解析(世界名暂时就是目录名)。
This commit is contained in:
AskaEth committed 2026-11-11 06:00:27 +08:00
1 parent 3789cb7e61
commit f217d83680
36 files changed
+2702 -17

No files matched your search

+20
View File
@@ -105,6 +105,26 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
("InstancesModsReasonNotFile", "that is not a regular file", "那不是普通文件"),
("InstancesModsReasonCollision", "a file with that name already exists", "同名文件已存在"),
("InstancesModsReasonRename", "the rename failed", "改名失败"),
("InstancesSavesTitle", "SAVES", "存档"),
("InstancesSavesReadOnly", "Read-only in this version: nothing here is moved or deleted.", "这一版只读:这里不移动、也不删除任何东西。"),
("InstancesSavesCount", "{worlds} worlds · {shots} screenshots", "{worlds} 个世界 · {shots} 张截图"),
("InstancesSavesLoading", "Reading saves/…", "正在读取 saves/…"),
("InstancesSavesEmpty", "No worlds or screenshots yet — play once and they show up here.", "还没有世界或截图——玩一次就会出现在这里。"),
("InstancesSavesMissingDir", "This instance has no saves/ or screenshots/ directory yet.", "这个实例还没有 saves/ 或 screenshots/ 目录。"),
("InstancesSavesWorlds", "WORLDS", "世界"),
("InstancesSavesScreenshots", "SCREENSHOTS", "截图"),
("InstancesSavesNoWorlds", "No worlds yet.", "还没有世界。"),
("InstancesSavesNoScreenshots", "No screenshots yet.", "还没有截图。"),
("InstancesSavesWorldIcon", "has icon", "有图标"),
("InstancesSavesTimeUnknown", "time unknown", "时间未知"),
("InstancesSavesSkipped", "{count} entries skipped", "跳过 {count} 项"),
("InstancesSavesOpenFolder", "Open saves folder", "打开存档文件夹"),
("InstancesSavesRetry", "Retry", "重试"),
("InstancesSavesFailed", "Could not read saves/: {reason}", "读取 saves/ 失败:{reason}"),
("InstancesSavesOpenFailed", "Could not open the folder: {reason}", "打开文件夹失败:{reason}"),
("InstancesSavesReasonDir", "the directory is not usable", "目录不可用"),
("InstancesSavesReasonInstance", "the instance is gone", "实例已不存在"),
("InstancesSavesReasonOpen", "the file manager did not start", "文件管理器没起来"),
];
/// 金表的**全量**行(本文件 → [`tail`] → [`tail_ai`] → [`tail2`],顺序即 `Key::ALL`)。
+24
View File
@@ -154,6 +154,30 @@ macro_rules! instances_entries {
InstancesModsReasonNotFile => ("that is not a regular file", "那不是普通文件"),
InstancesModsReasonCollision => ("a file with that name already exists", "同名文件已存在"),
InstancesModsReasonRename => ("the rename failed", "改名失败"),
// ── 存档区(M235 · STOREMVP):**只读**列 `saves/` 与 `screenshots/` + 打开文件夹。
// U3 的边界写在 `InstancesSavesReadOnly` 里,页面固定画出来——用户点开就看见
// 「这一版只读」,不会以为漏了删除按钮。`saves/` / `screenshots/` 是**目录名**,
// 两套语言都原样保留(与 MODS 区的 `mods` 同一条口径)。
InstancesSavesTitle => ("SAVES", "存档"),
InstancesSavesReadOnly => ("Read-only in this version: nothing here is moved or deleted.", "这一版只读:这里不移动、也不删除任何东西。"),
InstancesSavesCount => ("{worlds} worlds · {shots} screenshots", "{worlds} 个世界 · {shots} 张截图"),
InstancesSavesLoading => ("Reading saves/…", "正在读取 saves/…"),
InstancesSavesEmpty => ("No worlds or screenshots yet — play once and they show up here.", "还没有世界或截图——玩一次就会出现在这里。"),
InstancesSavesMissingDir => ("This instance has no saves/ or screenshots/ directory yet.", "这个实例还没有 saves/ 或 screenshots/ 目录。"),
InstancesSavesWorlds => ("WORLDS", "世界"),
InstancesSavesScreenshots => ("SCREENSHOTS", "截图"),
InstancesSavesNoWorlds => ("No worlds yet.", "还没有世界。"),
InstancesSavesNoScreenshots => ("No screenshots yet.", "还没有截图。"),
InstancesSavesWorldIcon => ("has icon", "有图标"),
InstancesSavesTimeUnknown => ("time unknown", "时间未知"),
InstancesSavesSkipped => ("{count} entries skipped", "跳过 {count} 项"),
InstancesSavesOpenFolder => ("Open saves folder", "打开存档文件夹"),
InstancesSavesRetry => ("Retry", "重试"),
InstancesSavesFailed => ("Could not read saves/: {reason}", "读取 saves/ 失败:{reason}"),
InstancesSavesOpenFailed => ("Could not open the folder: {reason}", "打开文件夹失败:{reason}"),
InstancesSavesReasonDir => ("the directory is not usable", "目录不可用"),
InstancesSavesReasonInstance => ("the instance is gone", "实例已不存在"),
InstancesSavesReasonOpen => ("the file manager did not start", "文件管理器没起来"),
} }
};
}