fix(download): M94 r1 —— 五条复审(P2-1/2/3/4/6)
P2-1 占位规则统一:加载态原先贴顶、空态居中 → 两态都走同一套「内容在可用区居中」
(依据:components::feedback::empty_state 的既有口径 12b §15.4;两态互切不再纵向跳)。
实测两个状态的内容中心线 y=449 / y=445(差 4px)。
P2-2 文案只说真能做到的事:
- 删「The list appears after the first successful fetch; this page never goes blank.」
(工程话术 + 承诺了 UI 做不到的「永空白」)→「如果一直停在这里,点右上角的刷新。」
(对应页头那个真的 Refresh 按钮)
- 空态「Switch the category or clear the filter」删掉「清筛选」(页面没有筛选控件)
→「换一个分类,或刷新一次列表。」
- 副标题「Version list from the official manifest」去掉 manifest 这种工程词 →「可以安装的版本」
P2-3 去掉静止的 indeterminate 进度条:页面没有帧订阅,indeterminate 只会拿到一个固定相位
(看起来像 17% 卡住)。加载态改成纯文字;安装面板只在进度已知时画 determinate。
两帧证据:215/512 → 条填 177px(42%),409/512 → 338px(80%),条随事件走而不是动画。
P2-4 失败态与安装中彻底不同构:失败不再画进度条,改成整块 ErrorContainer 面板
(ErrorContainer 底 + Error 图标 + Error 色标题 + 停在哪个文件 + 说明 + 实心「打开日志」)。
实测底色:浅色 (255,218,214) / 深色 (147,0,10),安装中面板底色仍是页面底色。
P2-6 死 key 与假守卫:
- 删掉没用的 DownloadSelected;
- 用真的守卫替换原来那条「前缀遍历 + 条数下限」:every_download_key_is_referenced_by_the_page
用 include_str! 把页面源码编进来,目录里每个 Download* key 必须在源码里出现 Key::Xxx。
实证:把 DownloadSelected 放回去 → 该测试红并点名 key;删掉 → 绿。
注释里也写清抓不住的三种情况(反向是编译错误 / 译文完整性归 i18n 自己的测试 / 宏拼 key 能骗过它)。
门禁:fmt --check 干净、clippy -D warnings 干净、app-iced 276 + i18n 23 全绿;
tests.rs 384→383 行(不再贴顶),其余文件均 ≤400。
This commit is contained in:
1 parent
57e8380b24
commit
9d6ee70301
6 files changed
+229
-145
No files matched your search
@@ -22,10 +22,14 @@
|
||||
//!
|
||||
//! | 状态 | 条件 | 组件 |
|
||||
//! |---|---|---|
|
||||
//! | 加载 | 快照里没有版本列表([`Download::is_loading`]) | 不确定态进度条 + 文案([`states::loading`]) |
|
||||
//! | 空 | 列表拉到了、过滤后为空([`Download::is_empty`]) | `EmptyState` + 刷新 CTA([`states::empty`]) |
|
||||
//! | 加载 | 快照里没有版本列表([`Download::is_loading`]) | 标题 + 说明,**居中、无进度形状**([`states::loading`]) |
|
||||
//! | 空 | 列表拉到了、过滤后为空([`Download::is_empty`]) | `EmptyState` + 刷新 CTA,**同样居中**([`states::empty`]) |
|
||||
//! | 有数据 | 有版本行 | Tabs + 计数头 + `ListItem` 列表([`rows`]) |
|
||||
//! | 安装中 / 失败 | `AppState::install` 不为空 | 进度条 + 阶段 + 「取消」/「打开日志」([`install`]) |
|
||||
//! | 安装中 | `AppState::install` 在跑 | 阶段 + **进度条(仅当进度已知)** + 计数 + 「取消」([`install`]) |
|
||||
//! | 失败 | `install.stage == Failed` | **`ErrorContainer` 面板**:`Error` 图标/标题 + 实心「打开日志」,**没有进度条**([`install`]) |
|
||||
//!
|
||||
//! 两个占位状态(加载 / 空)走**同一套规则**:内容在可用区居中(依据:`feedback::empty_state`
|
||||
//! 的既有口径 `12b §15.4`,且两态互切时内容不会纵向跳)。
|
||||
//!
|
||||
//! ## 意图 → 消息 → 指令(页面只发意图,不做乐观回显)
|
||||
//!
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//! --screen=download --screenshot=out.png --screenshot-theme=light --software-renderer
|
||||
//! ```
|
||||
//!
|
||||
//! 取值:`ready`(缺省)/ `loading` / `empty` / `selected` / `installing` / `failed`。
|
||||
//! 取值:`ready`(缺省)/ `loading` / `empty` / `selected` / `installing` / `installing-late` / `failed`。
|
||||
//! **只有 harness 与演示走这里**:产品路径由 `app/view.rs` 从 presenter 快照建模型。
|
||||
|
||||
use unlucky_presenter::dto::{
|
||||
@@ -45,6 +45,12 @@ pub fn demo(catalog: Catalog) -> Download {
|
||||
selected: Some("1.21.9".to_string()),
|
||||
..ready(catalog)
|
||||
},
|
||||
// 同一状态的第二个采样点:进度条随**事件**推进(不是动画),出图时两帧对照
|
||||
Some("installing-late") => Download {
|
||||
install: Some(install(InstallStage::Downloading, 0.8, catalog)),
|
||||
selected: Some("1.21.9".to_string()),
|
||||
..ready(catalog)
|
||||
},
|
||||
Some("selected") => Download {
|
||||
selected: Some("1.21.9".to_string()),
|
||||
..ready(catalog)
|
||||
|
||||
@@ -2,26 +2,37 @@
|
||||
//!
|
||||
//! ## 这一块为什么钉在底部而不是跟着列表滚
|
||||
//!
|
||||
//! 列表是**线性进度项 + 版本集**,装的是「几十行」;底部按钮如果跟着滚,用户在列表中间永远
|
||||
//! 看不到「安装」在哪。所以列表独立滚动(`rows::list`),面板固定占尾部一行高度。
|
||||
//! 列表装的是几十行版本;底部按钮如果跟着滚,用户在列表中间永远看不到「安装」在哪。
|
||||
//! 所以列表独立滚动(`rows::list`),面板固定占尾部一块高度。
|
||||
//!
|
||||
//! ## 只有 intent,没有乐观回显
|
||||
//! ## 只有意图,没有乐观回显
|
||||
//!
|
||||
//! | 控件 | 消息 | app 层落点 |
|
||||
//! |---|---|---|
|
||||
//! | 「安装」 | [`Message::InstallVersion`] | `AppCommand::InstallVersion(id)` |
|
||||
//! | 「取消安装」 | [`Message::CancelInstall`] | `AppCommand::CancelInstall` |
|
||||
//! | 「打开日志」 | [`Message::Navigate(Route::Logs)]` | `AppState::navigate`(不派指令) |
|
||||
//! | 「打开日志」 | [`Message::Navigate(Route::Logs)`] | `AppState::navigate`(不派指令) |
|
||||
//!
|
||||
//! 按钮按下**不改任何本地状态**:进度条读的是 `model.install`,它只由 presenter 的
|
||||
//! `InstallProgress` 事件灌进来(`apply` → 视图模型重建)。于是「点了没变」= 后端还没回事件,
|
||||
//! 正是要的行为。
|
||||
//! `InstallProgress` 事件灌进来(`apply` → 视图模型重建)。于是「点了没变」= 后端还没回事件。
|
||||
//!
|
||||
//! ## 三种面板形态刻意不同构(M94 r1 / P2-3 + P2-4)
|
||||
//!
|
||||
//! | 状态 | 形态 | 为什么 |
|
||||
//! |---|---|---|
|
||||
//! | 选好了 | 版本 id + 实心「安装」 | 本页唯一的实心按钮(rubric K2) |
|
||||
//! | 安装中 | 版本 id + 阶段 + **进度条**(仅当进度已知)+ 计数 + 文本「取消安装」 | 有进度才有条 |
|
||||
//! | 失败 | **整块 `ErrorContainer` 面板**:`Error` 图标 + `Error` 色标题 + 停在哪 + 实心「打开日志」 | **不画进度条**——进度条正是「看起来还在跑」的来源;颜色/图标/措辞/按钮四样一起变,才一眼可辨 |
|
||||
//!
|
||||
//! 进度未知(刚派发、后端还没给文件计数)时**不画条**:页面没有帧订阅,`Progress::indeterminate`
|
||||
//! 只会得到一个静止的假进度(复审 P2-3 的「17% 卡住」),不如只写「等第一个文件…」。
|
||||
|
||||
use iced::widget::{column, container, row};
|
||||
use iced::{Alignment, Element, Length, Padding};
|
||||
use iced::{Alignment, Background, Border, Element, Length, Padding};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
|
||||
@@ -37,9 +48,6 @@ use super::super::frame;
|
||||
use super::super::Message;
|
||||
use super::model::{Download, InstallView, VersionRow};
|
||||
|
||||
/// 不确定态进度条的相位(页面没有帧订阅 ⇒ 不做动画,取一帧好看的静态相位)。
|
||||
const INDETERMINATE_PHASE: f32 = 0.35;
|
||||
|
||||
/// 底部的安装面板。
|
||||
pub(super) fn panel<'a>(ui: &'a Ui, catalog: Catalog, model: &'a Download) -> Element<'a, Message> {
|
||||
let content = match &model.install {
|
||||
@@ -94,52 +102,50 @@ fn ready<'a>(ui: &'a Ui, catalog: Catalog, entry: &'a VersionRow) -> Element<'a,
|
||||
)
|
||||
};
|
||||
|
||||
row![label.width(Length::Fill), action]
|
||||
row![label, action]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 安装中 / 已收尾:阶段 + 进度条 + 计数 + 速率,失败时给可读文案与出口。
|
||||
/// 安装中 / 已收尾(失败另走 [`failed`])。
|
||||
fn installing<'a>(ui: &'a Ui, catalog: Catalog, install: &'a InstallView) -> Element<'a, Message> {
|
||||
let bar = if install.known {
|
||||
linear(ui, Progress::determinate(install.ratio))
|
||||
} else {
|
||||
linear(ui, Progress::indeterminate(INDETERMINATE_PHASE))
|
||||
};
|
||||
if install.failed() {
|
||||
return failed(ui, catalog, install);
|
||||
}
|
||||
|
||||
let mut body = column![
|
||||
row![
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
false,
|
||||
install.version.as_str(),
|
||||
ui.color(Role::OnSurface),
|
||||
))
|
||||
.width(Length::Fill),
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::LabelMedium,
|
||||
false,
|
||||
install.stage_label.as_str(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center),
|
||||
bar,
|
||||
let head = row![
|
||||
container(frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
false,
|
||||
install.version.as_str(),
|
||||
ui.color(Role::OnSurface),
|
||||
))
|
||||
.width(Length::Fill),
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
TypeRole::LabelMedium,
|
||||
false,
|
||||
install.detail.as_str(),
|
||||
install.stage_label.as_str(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.width(Length::Fill);
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
let mut body = column![head];
|
||||
// 进度已知才画条:未知时画 indeterminate 会得到一个静止的假进度(见模块文档)。
|
||||
if install.known {
|
||||
body = body.push(linear(ui, Progress::determinate(install.ratio)));
|
||||
}
|
||||
body = body.push(frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
install.detail.as_str(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
));
|
||||
if let Some(rate) = &install.rate {
|
||||
body = body.push(frame::text_role(
|
||||
ui,
|
||||
@@ -150,36 +156,84 @@ fn installing<'a>(ui: &'a Ui, catalog: Catalog, install: &'a InstallView) -> Ele
|
||||
));
|
||||
}
|
||||
|
||||
// 失败态:**可读的原因出口**(结构化文案 + 去日志页),不静默、不搬 core 的中文串。
|
||||
if install.failed() {
|
||||
body = body.push(frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
catalog.text(Key::DownloadFailedHint),
|
||||
ui.color(Role::Error),
|
||||
));
|
||||
}
|
||||
|
||||
let action = if install.running() {
|
||||
buttons::text(
|
||||
ui,
|
||||
Button::new(catalog.text(Key::DownloadCancel), Message::CancelInstall),
|
||||
)
|
||||
} else if install.failed() {
|
||||
buttons::outlined(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::DownloadOpenLogs),
|
||||
Message::Navigate(Route::Logs),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
frame::gap(0.0)
|
||||
};
|
||||
|
||||
column![body, action]
|
||||
column![
|
||||
body.spacing(ui.space(Space::X2)).width(Length::Fill),
|
||||
action
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 失败态:一整块 `ErrorContainer` 面板(**没有进度条**)。
|
||||
///
|
||||
/// 四样一起变,保证与「正在下载」一眼可辨:容器色(`ErrorContainer`)/ 图标(`Error`)/
|
||||
/// 措辞(`Install failed` 用 `Error` 色标题)/ 主按钮(实心「打开日志」,不是文本按钮)。
|
||||
fn failed<'a>(ui: &'a Ui, catalog: Catalog, install: &'a InstallView) -> Element<'a, Message> {
|
||||
let head = row![
|
||||
frame::glyph(IconKind::Error, ui.color(Role::Error), frame::ICON_PX),
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::TitleSmall,
|
||||
true,
|
||||
catalog.text(Key::DownloadStageFailed),
|
||||
ui.color(Role::Error),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
let stopped = frame::text_role_owned(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
format!("{} · {}", install.version, install.detail),
|
||||
ui.color(Role::OnErrorContainer),
|
||||
);
|
||||
|
||||
let action = buttons::filled(
|
||||
ui,
|
||||
Button::new(
|
||||
catalog.text(Key::DownloadOpenLogs),
|
||||
Message::Navigate(Route::Logs),
|
||||
)
|
||||
.leading(IconKind::Logs),
|
||||
);
|
||||
|
||||
container(
|
||||
column![
|
||||
head,
|
||||
stopped,
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
catalog.text(Key::DownloadFailedHint),
|
||||
ui.color(Role::OnErrorContainer),
|
||||
),
|
||||
action,
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding(ui.space(Space::X4))
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(Background::Color(ui.color(Role::ErrorContainer))),
|
||||
border: Border {
|
||||
radius: ui.radius_dp(ShapeScale::Large),
|
||||
..Border::default()
|
||||
},
|
||||
..container::Style::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
@@ -1,19 +1,30 @@
|
||||
//! 加载态与空态(**没有可用版本数据时页面不能是白屏**)。
|
||||
//! 占位状态(加载 / 空):**同一套占位规则——内容在可用区居中**(M94 r1 / P2-1)。
|
||||
//!
|
||||
//! | 状态 | 触发条件(都能从快照推出来) | 画什么 |
|
||||
//! | 状态 | 条件(都能从快照推出来) | 画什么 |
|
||||
//! |---|---|---|
|
||||
//! | 加载 | `model.is_loading()`:快照里还没有版本列表 | 不确定态进度条 + 「正在加载版本列表…」+ 一句说明 |
|
||||
//! | 空 | `model.is_empty()`:列表拉到了,但过滤后一行不剩 | 真 [`empty_state`](图标块 96 + 24 标题 + 说明 + 一个刷新 CTA) |
|
||||
//! | 加载 | `model.is_loading()`:快照里还没有版本列表 | 标题 + 一句说明(**没有任何进度形状**) |
|
||||
//! | 空 | `model.is_empty()`:列表拉到了,但过滤后一行不剩 | 真 [`empty_state`](图标块 96 + 24 标题 + 说明 + 刷新 CTA) |
|
||||
//!
|
||||
//! 两者都**不留白**:加载态给得出「还在等」的视觉证据(进度条不是装饰,是唯一的非文字信号),
|
||||
//! 空态给得出「下一步做什么」(刷新)。
|
||||
//! ## 为什么是「都居中」(P2-1)
|
||||
//!
|
||||
//! 拉取**失败**在这条通道上没有结构化事件(presenter 只有 `VersionListChanged`),所以列表拉不到
|
||||
//! 会停在加载态而不是谎报「没有版本」——加载态的说明文案里就写着这件事。安装失败的错误态在
|
||||
//! [`super::install`] 里(那里的 `Failed` 阶段是结构化字段,可以放心翻译)。
|
||||
//! `components::feedback::empty_state` 的既有口径(`12b §15.4`)就是「内容在可用区居中」;
|
||||
//! 加载态原先贴顶,等于同一个页面里两套占位规则,两态互切时内容会纵向跳一次。所以本轮把加载态
|
||||
//! 也放进同一条规则里。**有数据时列表仍然贴顶**——那是有内容的状态,不属于占位规则。
|
||||
//!
|
||||
//! ## 为什么加载态没有进度条(P2-3)
|
||||
//!
|
||||
//! 页面**没有帧订阅**(`app/subscription.rs` 的 `needs_frames` 归 app 层),所以
|
||||
//! `Progress::indeterminate(phase)` 拿到的是一个**静止的**相位——用户看到的是「17% 卡住」。
|
||||
//! 与其留一个「看起来有进度但不动」的控件,不如给一句明确的「正在加载」文字;等 app 层真有了
|
||||
//! 帧订阅再来带动画(那时这里换回 indeterminate 才有意义)。
|
||||
//!
|
||||
//! ## 文案口径(P2-2)
|
||||
//!
|
||||
//! 只说页面真能做到的动作:说明里那句「点右上角的刷新」对应页头那个真的 Refresh 按钮
|
||||
//! (`Message::Action(Action::RefreshSources)`);没有写「稍后会自动重试」这种 UI 做不到的承诺。
|
||||
|
||||
use iced::widget::column;
|
||||
use iced::{Element, Length};
|
||||
use iced::widget::{column, container};
|
||||
use iced::{Alignment, Element, Length};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::icons::IconKind;
|
||||
@@ -22,42 +33,50 @@ use unlucky_design::typography::TypeRole;
|
||||
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
|
||||
use crate::app::components::feedback::{empty_state, linear, EmptyState, Progress};
|
||||
use crate::app::components::feedback::{empty_state, EmptyState};
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::frame;
|
||||
use super::super::{Action, Message};
|
||||
|
||||
/// 不确定态进度条的相位(没有帧订阅,取一帧静态相位;见 `install.rs` 同名常量)。
|
||||
const INDETERMINATE_PHASE: f32 = 0.35;
|
||||
/// 两个占位状态共用的外壳:水平 + 垂直都居中(见模块文档的两条理由)。
|
||||
fn centered<'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_x(Alignment::Center)
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 加载态。
|
||||
/// 加载态:一句「正在加载」+ 一句这个页面真能做的事(点刷新)。
|
||||
pub(super) fn loading<'a>(ui: &'a Ui, catalog: Catalog) -> Element<'a, Message> {
|
||||
column![
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyLarge,
|
||||
true,
|
||||
catalog.text(Key::DownloadLoading),
|
||||
ui.color(Role::OnSurface),
|
||||
),
|
||||
linear(ui, Progress::indeterminate(INDETERMINATE_PHASE)),
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
catalog.text(Key::DownloadLoadingHint),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X3))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
centered(
|
||||
column![
|
||||
frame::text_role_centered(
|
||||
ui,
|
||||
TypeRole::TitleMedium,
|
||||
true,
|
||||
catalog.text(Key::DownloadLoading),
|
||||
ui.color(Role::OnSurface),
|
||||
),
|
||||
frame::text_role_centered(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
catalog.text(Key::DownloadLoadingHint),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X3))
|
||||
.width(Length::Fill)
|
||||
.align_x(Alignment::Center),
|
||||
)
|
||||
}
|
||||
|
||||
/// 空态(真 `EmptyState`:CTA 就是标准 Filled 按钮,走按钮家族)。
|
||||
pub(super) fn empty<'a>(ui: &'a Ui, catalog: Catalog) -> Element<'a, Message> {
|
||||
empty_state(
|
||||
centered(empty_state(
|
||||
ui,
|
||||
EmptyState::new(
|
||||
IconKind::Download,
|
||||
@@ -68,5 +87,5 @@ pub(super) fn empty<'a>(ui: &'a Ui, catalog: Catalog) -> Element<'a, Message> {
|
||||
catalog.text(Key::DownloadRefresh),
|
||||
Message::Action(Action::RefreshSources),
|
||||
),
|
||||
)
|
||||
))
|
||||
}
|
||||
@@ -73,8 +73,6 @@ fn extra_width_goes_to_margin_not_to_the_column() {
|
||||
// --------------------------------------------------------------- 快照 → 模型
|
||||
|
||||
/// 真快照进得来:行数、分类、选中项、已装标记全部来自 `AppState`。
|
||||
///
|
||||
/// 抓得住:页面自己造数据、或把 `installed` / `kind` 丢了。
|
||||
/// 抓不住:快照字段在 `app/view.rs` 被漏接(那由 `app/update.rs` 的接线测试兜)。
|
||||
#[test]
|
||||
fn the_snapshot_becomes_the_page_model() {
|
||||
@@ -125,8 +123,6 @@ fn the_snapshot_becomes_the_page_model() {
|
||||
}
|
||||
|
||||
/// 快照里的两个过滤条件(`installed_only` / `download_query`)原样照用。
|
||||
///
|
||||
/// 抓得住:页面把 presenter 的过滤条件丢了(用户设了「仅已装」,页面照画全部)。
|
||||
/// 抓不住:过滤词大小写之外的语言学规则(那属于 presenter 的搜索口径)。
|
||||
#[test]
|
||||
fn presenter_filters_still_apply() {
|
||||
@@ -151,9 +147,7 @@ fn presenter_filters_still_apply() {
|
||||
assert!(queried.rows.iter().all(|row| row.id.starts_with("1.21")));
|
||||
}
|
||||
|
||||
/// 空快照 = 加载态;过滤掉所有行 = 空态。两者都**不许**是白屏。
|
||||
///
|
||||
/// 抓得住:把「还没拉到」画成「没有版本」(谎报空态),或反过来。
|
||||
/// 空快照 = 加载态;过滤掉所有行 = 空态(抓「把还没拉到画成没有版本」,或反过来)。
|
||||
/// 抓不住:真机上卡在加载态多久(没有超时信号,见模块文档的缺口表)。
|
||||
#[test]
|
||||
fn loading_and_empty_are_different_states() {
|
||||
@@ -213,31 +207,39 @@ fn category_commands_are_real() {
|
||||
|
||||
// --------------------------------------------------------------- 文案(i18n)
|
||||
|
||||
/// 中英两套目录都能把这一页的每个 key 求出来(缺译会露出 `⟪missing⟫`)。
|
||||
/// 每个 `Download*` key 都**真的被页面引用**(死 key 守门,M94 r1 / P2-6)。
|
||||
///
|
||||
/// 「这一页的 key」按 `Download` 前缀取,不手写清单:清单会漏、会在下一次加 key 时不同步,
|
||||
/// 而前缀遍历顺手还守住了「不许把 key 悄悄删掉」(条数下限)。
|
||||
///
|
||||
/// 抓得住:新增 key 只写了一种语言(另一套会显示 `⟪missing⟫`)。
|
||||
/// 抓不住:译文质量,以及**没有用到的** key(那是 `catalog` 自己的完整性测试)。
|
||||
/// 复审抓到的 `DownloadSelected` 就是这条要拦的东西:目录里定义了、页面没人用。所以这里把页面
|
||||
/// 源码用 `include_str!` 编进来扫一遍。**抓不住什么也说清**:反向(引用了不存在的 key)是编译错误,
|
||||
/// 不需要测试;译文对不对、两套语言都填了没由 `i18n` 自己的 `built_in_catalogs_have_every_key` 守;
|
||||
/// 它只看字面出现,用宏拼 key 名能骗过它。
|
||||
#[test]
|
||||
fn every_download_key_exists_in_both_catalogs() {
|
||||
let download_keys: Vec<Key> = Key::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|key| key.name().starts_with("Download"))
|
||||
.collect();
|
||||
assert!(download_keys.len() >= 25, "下载页的 key 不该被悄悄删掉");
|
||||
fn every_download_key_is_referenced_by_the_page() {
|
||||
const SOURCES: [&str; 6] = [
|
||||
include_str!("../download.rs"),
|
||||
include_str!("model.rs"),
|
||||
include_str!("rows.rs"),
|
||||
include_str!("install.rs"),
|
||||
include_str!("states.rs"),
|
||||
include_str!("demo.rs"),
|
||||
];
|
||||
|
||||
for lang in [Lang::En, Lang::ZhCn] {
|
||||
let catalog = lang.catalog();
|
||||
for key in &download_keys {
|
||||
let text = catalog.text(*key);
|
||||
assert_ne!(text, unlucky_i18n::catalog::MISSING, "{key:?} 缺译");
|
||||
assert!(!text.is_empty(), "{key:?} 是空串");
|
||||
}
|
||||
assert!(catalog.missing().is_empty(), "内置目录不许有缺译");
|
||||
let mut checked = 0;
|
||||
for key in Key::ALL
|
||||
.iter()
|
||||
.filter(|key| key.name().starts_with("Download"))
|
||||
{
|
||||
let needle = format!("Key::{}", key.name());
|
||||
assert!(
|
||||
SOURCES.iter().any(|source| source.contains(&needle)),
|
||||
"{needle} 在目录里定义了,但下载页没有任何地方用它(死 key,删掉或接上)"
|
||||
);
|
||||
checked += 1;
|
||||
}
|
||||
assert!(
|
||||
checked > 20,
|
||||
"下载页的 key 不该被成批删掉(只查到 {checked} 条)"
|
||||
);
|
||||
}
|
||||
|
||||
/// 页面级的文案全部按 `Catalog` 走:中文目录出中文,英文目录出英文,**且不含 core 的中文业务串**。
|
||||
@@ -360,10 +362,7 @@ fn a_failed_install_stays_visible() {
|
||||
assert_eq!(model.stage_label, "Install failed");
|
||||
}
|
||||
|
||||
/// 演示数据自己也得守规矩(它是出图的基线)。
|
||||
///
|
||||
/// 抓得住:演示数据里混进中文字面量、或行数与分类对不上。
|
||||
/// 抓不住:图上好不好看。
|
||||
/// 演示数据自己也得守规矩(它是出图的基线):抓中文字面量混入 / 行数与分类对不上。
|
||||
#[test]
|
||||
fn the_demo_data_is_sane() {
|
||||
let catalog = Lang::En.catalog();
|
||||
|
||||
@@ -119,8 +119,11 @@ catalog! {
|
||||
// --- download page (M94) ---
|
||||
// 下载页(`crates/app-iced/src/pages/download`)。一句话一个 key:带变量的句子用 `{name}`,
|
||||
// 页面只做 `Catalog::render`,不拼片段(`bridge` 的规矩 1)。
|
||||
//
|
||||
// 文案口径(M94 r1 / P2-2):**只说这个页面真能做到的动作**(切分类、刷新、看日志),
|
||||
// 不写「稍后会自动重试」这类 UI 没有的能力,也不拿文案去补功能。
|
||||
DownloadTitle => ("Downloads", "下载"),
|
||||
DownloadSubtitle => ("Version list from the official manifest", "来自官方版本清单的版本列表"),
|
||||
DownloadSubtitle => ("Versions you can install", "可以安装的版本"),
|
||||
DownloadRefresh => ("Refresh", "刷新"),
|
||||
DownloadInstall => ("Install", "安装"),
|
||||
DownloadCancel => ("Cancel install", "取消安装"),
|
||||
@@ -128,16 +131,15 @@ catalog! {
|
||||
DownloadCount => ("{count} versions", "{count} 个版本"),
|
||||
DownloadRowSupporting => ("{kind} · released {date}", "{kind} · 发布于 {date}"),
|
||||
DownloadSelectHint => ("Pick a version, then install", "选一个版本再安装"),
|
||||
DownloadSelected => ("Selected {version}", "已选 {version}"),
|
||||
DownloadLoading => ("Loading the version list…", "正在加载版本列表…"),
|
||||
DownloadLoading => ("Loading versions…", "正在加载版本列表…"),
|
||||
DownloadLoadingHint => (
|
||||
"The list appears after the first successful fetch; this page never goes blank.",
|
||||
"首次拉取成功后这里会出现版本列表;这一页不会是一片白。"
|
||||
"If this stays here, press Refresh in the top right.",
|
||||
"如果一直停在这里,点右上角的刷新。"
|
||||
),
|
||||
DownloadEmptyTitle => ("No matching versions", "没有匹配的版本"),
|
||||
DownloadEmptyTitle => ("No versions to show", "没有可显示的版本"),
|
||||
DownloadEmptyBody => (
|
||||
"Switch the category or clear the filter to see more.",
|
||||
"换一个分类或清掉筛选后再看。"
|
||||
"Pick another category, or refresh the list.",
|
||||
"换一个分类,或刷新一次列表。"
|
||||
),
|
||||
DownloadCategoryAll => ("All", "全部"),
|
||||
DownloadCategoryRelease => ("Release", "正式版"),
|
||||
|
||||
Reference in new issue
Block a user