Files
Unlucky-Launcher/crates/app-iced/src/pages/download.rs
T
AskaEth f408bf5312 fix(app-iced): M107 r2 复审 —— clippy needless_lifetimes + 搜索中点桶立刻生效
- **P1-1**:`group::active_bucket` 的显式生命周期是多余的(`needless_lifetimes`),省掉之后
  `cargo clippy -p unlucky-app-iced -p unlucky-i18n -p unlucky-presenter --all-targets
  -j 2 -- -D warnings` 才真的	exit 0。上一轮报告的那条「clippy 本轮未复跑」正是红的根因——
  本轮把 clippy 纳入每次门禁(见报告)。
- **P2-1(b)**:搜索期间第二级侧栏原来「可点但没反应」(一致性放宽让桶失效,点击要等清空搜索
  才生效)。现在**点桶 = 退出搜索 + 按该桶过滤**:`SelectMajor` 发现 `has_query()` 就先落选择
  再 `rebuild_after_query("")`,一次点击做完「我要看这个桶」这件事。
  判据:app 层 `clicking_a_bucket_during_a_search_leaves_the_search`(432 passed)。

门禁(真实退出码,无管道):
`cargo fmt --all --check` = 0;`clippy … -D warnings -j 2` = 0;`cargo test -p unlucky-app-iced --lib -j 2` = 432 passed / 0 failed
2026-11-05 02:01:12 +08:00

