diff --git a/crates/app-iced/src/pages/download/options/model.rs b/crates/app-iced/src/pages/download/options/model.rs index 0a50dd6..a8e91f2 100644 --- a/crates/app-iced/src/pages/download/options/model.rs +++ b/crates/app-iced/src/pages/download/options/model.rs @@ -215,6 +215,12 @@ pub struct OptionsView { pub instance_name: String, /// 不兼容 / 取不到的原因(`Some` ⇒ 确认键按下不去) pub problem: Option, + /// 加载器相关的**中性说明**(不是错误:不套错误色、不禁用确认键)。 + /// + /// 目前只有 LiteLoader 有:下载页只能装「原版 + 加载器」这一层,叠在 Forge 上要走命令行 + /// (M222 的边界,见 [`Key::DownloadOptionsLiteLoaderVanillaOnly`])。把它塞进 `problem` + /// 会让用户以为这个组合不能装、还会按不下去确认键,所以单独一条。 + pub note: Option, /// 本地已装(确认键给「已安装」禁用态) pub installed: bool, /// 现在能不能点「安装」 @@ -276,6 +282,13 @@ impl OptionsView { (Ok(()), _) => None, }; + // 中性说明:只解释「这一页能做到哪一层」,不参与确认键判定。 + let note = (loader == Loader::LiteLoader).then(|| { + catalog + .text(Key::DownloadOptionsLiteLoaderVanillaOnly) + .to_string() + }); + // 四条同时成立才按得下去:不兼容没有、列表真的就位(原版不需要列表)、没装过。 // 「列表没到位」既包括 `Loading` 也包括 `Failed` 与空表——它们都已经在 `problem` 里说了原因。 let ready = loader == Loader::Vanilla @@ -289,6 +302,7 @@ impl OptionsView { loader_version, instance_name: draft.instance_name.clone(), problem, + note, installed: row.installed, can_confirm, } diff --git a/crates/app-iced/src/pages/download/options/tests.rs b/crates/app-iced/src/pages/download/options/tests.rs index 016886e..fdc769d 100644 --- a/crates/app-iced/src/pages/download/options/tests.rs +++ b/crates/app-iced/src/pages/download/options/tests.rs @@ -272,3 +272,61 @@ fn installed_versions_cannot_be_confirmed_again() { assert!(options.installed); assert!(!options.can_confirm); } + +/// 中性说明(M222):只有 LiteLoader 带「这里只叠原版」那条,且它**不影响**确认键。 +/// +/// 抓得住:说明被塞进 `problem`(会让用户以为不能装、确认键按不下去);或者说明跑到别家 +/// 加载器下面(Forge/Fabric 没有这条边界)。 +#[test] +fn liteloader_carries_a_neutral_note_without_blocking_confirm() { + let state = state_with( + LoaderKind::LiteLoader, + "1.12.2", + Ok(vec!["1.12.2-SNAPSHOT"]), + ); + let options = OptionsView::of( + &row("1.12.2", VersionKind::Release), + &state, + &draft(Loader::LiteLoader), + Lang::En.catalog(), + ); + let note = options.note.as_deref().expect("LiteLoader 应带中性说明"); + assert!(note.contains("vanilla"), "{note}"); + assert!( + note.contains("--loader liteloader"), + "要给出可执行的下一步:{note}" + ); + assert!(options.problem.is_none(), "中性说明不能变成错误"); + assert!(options.can_confirm, "说明不该禁用确认键"); + + // 中文目录同样给中文说明,且仍然不禁用确认键。 + let zh = state_with( + LoaderKind::LiteLoader, + "1.12.2", + Ok(vec!["1.12.2-SNAPSHOT"]), + ); + let options = OptionsView::of( + &row("1.12.2", VersionKind::Release), + &zh, + &draft(Loader::LiteLoader), + Lang::ZhCn.catalog(), + ); + let note = options.note.as_deref().expect("中文也要有说明"); + assert!(note.contains("原版"), "{note}"); + assert!(options.can_confirm); + + // 别家加载器不带这条说明。 + for loader in [Loader::Vanilla, Loader::Fabric, Loader::Forge] { + let state = state_with(loader.kind(), "1.20.1", Ok(Vec::new())); + let options = OptionsView::of( + &row("1.20.1", VersionKind::Release), + &state, + &draft(loader), + Lang::En.catalog(), + ); + assert!( + options.note.is_none(), + "{loader:?} 不该带 LiteLoader 的说明" + ); + } +} diff --git a/crates/app-iced/src/pages/download/options/view.rs b/crates/app-iced/src/pages/download/options/view.rs index c153ebf..32c4de4 100644 --- a/crates/app-iced/src/pages/download/options/view.rs +++ b/crates/app-iced/src/pages/download/options/view.rs @@ -93,6 +93,18 @@ pub(crate) fn section<'a>( .spacing(ui.space(Space::X2)) .width(Length::Fill); + // 中性说明(M222):只解释这一页能装到哪一层,不套错误色、不影响确认键。 + // 换行由 iced 的文本布局负责,长句不会横向溢出;但**视觉验收仍需截图确认**(D30)。 + if let Some(note) = &options.note { + body = body.push(frame::text_role( + ui, + TypeRole::BodySmall, + false, + note.as_str(), + ui.color(Role::OnSurfaceVariant), + )); + } + // 「加载器版本」这一段只在**真该有列表**时出现:原版没有、静态不兼容没有(原因在下面那行)、 // 取不到也没有(同样在原因行)。留一个空标题只会让人以为界面缺了一块。 if show_versions(options) { diff --git a/crates/i18n/src/catalog/download.rs b/crates/i18n/src/catalog/download.rs index b6614e4..ea82f24 100644 --- a/crates/i18n/src/catalog/download.rs +++ b/crates/i18n/src/catalog/download.rs @@ -99,6 +99,12 @@ macro_rules! download_entries { "{loader} needs Minecraft {min} or newer", "{loader} 需要 Minecraft {min} 或更新" ), + // M222:下载页只做「原版 + 加载器」一层,Forge 父链只有命令行能走。中性说明, + // 不是错误(不禁用确认键、不套错误色)——所以它不走 `problem` 那条通道。 + DownloadOptionsLiteLoaderVanillaOnly => ( + "LiteLoader stacks on vanilla only — for Forge, run: ull install --loader liteloader", + "LiteLoader 只叠在原版上 —— 要叠 Forge 请运行:ull install --loader liteloader" + ), DownloadCancelled => ( "Install cancelled — finished files are kept, the instance was not created", "安装已取消 —— 已下好的文件保留,实例未登记" diff --git a/crates/i18n/src/catalog/golden/tail_ai.rs b/crates/i18n/src/catalog/golden/tail_ai.rs index 4a120dc..e99f673 100644 --- a/crates/i18n/src/catalog/golden/tail_ai.rs +++ b/crates/i18n/src/catalog/golden/tail_ai.rs @@ -106,6 +106,7 @@ pub(super) const ROWS: &[(&str, &str, &str)] = &[ ("DownloadOptionsInstanceNameHint", "Empty = use the version id", "留空就用版本号"), ("DownloadOptionsIncompatible", "{loader} has no builds for {version}", "{loader} 没有适配 {version} 的构建"), ("DownloadOptionsNeedsAtLeast", "{loader} needs Minecraft {min} or newer", "{loader} 需要 Minecraft {min} 或更新"), + ("DownloadOptionsLiteLoaderVanillaOnly", "LiteLoader stacks on vanilla only — for Forge, run: ull install --loader liteloader", "LiteLoader 只叠在原版上 —— 要叠 Forge 请运行:ull install --loader liteloader"), ("DownloadCancelled", "Install cancelled — finished files are kept, the instance was not created", "安装已取消 —— 已下好的文件保留,实例未登记"), ("LogsSubtitleIdle", "Nothing is running right now", "当前没有运行中的实例"), ("LogsStatus", "{instance} · {when} · {outcome}", "{instance}|{when}|{outcome}"),