feat(presenter): 多根发现(配置根 + 官方启动器标准位置),实例按自己的根解析存档/模组/启动(M274)

This commit is contained in:
AskaEth committed 2026-11-12 19:55:51 +08:00
1 parent dfc0f4bcb3
commit dc8323531b
20 files changed
+943 -121

No files matched your search

@@ -116,6 +116,10 @@ pub(super) fn section<'a>(
body = body.push(note(ui, texts.saves_loading()));
return frame::card(ui, body);
}
// 来源那一行(M274):多根之后「这个实例的存档到底在哪个目录」必须看得见。
if saves.has_view && !saves.source.is_empty() {
body = body.push(note(ui, &saves.source));
}
if let Some(error) = saves.error.as_deref() {
body = body.push(frame::text_role(
ui,
@@ -86,6 +86,13 @@ pub struct Saves {
pub retryable: bool,
/// 手里有没有一份**快照**(`false` = 只拿到了失败,"还没有世界"那几句不许画)。
pub has_view: bool,
/// 来源那一行(M274 多根):`存档目录:<绝对路径>`,**在这一层拼好**。
///
/// 便携目录与官方启动器是两个不同的根,两个根里都可能有一个叫 `A` 的实例、各有一批世界。
/// 世界名分辨不出来源,所以把路径原样摆出来(也是排障时要问的第一件事)。
/// 为什么不是页面里现拼:iced 的 `Element` 只借这一帧的数据,`view()` 返回的树借的是局部
/// `String`,一出栈就 `E0515`(与 [`Saves::count`] 同一条理由)。
pub source: String,
/// 两个目录都不存在(新实例的常态:还没进过游戏)。
pub missing: bool,
/// 世界行。
@@ -162,6 +169,7 @@ pub fn from_state(state: &SavesState, catalog: Catalog) -> Saves {
};
if let Some(view) = &state.view {
out.has_view = true;
out.source = texts.saves_source(&view.dir);
out.missing = !view.exists && !view.screenshots_exists;
// 计数直接问快照(`view.world_count()`),不从刚装好的 `out.worlds` 再数一遍——
// M224 出图实测过"先算计数后装列表"画出 `0 启用 · 0 禁用` 的那种顺序错,这里从根上绕开。
@@ -253,5 +253,39 @@ fn millisecond_stamps_are_rendered_in_utc() {
);
}
/// **来源那一行**(M274 多根):必须是这一份快照读的那个目录,两种语言都说出来。
///
/// 为什么非要有这一行:便携目录与官方启动器是两个不同的根,两个根里都可能有一个叫 `A` 的实例。
/// 世界名分辨不出来源,只有路径能分辨(也是排障时要问的第一件事)。
///
/// 抓得住:来源行拿了别的实例 / 别的根的路径(界面上一模一样的错),或切了根却没跟着变;
/// 抓不住:页面真的把这一行画出来(`Element` 没有内省接口,见模块文档)。
#[test]
fn the_source_line_names_the_folder_that_was_read() {
let mut state = bound_state("a");
state.changed(view("a"));
let zh = from_state(&state, Lang::ZhCn.catalog());
let en = from_state(&state, Lang::En.catalog());
assert!(
zh.source.contains("C:/mc/saves"),
"来源行没带目录:{:?}",
zh.source
);
assert!(en.source.contains("C:/mc/saves"), "{:?}", en.source);
assert_ne!(zh.source, en.source, "切换语言时这一行要变");
// 换一个根(另一个 `.minecraft`)⇒ 这一行跟着变(不许留在旧路径上)。
let mut other = view("a");
other.dir = "D:/official/.minecraft/saves".to_string();
let mut state = bound_state("a");
state.changed(other);
let moved = from_state(&state, Lang::ZhCn.catalog());
assert!(
moved.source.contains("D:/official"),
"换了根之后来源行没跟着变:{:?}",
moved.source
);
}
mod preview;
mod write;
@@ -37,6 +37,10 @@ impl Texts {
pub(in crate::pages::instances) fn saves_missing_dir(self) -> &'static str {
self.text(Key::InstancesSavesMissingDir)
}
/// 「这一份列表读自哪个目录」(M274 多根:世界名分辨不出来源,绝对路径可以)。
pub(in crate::pages::instances) fn saves_source(self, dir: &str) -> String {
self.0.render(Key::InstancesSavesSource, &[("dir", dir)])
}
pub(in crate::pages::instances) fn saves_worlds(self) -> &'static str {
self.text(Key::InstancesSavesWorlds)
}