fix(app-iced): M109 r1 —— CF 来源装入闸门、has_more 读最近一页、末行切半与文案/图标
复审 p1-2items 的两条 P1 与三条 P2,逐条收口。 `blockers_of` 只看版本/加载器/目标实例,不看来源;而 host 的 `resolve_file` 对 CF 直接 返回「未接通」⇒ 切到 CF 源后每条都必然失败,UI 一声不响。 - 新增 `Blocker::SourceNotWired` + 文案 `ContentSourceNotWired`(中英); - `blockers_of` 的入参从 `Option<(&[String], &[String])>` 收成 `Option<HitFacts>`(版本 / 加载器 / **来源**),两处调用点(产品路径、演示数据)一起改; - 判据同构于整合包的 `NeedsPipeline`:**点之前**禁用并给一条原因;CF 文件端点接通后删掉这条。 DTO 的 `has_more()` 判「本页装满」是对的,但 reducer 在 `offset>0` 时先 extend 再判, 被判定的是**累计条数** ⇒ 首页满 20 条之后恒真:「加载更多」永不消失、「没有更多了」在 真实翻页路径上不可达,且每点一次白发一次网络请求。 - `ContentState` 新增 `last_page_filled`(**先**按这一页记一位,再并进累计列表); - 页面的 `has_more` 读它(`&& !pending`);换条件(offset 归零)时清位; - 新增 `crates/presenter/src/state/content_tests.rs` 四条用例(state.rs 贴 400 行上限,拆文件): `a_short_last_page_clears_has_more` / `an_empty_last_page_keeps_has_more_false` / `changing_the_query_resets_the_page_flag` / `a_stale_batch_does_not_touch_the_page_flag`; - 变异自证:把判据改回「读累计」后前两条红(`末页不满 ⇒ 不许说还有更多`、`空页 ⇒ 没有更多`), 改回绿。 - **末行被 footer 切半**(复审定位的真因):`view.rs` 的 body 列漏了 `.spacing(..)`,而列表 纵向内边距是 0 ⇒ 视口底边正好切在半行上。补 `Space::X2`,并说明为什么必须给。 - **文案去掉开发者术语与内部编号**:`ContentSourceUnavailable` 不再写 `ULL_CF_API_KEY` (改「CurseForge 需要在设置里配置密钥才能搜索」);`ContentModpackNotReady` 不再写 mission 编号(改「整合包安装尚未开放」);host 侧那条 `InstallProgress` 失败文案同步去编号。 - **图标**:四类改为 `Folder` / `Plus` / `Restore` / `Expand`——不借用导航五枚(同屏撞形, 复审 P2-3)、不用填充字形(`no_hardcoded_tokens` 门禁);偏离表按**实际取值**重写,并补一条 「CF 的装入也是禁用态」的登记。 - 演示数据加 `ULL_CONTENT_STATE=cf` 一屏,出图证明 P1-1 的禁用态(`12-cf-not-wired-light.png`)。 门禁(逐条落盘退出码,不走管道):fmt 0 / clippy `-D warnings`(app-iced+presenter+i18n)0 / `cargo test -p unlucky-app-iced --lib` 0(451 passed)/ `cargo test -p unlucky-presenter` 0 (174 passed)/ `cargo test --workspace --exclude unlucky-app` 0。改动文件最大 397 行。 取证:`DevDocs/artifacts/m109/`(12 张,全部重出)
This commit is contained in:
1 parent
6b161227d8
commit
42099d4336
13 files changed
+246
-40
No files matched your search
@@ -335,6 +335,12 @@ pub struct ContentState {
|
||||
pub pending: bool,
|
||||
/// 最近一次失败的原文(`None` = 没失败)
|
||||
pub failed: Option<String>,
|
||||
/// **最近一页**是否装满(`hits.len() >= limit`)。
|
||||
///
|
||||
/// 为什么不是「累计条数够一页」:分页是**累加**到 `results.hits` 里的,累计值在第 2 页之后
|
||||
/// 必然 ≥ limit ⇒ 用累计判会得到「has_more 恒真」:加载更多永不消失、「没有更多了」在真实
|
||||
/// 翻页路径上不可达,而且每点一次白发一次网络请求(复审 P1-2)。所以这一位单独记。
|
||||
pub last_page_filled: bool,
|
||||
}
|
||||
|
||||
impl Default for ContentState {
|
||||
@@ -344,6 +350,7 @@ impl Default for ContentState {
|
||||
results: None,
|
||||
pending: false,
|
||||
failed: None,
|
||||
last_page_filled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub(super) fn install(
|
||||
fail(
|
||||
tx,
|
||||
progress,
|
||||
"整合包安装管线未落地(M111);内容库对整合包只做检索",
|
||||
"整合包安装尚未开放;内容库对整合包只做检索",
|
||||
epoch,
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -289,6 +289,8 @@ impl AppState {
|
||||
// 迟到的一批:用户已经改了条件,覆盖上去就会出现「筛选条说 A、列表是 B」。
|
||||
return;
|
||||
}
|
||||
// **先**按「这一页装没装满」记一位,再把它并进累计列表(复审 P1-2:判据不能读累计值)。
|
||||
self.content.last_page_filled = results.has_more();
|
||||
match &mut self.content.results {
|
||||
Some(current) if results.query.offset > 0 => {
|
||||
current.hits.extend(results.hits);
|
||||
@@ -305,6 +307,7 @@ impl AppState {
|
||||
self.content.pending = true;
|
||||
self.content.failed = None;
|
||||
if query.offset == 0 {
|
||||
self.content.last_page_filled = false;
|
||||
// 换条件(不是换页):先把旧结果清掉,否则加载态会画出上一批命中项。
|
||||
self.content.results = None;
|
||||
}
|
||||
@@ -419,3 +422,6 @@ impl AppState {
|
||||
#[cfg(test)]
|
||||
#[path = "state_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod content_tests;
|
||||
@@ -0,0 +1,130 @@
|
||||
//! 内容库状态机的守门测试(M109;复审 P1-2 的补课)。
|
||||
//!
|
||||
//! 从 `state.rs` 拆出(那个文件贴 400 行硬上限)。钉的是**翻页判据**:
|
||||
//! `has_more` 必须读「最近一页是否装满」,不能读累计条数——否则第 2 页之后恒真。
|
||||
|
||||
use crate::dto::{
|
||||
AppEvent, ContentHit, ContentKind, ContentQuery, ContentResults, ContentSource, CoreEvent,
|
||||
};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
fn hit(index: usize) -> ContentHit {
|
||||
ContentHit {
|
||||
id: format!("p{index}"),
|
||||
slug: format!("p{index}"),
|
||||
title: format!("Project {index}"),
|
||||
summary: "s".to_string(),
|
||||
author: "a".to_string(),
|
||||
downloads: 0,
|
||||
updated: "2026-01-01".to_string(),
|
||||
source: ContentSource::Modrinth,
|
||||
game_versions: vec!["1.20.1".to_string()],
|
||||
loaders: vec!["fabric".to_string()],
|
||||
icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 造一页结果:`offset` 是游标,`count` 是这一页实际回来几条。
|
||||
fn page(offset: u32, count: u32) -> ContentResults {
|
||||
let mut query = ContentQuery::first(ContentKind::Mod);
|
||||
query.offset = offset;
|
||||
ContentResults {
|
||||
query,
|
||||
hits: (0..count)
|
||||
.map(|index| hit(offset as usize + index as usize))
|
||||
.collect(),
|
||||
total: 999,
|
||||
curseforge: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(state: &mut AppState, results: ContentResults) {
|
||||
state.apply(AppEvent::Core(CoreEvent::ContentResults(Box::new(results))));
|
||||
}
|
||||
|
||||
/// 复审 P1-2 的第一条:装满一页 ⇒ 还有更多;下一(末)页不满 ⇒ 没有了。
|
||||
#[test]
|
||||
fn a_short_last_page_clears_has_more() {
|
||||
let mut state = AppState::new();
|
||||
let limit = ContentQuery::first(ContentKind::Mod).limit;
|
||||
|
||||
apply(&mut state, page(0, limit));
|
||||
assert!(state.content.last_page_filled, "首页装满 ⇒ 还有下一页");
|
||||
assert_eq!(
|
||||
state.content.results.as_ref().map_or(0, |r| r.hits.len()),
|
||||
limit as usize
|
||||
);
|
||||
|
||||
// 末页只回来 3 条:累计 23 条「够一页了」,但**这一页**没装满 ⇒ 没有更多
|
||||
apply(&mut state, page(limit, 3));
|
||||
assert!(
|
||||
!state.content.last_page_filled,
|
||||
"末页不满 ⇒ 不许说还有更多(判据不能读累计条数)"
|
||||
);
|
||||
assert_eq!(
|
||||
state.content.results.as_ref().map_or(0, |r| r.hits.len()),
|
||||
limit as usize + 3,
|
||||
"命中项仍然要累加"
|
||||
);
|
||||
}
|
||||
|
||||
/// 复审 P1-2 的第二条:再点一次,回来的空页仍然是「没有更多」。
|
||||
#[test]
|
||||
fn an_empty_last_page_keeps_has_more_false() {
|
||||
let mut state = AppState::new();
|
||||
let limit = ContentQuery::first(ContentKind::Mod).limit;
|
||||
|
||||
apply(&mut state, page(0, limit));
|
||||
apply(&mut state, page(limit, 0));
|
||||
assert!(!state.content.last_page_filled, "空页 ⇒ 没有更多");
|
||||
|
||||
// 用户又点了一次「加载更多」:空页不会把状态翻回真
|
||||
apply(&mut state, page(limit + limit, 0));
|
||||
assert!(
|
||||
!state.content.last_page_filled,
|
||||
"再点一次空页 ⇒ 仍然没有更多"
|
||||
);
|
||||
assert_eq!(
|
||||
state.content.results.as_ref().map_or(0, |r| r.hits.len()),
|
||||
limit as usize,
|
||||
"空页不改变列表内容"
|
||||
);
|
||||
}
|
||||
|
||||
/// 换条件(offset 回到 0)时把「最近一页装满」清掉:不然新条件的第一帧就会画「没有更多了」。
|
||||
#[test]
|
||||
fn changing_the_query_resets_the_page_flag() {
|
||||
let mut state = AppState::new();
|
||||
let limit = ContentQuery::first(ContentKind::Mod).limit;
|
||||
|
||||
apply(&mut state, page(0, limit));
|
||||
assert!(state.content.last_page_filled);
|
||||
|
||||
let mut query = ContentQuery::first(ContentKind::Mod);
|
||||
query.text = "sodium".to_string();
|
||||
state.set_content_query(query);
|
||||
assert!(!state.content.last_page_filled, "换条件 ⇒ 清掉上一批的判据");
|
||||
assert!(state.content.pending);
|
||||
assert!(
|
||||
state.content.results.is_none(),
|
||||
"换条件要清列表(不然加载态画旧命中项)"
|
||||
);
|
||||
}
|
||||
|
||||
/// 迟到的一批不许改写判据(用户已经换了条件)。
|
||||
#[test]
|
||||
fn a_stale_batch_does_not_touch_the_page_flag() {
|
||||
let mut state = AppState::new();
|
||||
let limit = ContentQuery::first(ContentKind::Mod).limit;
|
||||
|
||||
apply(&mut state, page(0, limit));
|
||||
let mut query = ContentQuery::first(ContentKind::Mod);
|
||||
query.text = "sodium".to_string();
|
||||
state.set_content_query(query);
|
||||
|
||||
// 旧条件的一页这时才回来:既不改结果,也不改「还有更多」
|
||||
apply(&mut state, page(limit, 5));
|
||||
assert!(!state.content.last_page_filled);
|
||||
assert!(state.content.results.is_none(), "迟到的一批被丢掉");
|
||||
}
|
||||
Reference in new issue
Block a user