Files
Unlucky-Launcher/crates/app-iced/src/pages/nav.rs
T
AskaEth bbf96de9b3 feat(app-iced): M108 下载页显示名改成「版本管理 / Version management」
用户令(2026-11-04):当前下载页改名叫版本管理,下载页只负责下载整合包和 Mod/光影。
本 mission 只改显示层,标识符(Route::Download / Screen::Download / NavItem::Download /
文件名)一个都没动,标识符改名另登 finding 后置。

i18n(catalog/nav.rs + catalog/download.rs,金表同步):
- NavDownload = Versions/版本管理 —— 88px 收起轨用短名
- NavDownloadExpanded = Version management/版本管理 —— 240px 展开抽屉用全名
- DownloadTitle = Version management/版本管理 —— 页标题
- DownloadSubtitle = Manage versions and loaders: install, upgrade, repair/
  管理版本与加载器:安装、升级、修复
拆两条 key 的理由:18 字符英文 ≈101px 塞不进 88px 轨(实测两通道都被裁:产品通道只显示
Version,组件版轨被切成 Version manage);中文 4 字实测 48.0px(四种 CJK 字体同值)放得下,
所以中文两条同值、不缩。

- pages/nav.rs:NAV 第 3 列 &str → Key;label_key(label, expanded) 是两态切换点
  (用 bool 参数而非 enum:240 态在产品里还没有渲染路径,枚举变体永远构造不到会被
  -D warnings 的 dead_code 拦下)。
- shell/nav.rs:NavItem::Download 显示 Versions(产品路径的轨;i18n 接线归 M106,
  本 mission 只换文案值)。
- 守门:pages/tests.rs 断言导航/页标题取值 + 两态切换 + 目录里凡指向这一页的文案都不许再说
  「下载」(豁免 DownloadStageDownloading);shell/nav.rs 断言标签是 Versions 且 ≤12 字符。
2026-11-05 02:54:04 +08:00