203 lines
9.9 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.
//! **版本管理页**(原下载页):三栏版面 —— 版本类型 / 主版本 / 列表 + 安装面板。
//!
//! 用户令(信息架构调整)把这一页从「下载」改称「版本管理」:下载页只负责整合包与 Mod/光影,
//! 而这一页管的是**游戏版本与加载器安装**。本文件是那次改版后的页面装配点。
//! **M102 版面订正**:旧版把整页块压到 424 并水平居中(1180 宽下左缘 422、页头标题在 112,
//! 左右各空 ~300px,视觉复审判为「内容缩在中间一条窄栏」)。现在页面块走 [`frame::body`]
//! ——左缘与页头标题同一条线(= 页边距 24),宽 = 块宽 `frame::body_width`(1180 下 1044,
//! 极宽窗封顶 **1440**)。424 只约束**成段散文**(本页没有),三栏的每一栏都吃块宽。
//!
//! ## 版面:三栏,宽度由布局系统裁决
//!
//! ```text
//! ┌─────────┬─────────┬──────────────────────────────┐
//! │ 版本类型 │ 主版本 │ 搜索条 · 命中数 · 计数头 │
//! │ 140/64 │ 140/64 │ 版本行(单行四项) │
//! │ │ │ … │
//! │ │ │ 安装面板(M105 的选项区在这里)│
//! └─────────┴─────────┴──────────────────────────────┘
//! ```
//!
//! 三个数字**没有一个是在本文件里拍的**:它们来自
//! [`VersionRails`](super::layout::VersionRails)(宽档裁决 + 数值表在 `pages/layout/rails.rs`)。
//! 页面只做一件事:把裁决结果交给 [`rails`] 画出来。
//!
//! ## 原顶部 4 个 tab 的去处(不是新造分类)
//!
//! 四个 chip(全部 / 正式版 / 预览版 / 远古版)**语义一字不改**,只是从顶部横排搬到第一栏竖排;
//! 窄到三栏不成立时(可用宽 < 600)它们**原样回到顶部**([`rails::stacked_chips`])——同一条
//! [`Category`] 谓词、同一条 `AppCommand::ListVersions`。
//!
//! ## 意图 → 消息 → 指令(页面只发意图,不做乐观回显)
//!
//! | 用户动作 | 页面消息 | app 层落点 |
//! |---|---|---|
//! | 点分类 tab | [`Message::SetCategory`] | `AppCommand::ListVersions(category)`(`All` 不派:画现有快照) |
//! | 点一行 | [`Message::SelectVersion`] | **只改视图模型**(选中不是业务命令) |
//! | 点「安装」 | [`Message::InstallVersion`] | `AppCommand::CreateInstance(草稿)`(M105:带加载器与实例名) |
//! | 点「取消安装」 | [`Message::CancelInstall`] | `AppCommand::CancelInstall` |
//! | 点「刷新」/「打开日志」 | `Message::Action(RefreshSources)` / `Message::Navigate(Logs)` | `ListVersions` / `AppState::navigate` |
//!
//! ## M105:安装选项(加载器维度)
//!
//! 选中版本**不再直接开工**。面板下半块变成「安装选项」区:五家加载器(原版 / Forge /
//! NeoForge / Fabric / Quilt)的 chip、该加载器的官方版本列表、实例名,点「安装」才派
//! `CreateInstance`。选项的决策与画法在 [`options`](子模块),本文件只负责把模型交给它。
//! 加载器版本列表来自 core 的 `LoaderSource::loader_versions`(经
//! `AppCommand::ListLoaderVersions` 回流到快照);取不到时页面给具名原因并**禁掉确认**——
//! 不许「选完才失败」。
//!
//! 进度条只读 `model.install`:它由 presenter 的 `InstallProgress` 事件经 `apply` 灌进来。
//! 于是「点了没变」只有一种解释——后端还没回事件,这正是要的(P3-2 的纪律)。
//!
//! ## M107:两级侧栏从哪来(第一级 = 版本类型,第二级 = 主版本桶)
//!
//! 第一级 = 版本类型(4 项,固定);第二级 = 主版本桶(由快照里的版本 id 按 [`group`] 的规则归纳,
//! 远古版这类「没有主版本概念」的只有「全部版本」一项)。
//!
//! ## 搜索(三层口径)
//!
//! | 情形 | 画面 |
//! |---|---|
//! | 有命中 | 列表照常;侧栏**位置不动**;搜索条右侧给「N 个匹配」(跨类型读数) |
//! | 一个都不命中 | [`states::no_match`]:没有匹配 + 换短片段 + 清空 |
//! | 有命中但不在当前类型 | [`states::elsewhere`]:N 个在别的类型 + 「看全部类型」 |
//! | 搜索中点第二级桶 | **退出搜索 + 按那个桶过滤**(点桶 = 「我就是要看这个桶」,见 `on_download`) |
//!
//! 查询词写在 presenter 快照的 `download_query` 里(页面无状态,见 [`model::Download::from_snapshot`]),
//! 所以「搜索 → 任何事件回来之后仍然在搜」这条不需要页面持有任何字段。
//!
//! ## 状态矩阵(加载 / 空 / 有数据 / 安装中 / 失败)
//!
//! 与 M94 时一致,唯一的改动是**空态与搜索态都留在三栏骨架里**:侧栏与搜索框照旧在,
//! 只有列表区换成占位内容——白屏会让人以为列表坏了。
//!
//! ## 两个纯视图态的字段(快照里没有,重建会丢)
//!
//! `selected_major`(第二级侧栏选中)与 `type_rail_folded`(类型栏手动折叠)**不在 presenter 快照里**
//! (`AppState` 没有这两个字段,加字段要动 `pages/mod.rs` 的偏离表与 presenter,见 M107 报告)。
//! 所以它们与既有的 `selected` 同一档处理:`on_download` 直接改视图模型,**不重建**;
//! 唯一会重建的路径是搜索(`rebuild_after_query`),那里把两个字段在重建前后各搬一次。
//! 其余事件(安装进度、版本列表刷新)回来时这两个字段会回到默认值——与 `selected` 的既有缺口同源,
//! 长期要靠 presenter 加字段,已登记在报告里。
//!
//! ## 版面判据(M102 口径:页面块 vs 散文列)
//!
//! 页面仍是 `PaneKit::Single`(页内自己切三栏)。整页内容走 [`frame::body`]:块左缘 =
//! 页头标题左缘 = `rail 88 + gutter 24 = 112`,块宽 = [`frame::body_width`](pane 宽封顶
//! **1440**)——视觉复审 ①(内容缩在中间一条窄栏、与页头错位)因此收口。
//! **三栏的列宽从「块宽」算**([`VersionRails::of_metrics`] 读 `frame::body_width`,不是
//! `metrics.available`):否则 1576+ 的超宽窗上三列之和会超出 1440 的块、右侧被裁。
//! 424([`super::layout::measure_cap_px`])在本页只对**成段散文**有效——这一页没有散文。
mod demo;
mod group;
mod install;
mod model;
mod options;
mod rails;
mod rows;
mod states;
#[cfg(test)]
mod tests;
pub use demo::demo;
pub use model::{cancelled_toast, Category, Download};
pub use options::{InstallDraft, Loader};
use iced::widget::column;
use iced::{Element, Length};
use unlucky_design::spacing::Space;
use unlucky_i18n::{Catalog, Key, Lang};
use crate::app::components::buttons::{self, Button};
use crate::app::components::divider;
use crate::theme::Ui;
use super::frame::{self, Header};
use super::layout::{FrameMetrics, VersionRails};
use super::{Action, Message};
/// 整页(**唯一**的真数据入口:`model` 由 `app/view.rs` 从 presenter 快照建)。
///
/// `lang` 是 i18n 的入参,调用点传的是 `pages::Model.lang`(M106 起 = `App` 的生效语言,
/// 见 `ui::lang::LangState`);页面自己不读全局、不猜语言。
pub fn view<'a>(
ui: &'a Ui,
metrics: FrameMetrics,
model: &'a Download,
lang: Lang,
) -> Element<'a, Message> {
let catalog = lang.catalog();
let header = Header::new(catalog.text(Key::DownloadTitle))
.subtitle(catalog.text(Key::DownloadSubtitle))
.action(buttons::text(
ui,
Button::new(
catalog.text(Key::DownloadRefresh),
Message::Action(Action::RefreshSources),
),
));
frame::page(ui, metrics, header, body(ui, metrics, catalog, model))
}
/// 正文:加载态仍是居中占位;其余状态都长在三栏骨架上。
fn body<'a>(
ui: &'a Ui,
metrics: FrameMetrics,
catalog: Catalog,
model: &'a Download,
) -> Element<'a, Message> {
if model.is_loading() {
// 还没拉到版本列表:三栏没有内容可填,给占位(安装面板照旧带着,见 M94 复审 p3)
let content: Element<'a, Message> = if model.install.is_some() {
column![
states::loading(ui, catalog),
divider::full(ui),
install::panel(ui, catalog, model)
]
.spacing(ui.space(Space::X3))
.width(Length::Fill)
.height(Length::Fill)
.into()
} else {
states::loading(ui, catalog)
};
return frame::body(metrics, content);
}
// 宽度裁决:档位来自布局系统,「用户手动折叠了类型栏」这一条覆盖叠在上面。
let rails = {
let rails = VersionRails::of_metrics(metrics);
if model.type_rail_folded {
rails.with_type_folded()
} else {
rails
}
};
let content: Element<'a, Message> = if rails.tier.is_three_column() {
rails::three_column(ui, catalog, model, rails)
} else {
// 三栏不成立(可用宽 < 600):类型与主版本原样退回顶部 chips,列表吃掉整条可用宽
column![
rails::stacked_chips(ui, catalog, model),
divider::full(ui),
rows::content(ui, catalog, model),
]
.spacing(ui.space(Space::X2))
.width(Length::Fill)
.height(Length::Fill)
.into()
};
// M102:整页块走 `frame::body`(贴左、块宽封顶 1440),与页头标题同一条左缘。
// 三栏都在这个块里切,所以 `VersionRails` 读的是块宽而不是 `metrics.available`。
frame::body(metrics, content)
}