205 lines
9.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 导航轨的**部件版**实现:`NAV` 表 + [`rail`](宽 = 令牌 `nav_rail_width` = 88)。
//!
//! ## 两套 rail 的关系(M90 r3 订正,复审 P2-1;M99 把这张表从 `pages/mod.rs` 搬到这里)
//!
//! 产品路径(`app/view.rs`)走 `shell::view_with_content`,导航轨由 **`shell/nav.rs` 的手搓实现**画
//! ——它与 M78 组件几何同值(宽 88、药丸 56×32),复审逐像素量过「r1 产品图(真组件 rail)vs r2
//! 产品图(shell rail)在 y≥48 区域差异为 0」。组件版的 [`rail`] 只服务 `pages::app`
//! (`pages_demo` 出口图、harness)与单测。
//!
//! 两边的守门是分开的:`shell/nav.rs` 自带 indicator 56×32 的单测,`pages/tests.rs` 守的是下面
//! 这张表(表长 5 / 每一项都指向能高亮它的路由 / 第 5 项没有 section)。**将来哪边先漂没人拦**
//! ——统一要泛型化 shell 的消息类型,成本大于收益;在那之前任何改 rail 几何的改动都要两处一起改
//! 并各自跑守门(`pages/mod.rs` 偏离登记 #10)。
use iced::Element;
use unlucky_design::icons::IconKind;
use unlucky_i18n::{Key, Lang};
use unlucky_presenter::router::{NavSection, Route};
use crate::app::components::navigation::drawer::{self, Navigation};
use crate::app::components::navigation::navigation;
use crate::theme::Ui;
use super::Message;
/// 导航轨的固定项:图标 + 文案 key + 目标路由(顺序 = 自上而下)。
///
/// 顺序与 `presenter::router::NavSection` 的声明顺序一致,改这里等于改导航栏。
///
/// 第 3 列是 [`Key`] 而不是字面量(M108):导航文案归目录管,`pages_demo` 出的图与将来的中文
/// 界面读的是同一份表,不会出现「页面翻译了、导航还是硬编码英文」。
///
/// **显示名 ≠ 标识符**:`Key::NavDownload` 这条说的是「版本管理 / Versions」,而它指向的仍是
/// `Route::Download`(用户令只改显示层,标识符改名后置)。
///
/// 表里存的是**收起态(88)的 key**;展开态(240)要对 [`label_key`] 过一道才换全名。
/// 可见性是 `pub(crate)`:`pages/tests.rs` 的三条断言读它(M99 从 `pages/mod.rs` 搬出来时,
/// 它从「模块私有」变成「crate 内可见」,断言一条没动、强度也没降)。
pub(crate) const NAV: [(Option<NavSection>, IconKind, Key, Route); 5] = [
(
Some(NavSection::Instances),
IconKind::Instances,
Key::NavInstances,
Route::Instances,
),
(
Some(NavSection::Download),
IconKind::Download,
Key::NavDownload,
Route::Download,
),
(
Some(NavSection::Logs),
IconKind::Logs,
Key::NavLogs,
Route::Logs,
),
(
Some(NavSection::Settings),
IconKind::Settings,
Key::NavSettings,
Route::Settings,
),
// AI 独立页没有导航高亮项(`Route::nav()` 对它返回 `None`);文案用 M106 给导航项新开的
// `Key::NavAi`(与产品路径的 `shell/nav.rs` 同一条 key,两条 rail 不许各借一个词)。
(None, IconKind::Ai, Key::NavAi, Route::Agent),
];
/// 这一项在当前宽度态下的文案 key。
///
/// `expanded` = D33 的 240 展开抽屉(用全名),`false` = 88 收起轨(用短名)。产品路径**当前只有
/// 88 收起态**,所以调用点只出现 `false`。
///
/// **为什么是 bool 参数而不是 `enum NavWidth { Rail, Drawer }`**:D33 的抽屉还没有渲染路径,
/// 枚举里那个 `Drawer` 变体在产品代码里永远构造不到,`-D warnings` 会按 `dead_code` 把它拦下来
/// (M108 第一版就是这么红的)。参数化之后没有「构造不到的状态」,切换点仍只有这一处。
pub(crate) const fn label_key(label: Key, expanded: bool) -> Key {
if expanded && matches!(label, Key::NavDownload) {
Key::NavDownloadExpanded
} else {
label
}
}
/// 导航轨:**真组件**(`components::navigation` 的 rail 形态,宽 = 令牌 `nav_rail_width` = 88)。
///
/// M84 这里是一条留白带;M90 换成 M78 的 `NavigationRail`,于是「导航项 → 路由」是真的可点。
///
/// 语言暂时固定 [`Lang::En`]:组件版 rail 的调用点只有 `pages/mod.rs` 的 `nav::rail(ui, model.active)`,
/// 那里还没把 `Model.lang` 传进来(`pages/mod.rs` 不在 M108 的 scope 内)。文案本身已经走 [`Key`],
/// 接线时只改这一行——产品路径那边同一笔欠账归 M106(它给 `shell/nav.rs` 接 i18n)。
pub(super) fn rail<'a>(ui: &'a Ui, active: Option<NavSection>) -> Element<'a, Message> {
let catalog = Lang::En.catalog();
let mut group = drawer::NavGroup::new();
for (section, icon, label, route) in NAV {
// 88 收起轨 = 短名(240 展开抽屉才用全名,见 [`label_key`])
let label = label_key(label, false);
let mut item = drawer::NavItem::new(catalog.text(label), icon, Message::Navigate(route));
if section.is_some() && active == section {
item = item.selected();
}
group = group.item(item);
}
navigation(ui, Navigation::rail().group(group))
}
#[cfg(test)]
mod tests {
//! 导航表与文案的守门(M108)。
//!
//! 这条断言原先挂在 `pages/tests.rs`,但那份文件在 M102 合并后是 464 行、撞了「单文件 ≤400 行」
//! 的硬门禁(`components/gate.rs` 按全仓判红),于是按「谁的数据谁守」挪到本文件——`NAV` 表在
//! 这里,守门也在这里,强度一条没降。
use super::*;
/// **M108**:这一页对外叫「版本管理」,代码里仍是 `Route::Download`。
///
/// 用户令(2026-11-04)只要**显示层**改名:全仓重命名标识符会与在途的多条线全面撞车,
/// 所以 `Route::Download` / `NavSection::Download` / `Screen::Download` / 文件名一个都不动。
///
/// 英文有两个不同长度的名字,**是刻意的**(见 `catalog/nav.rs` 的「两条 key」一节):
/// 88px 收起轨放不下 `Version management`(≈101px),用短名 `Versions`;页标题与 240 展开抽屉
/// 用全名。中文两处都是「版本管理」(≈48px,放得下就不缩)。
///
/// **抓得住**:① 导航项或页标题又写回「下载」「Download」;② 88 轨上误用全名(会截断/换行);
/// ③ 副标题退回「只说下载」;④ 中文被「为了对称」砍短。**抓不住**:产品路径的导航轨
/// (`shell/nav.rs` 是另一套手搓实现,它自己有一条同样的断言)、真画出来会不会截断(那要出图)、
/// 中文目录在真机上的字体(截图通道固定 En,见 M108 报告与 finding)。
#[test]
fn the_download_route_is_presented_as_version_management() {
let en = Lang::En.catalog();
let zh = Lang::ZhCn.catalog();
let entry = NAV
.iter()
.find(|entry| entry.3 == Route::Download)
.expect("导航表里有版本管理页那一项");
assert_eq!(entry.2, Key::NavDownload, "导航文案走目录,不写字面量");
// 88 收起轨 = 短名;240 展开与页标题 = 全名(中文两处同值)
assert_eq!(en.text(Key::NavDownload), "Versions");
assert_eq!(en.text(Key::NavDownloadExpanded), "Version management");
assert_eq!(en.text(Key::DownloadTitle), "Version management");
assert_eq!(zh.text(Key::NavDownload), "版本管理");
assert_eq!(zh.text(Key::NavDownloadExpanded), "版本管理");
assert_eq!(zh.text(Key::DownloadTitle), "版本管理");
assert_eq!(
label_key(Key::NavDownload, false),
Key::NavDownload,
"88 收起态必须用短名"
);
assert_eq!(
label_key(Key::NavDownload, true),
Key::NavDownloadExpanded,
"240 展开态换全名"
);
assert_eq!(
label_key(Key::NavLogs, true),
Key::NavLogs,
"其余四项两态同一条 key"
);
// 目录里凡是指向这一页的文案都不许再说「下载」。唯一豁免是 `Downloading files`:
// 那是安装流程里真在传文件的那一步,说的是当时的动作、不是页面身份。
let page_keys = Key::ALL.iter().filter(|key| {
(key.name().starts_with("Download") && **key != Key::DownloadStageDownloading)
|| **key == Key::NavDownload
|| **key == Key::NavDownloadExpanded
});
let mut checked = 0;
for key in page_keys {
assert!(
!en.text(*key).to_ascii_lowercase().contains("download"),
"{} 的英文文案还在说「下载」:{}",
key.name(),
en.text(*key)
);
assert!(
!zh.text(*key).contains("下载"),
"{} 的中文文案还在说「下载」:{}",
key.name(),
zh.text(*key)
);
checked += 1;
}
assert!(checked > 20, "只扫到 {checked} 条,守门形同虚设");
// 副标题要讲这个页面「管什么」(版本 + 加载器),不是「能下载什么」
let subtitle_en = en.text(Key::DownloadSubtitle);
let subtitle_zh = zh.text(Key::DownloadSubtitle);
assert!(
subtitle_en.contains("versions") && subtitle_en.contains("loaders"),
"副标题要提到版本与加载器:{subtitle_en}"
);
assert!(
subtitle_zh.contains("版本") && subtitle_zh.contains("加载器"),
"副标题要提到版本与加载器:{subtitle_zh}"
);
}
}