merge: origin/dev(1623f68)合入 M197 分支——解 golden/tail.rs 冲突
基线:合的是 **origin/dev**(`1623f68`)。`origin/main` 是 D41 的孤儿发布快照 (`d616d65` parents 为空),不参与合并。用 merge 而不是 rebase,保住三轮复审 clean 的 tip。 冲突只有一个:`crates/i18n/src/catalog/golden/tail.rs`(热点文件,多条 mission 都往这里加 key)。 解法(不重排别人的行、不改别人的译文): 1. 以 **dev 侧 398 行**为底稿(含其它 mission 后加的 key); 2. 把 M197 的 68 行(`SettingsJavaTooNew` … `SettingsJavaReasonInstallRootTooLong`) 整块插在 `SettingsJavaReasonWriteFailed` 之后、M174 助手域(`SettingsGroupAssistant`)之前 —— 即 `settings_java_entries` 那段之后,装配链的正确位置; 3. 两侧合起来 463 行会越过 400 行硬线,于是按既有先例把**M174 助手域起的那半截** (dev 的 260 行)整段纯搬移到 `golden/tail_ai.rs`:`tail.rs` 211 行 / `tail_ai.rs` 274 行, 链序 `golden → tail → tail_ai → tail2` 不变,逐字节不删不改; 4. 解冲突时发现 dev 的 golden 行还是 M197 之前的旧文案(`SettingsJavaEmptyBody` / `SettingsJavaAddHint` 的反引号、`SettingsJavaNeverScannedBody` 的旧说法),已把这三条的 金表行同步回域文件里的现行文案(`split_keeps_every_translation` 逐位置比对当场抓出来的)。 顺带(合并导致的越线):`crates/core/src/download/engine.rs` 两侧各 400 行、合并后 401 (我 +1 行 `kind: err.kind`,dev +1 行),把模块文档里那三行散文重排成两行(一字未删)→ 400 行。 同一次还把这文件里对 `MirrorTable::set_gh_proxy` 的旧描述改成 M193 的现实(那两个函数已被删)。 验证:fmt 0 · check(app-iced+core+i18n all-targets)0 · i18n 36 · engine_mock 8 · app-iced java 45 · core lib java 71 · setting 116 · app-iced lib 全量 832;全仓 file-size 守门脚本复制执行 → 超限 0。
This commit is contained in:
commit
6e80fd3563
283 files changed
+22601
-2188
No files matched your search
@@ -5,15 +5,16 @@
|
||||
//! | 这里 | 内容 |
|
||||
//! |---|---|
|
||||
//! | [`GROUP`] | 左列名 / 图标 / 演示行(产品路径走 [`super::ai::view::body`]) |
|
||||
//! | [`AI_GROUP`] | 本组在 `GROUPS` 里的下标(与 General / Update 同一条**按下标分发**:本组要 i18n 文案与 `Lang` 形参,`Group.body` 的签名里没有它) |
|
||||
//! | [`AI_GROUP`] | 本组在 `GROUPS` 里的下标(与 General / Update 同一条**按下标分发**:本组尚未迁到 `Group.body` 槽位,见下节) |
|
||||
//! | [`Message`] | 本线的消息口(落点 `app/update/settings/ai_about.rs`) |
|
||||
//! | [`State`] | 转发 `ai::State`(`Groups.ai_about` 的载荷类型不变,`settings/state.rs` 一行不用改) |
|
||||
//!
|
||||
//! ## 为什么 `body` 是 `None` 而不是 `Some(..)`
|
||||
//!
|
||||
//! `Group.body` 的签名是 `fn(&Ui, States) -> Option<Element>`:拿不到 `Lang`,也就画不出中英两套
|
||||
//! 文案(本组的硬要求是「不得硬编码中文」)。M106 的语言行与 M159 的更新组是同一个理由走了
|
||||
//! 按下标分发;本组沿用同一条路,代价是 `settings/mod.rs` 里多一行 `else if`。
|
||||
//! `Group.body` 的槽位(M179 起)拿的是 [`States`](super::States) 与**生效语言** `Lang`——本组要的
|
||||
//! 两件它其实都装得下。本组仍走按下标分发,是因为**还没迁过去**:槽位要求返回 `Option<Element>`,
|
||||
//! 而 `ai::view::body` 返回 `Element`,把本组(以及 Java 组)统一迁到槽位上是一次独立重构。
|
||||
//! M106 的语言行与 M159 的更新组当初也是同一个理由走上这条路,代价是 `settings/mod.rs` 多一行 `else if`。
|
||||
//!
|
||||
//! ## 密钥纪律
|
||||
//!
|
||||
|
||||
@@ -5,20 +5,25 @@
|
||||
//! 「Appearance」这个名字**;分组在左列的下标仍由 `crate::pages::appearance::APPEARANCE_GROUP`
|
||||
//! 声明,`settings/tests/mod.rs` 拿它和本文件比对(顺序被调换会红)。
|
||||
//!
|
||||
//! **这一组归 language+appearance 线**:真控件的实现全在 `pages/appearance/section.rs`(本文件只负责
|
||||
//! **这一组归 language+appearance 线**:真控件的实现全在 `pages/appearance/section/`(本文件只负责
|
||||
//! 把它挂进注册表),所以改控件时不用碰设置页目录。
|
||||
//!
|
||||
//! M101 r2:本组的渲染器签名改成收 [`States`](组状态 + 外观状态),于是「这一组要读哪些状态」
|
||||
//! 由本文件决定——`settings/mod.rs` 不再认识 `appearance::State`。
|
||||
//!
|
||||
//! M179 `I18NSEAM`:签名再加一个 `lang`(`Group.body` 的槽位本身就带语言,见 `settings/mod.rs` 的
|
||||
//! `Body` 类型别名),于是这一组的中文界面文案真的走词表——此前它只能写死 `Lang::En`,
|
||||
//! 因为渲染器拿不到「这一次生效的语言」。
|
||||
|
||||
use iced::Element;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::{Group, Message, States};
|
||||
|
||||
/// Appearance 分组。`body` 指着 [`body`](它再转给 `pages/appearance/section.rs` 的真控件);
|
||||
/// Appearance 分组。`body` 指着 [`body`](它再转给 `pages/appearance/section/` 的真控件);
|
||||
/// 没有外观运行期状态时(harness / `pages_demo`)返回 `None`,由 `form` 退回下面的演示行。
|
||||
pub(super) const GROUP: Group = Group {
|
||||
name: "Appearance",
|
||||
@@ -33,8 +38,10 @@ pub(super) const GROUP: Group = Group {
|
||||
};
|
||||
|
||||
/// 本组的真控件渲染器:有外观状态就画真控件(M93),没有(harness)返回 `None`。
|
||||
fn body<'a>(ui: &'a Ui, states: States<'a>) -> Option<Element<'a, Message>> {
|
||||
///
|
||||
/// `lang` 一路递给 `pages::appearance::section`——它是这一组唯一知道「界面现在是什么语言」的东西。
|
||||
fn body<'a>(ui: &'a Ui, states: States<'a>, lang: Lang) -> Option<Element<'a, Message>> {
|
||||
states
|
||||
.appearance
|
||||
.map(|state| crate::pages::appearance::section(ui, state))
|
||||
.map(|state| crate::pages::appearance::section(ui, state, lang))
|
||||
}
|
||||
@@ -1,9 +1,25 @@
|
||||
//! 设置页「Downloads」分组:首选源 / 并发数 / 校验(演示行)。
|
||||
//! 设置页「Downloads」分组:并发 / 单源尝试次数 / GitHub 加速前缀(M193)。
|
||||
//!
|
||||
//! **这一组归 download 线**:接真控件时在本文件把 `body` 换成 `Some(download::body)`(渲染器也写在本文件),
|
||||
//! `settings/mod.rs` 的注册表不用动。
|
||||
//!
|
||||
//! 注意与下载页(`pages/download*`)的区别:那是「装哪个版本」,这里是「怎么下」的默认值。
|
||||
//!
|
||||
//! ## 为什么这三行是「说语义」而不是「说当前值」(M193)
|
||||
//!
|
||||
//! 原来这三行是**写死的演示值**(`("Concurrency", "8 threads")` / `("Preferred source",
|
||||
//! "Automatic (region)")` / `("Verify checksums", "On")`)。用户实测把它们当成真值投诉过
|
||||
//! (「它默认显示 8,可我改成 2 也没变」)——因为 iced 侧**根本没有设置数据通路**:
|
||||
//! `pages::Model` 没有设置快照、`grep -rn "SettingsChanged" crates/app-iced/src` 零命中、
|
||||
//! [`State`] 是个空 struct 且无人填。而这三行里「8 threads」当时还是**假的**:引擎真值曾是 64
|
||||
//! (schema 说 8),「Verify checksums」更是没有任何校验开关(校验恒开)。
|
||||
//!
|
||||
//! 所以这一轮把假值换成**不会撒谎的语义行**:区间、默认值、「留空 / off / 前缀」三态各是什么。
|
||||
//! 真值(用户在 `launcher.json` 里填的那个数)不在这里显示——**宁可不说,也不说假的**。
|
||||
//! 真正的可改控件要等一条 presenter 快照 → app → `Groups.download` 的数据通路,另立 mission。
|
||||
//!
|
||||
//! **生效保证**:这三条设置真的会被引擎读走(`core::download::shared::set_tuning`,宿主侧
|
||||
//! 翻译在 `presenter::host::settings`),所以「行里的语义」与「引擎行为」是同一份契约。
|
||||
|
||||
use unlucky_design::icons::IconKind;
|
||||
|
||||
@@ -14,9 +30,11 @@ pub(super) const GROUP: Group = Group {
|
||||
name: "Downloads",
|
||||
icon: IconKind::Download,
|
||||
rows: &[
|
||||
("Preferred source", "Automatic (region)"),
|
||||
("Concurrency", "8 threads"),
|
||||
("Verify checksums", "On"),
|
||||
("并发数", "出厂 8 · 可调 1~64"),
|
||||
("单源尝试次数", "出厂 3 · 含首次 · 0/1 = 只试一次"),
|
||||
("GitHub 加速前缀", "留空 = 跟随内置默认"),
|
||||
("关掉它", "填 off = 关闭"),
|
||||
("换掉它", "填 URL 前缀 = 覆盖"),
|
||||
],
|
||||
body: None,
|
||||
};
|
||||
@@ -24,6 +42,7 @@ pub(super) const GROUP: Group = Group {
|
||||
/// Downloads 分组的运行期状态(M101 r2:**只归 download 线**)。
|
||||
///
|
||||
/// 加字段只改本文件:`Groups` 只认 `Groups.download` 这一个字段。
|
||||
/// 现在仍然是空的——真值的数据通路还没接(见文件头「为什么这三行是说语义」)。
|
||||
#[derive(Debug, Default)]
|
||||
pub struct State {}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! 设置页骨架:页头、左列分组导航、表单列的滚动区、演示卡片、空态位。
|
||||
//! 设置页骨架:页头、左列分组导航、表单列的滚动区、演示卡片。
|
||||
//!
|
||||
//! ## 为什么这里还再导出一批 `pages::frame` 的名字
|
||||
//!
|
||||
@@ -8,8 +8,8 @@
|
||||
//! 这不是新 API,只是一条转发(新增控件仍归 [`crate::pages::frame`])。
|
||||
|
||||
pub(super) use super::super::frame::{
|
||||
card, divider, empty_state, glyph_box, left_aligned, page, panes, scroll_style, single_column,
|
||||
text_action, text_role, Header, ICON_PX, SCROLLBAR_MARGIN, SCROLLBAR_WIDTH,
|
||||
card, divider, glyph_box, left_aligned, page, panes, scroll_style, single_column, text_action,
|
||||
text_role, Header, ICON_PX, SCROLLBAR_MARGIN, SCROLLBAR_WIDTH,
|
||||
};
|
||||
|
||||
use iced::widget::{column, container, mouse_area, row};
|
||||
@@ -18,7 +18,6 @@ use iced::{Alignment, Background, Border, Element, Length, Padding, Theme};
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::component_map::Component;
|
||||
use unlucky_design::elevation::MaterialLayer;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
@@ -30,9 +29,6 @@ use crate::theme::Ui;
|
||||
|
||||
use super::Group;
|
||||
|
||||
/// 空态位高度:给图标块 96 + 标题 + 副文案 + 按钮留够空间(`[估]`,按 12b §15 的元素最小高度累加)。
|
||||
const EMPTY_SLOT_HEIGHT: f32 = 320.0;
|
||||
|
||||
/// 页头:标题 + 副标题 + 两个占位动作。
|
||||
///
|
||||
/// M106:三条文案走 catalog(`lang` 由 `settings::view` 传下来)。页标题复用 `NavSettings`——
|
||||
@@ -218,21 +214,137 @@ fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Me
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 空态位:没有逐实例覆盖时的样子(12b §15 的口径)。
|
||||
pub(super) fn overrides_empty<'a>(ui: &'a Ui, lang: Lang) -> Element<'a, Message> {
|
||||
let catalog = lang.catalog();
|
||||
container(empty_state(
|
||||
ui,
|
||||
IconKind::Instances,
|
||||
catalog.text(Key::SettingsOverridesEmptyTitle),
|
||||
catalog.text(Key::SettingsOverridesEmptyBody),
|
||||
vec![text_action(
|
||||
ui,
|
||||
catalog.text(Key::SettingsOverridesAdd),
|
||||
Action::AddOverride,
|
||||
)],
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fixed(EMPTY_SLOT_HEIGHT))
|
||||
.into()
|
||||
// --------------------------------------------------------------- M196 守门
|
||||
|
||||
/// M196:设置页正文**不许**在最后一组之后挂一张空态卡。
|
||||
///
|
||||
/// ## 守的是哪一件事
|
||||
///
|
||||
/// 用户 2026-11-08 在 M174 的助手页与 M175 的 Java 页都看到同一张不属于本页的空态卡(四格图标 +
|
||||
/// 「没有逐实例覆盖」+ 说明被窗口下沿截断),两张图 1180×1100、卡片位置完全一致。根因是
|
||||
/// `settings/mod.rs` 的 `form()` 在分组循环之后**无条件**叠了一件固定 320dp 高的空态位
|
||||
/// (M84 骨架期的占位,M99 纯搬移时原样留下)——于是任何一组下面都长出同一张卡。
|
||||
///
|
||||
/// ## 两条判据怎么合起来咬死它
|
||||
///
|
||||
/// 正文由两半拼成:`mod.rs` 的 [`section_columns`](super::section_columns) **生产**「每组一件」的列,
|
||||
/// `form` 只把它们堆起来再滚出去。于是:
|
||||
///
|
||||
/// - [`the_form_stops_at_its_last_group`] 守**收尾**:`form()` 里 `stack = stack.push(` 恰好一处、
|
||||
/// 且以 `frame::scroll(ui, stack)` 收尾 —— 尾部多一件、或整段换成别的拼装(push 数变 0)都红;
|
||||
/// - [`the_form_holds_exactly_one_column_per_group`] 守**生产**:真调 `section_columns` 数它交出的
|
||||
/// 列数(窄窗 == `GROUPS.len()`,单选各组 == 1)—— 多塞一件就红,**不管那件叫什么名字**。
|
||||
///
|
||||
/// 两半合起来 = 「正文恰好这些列,没有尾巴」。
|
||||
///
|
||||
/// ## 为什么住在 `frame.rs` 而不是 `pages/settings/tests/`
|
||||
///
|
||||
/// `tests/` 归四条 section 线所有(各改各的),而这里守的是**本文件与 `mod.rs` 之间的分工**:
|
||||
/// 壳层不提供空态位、分发点不在尾部多堆一件。判据要是哪天挪进 `tests/`,也请原样保留这两条。
|
||||
///
|
||||
/// ## 抓得住 / 抓不住(据实写)
|
||||
///
|
||||
/// 抓得住(三种变异都实测过):①按原样把卡加回循环之后(`stack = stack.push(...)`)⇒ 收尾那条红;
|
||||
/// ②改成「塞进列清单」(`section_columns` 里多 push 一件)⇒ 列数那条红,换任何名字都躲不掉;
|
||||
/// ③把拼装整段重写成别的形状(不再用上面那个惯用法)⇒ 收尾那条数的 1 变成 0,同样红。
|
||||
/// 抓不住:**某一组自己的列里**换内容(比如某个分组真的需要自己的空态)。那不是本缺陷——本页允许
|
||||
/// 组内有空态,前提是它长在该组的列里、用本页语义的文案。`DevDocs/artifacts/m196/` 那组图是**当时**
|
||||
/// 的证据,不是将来的防线(静态图只能证明「现在没有」)。
|
||||
#[cfg(test)]
|
||||
mod no_stray_empty_state {
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::typography::Platform;
|
||||
|
||||
use crate::pages::update::Update;
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
use super::super::{demo_groups, section_columns, Language, States, GROUPS};
|
||||
|
||||
/// 设置页的分发点(本文件是它的被调用方)。
|
||||
const FORM: &str = include_str!("mod.rs");
|
||||
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
/// `form()` 的函数体:从它的签名取到下一个函数(`section_columns`)的签名之前。
|
||||
///
|
||||
/// 只取 `form` 自己:`section_columns` 里面有 `columns.push(`,那是另一条判据管的事。
|
||||
fn form_body() -> &'static str {
|
||||
let start = FORM
|
||||
.find("fn form<'a>(")
|
||||
.expect("settings/mod.rs 里找不到 form():分发点被改名了,本守门要跟着改");
|
||||
let rest = &FORM[start..];
|
||||
let end = rest
|
||||
.find("\nfn section_columns")
|
||||
.expect("settings/mod.rs 里找不到 section_columns():列生产被改名了,本守门要跟着改");
|
||||
&rest[..end]
|
||||
}
|
||||
|
||||
fn ui() -> Ui {
|
||||
Ui::with_platform(SchemeFlavor::Light, Platform::Windows, FAMILIES)
|
||||
}
|
||||
|
||||
/// 演示 / harness 路径那份只读状态(与 `pages::page` 同源,不伪造第二份)。
|
||||
fn states() -> States<'static> {
|
||||
States {
|
||||
appearance: None,
|
||||
groups: demo_groups(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 判据 ①(**收尾**):循环之后没有任何额外的 `push`,且函数以滚出去收尾。
|
||||
///
|
||||
/// 循环体里只有一件(一组一列),所以「`stack = stack.push(` 恰好一次」等于「尾部空着」;
|
||||
/// 拼装被整段重写成别的形状时计数变 0,同样红。
|
||||
#[test]
|
||||
fn the_form_stops_at_its_last_group() {
|
||||
let body = form_body();
|
||||
let pushes = body.matches("stack = stack.push(").count();
|
||||
assert_eq!(
|
||||
pushes, 1,
|
||||
"`form()` 里出现了 {pushes} 处 `stack = stack.push(`:分组循环里只该有一处。\
|
||||
尾部又被叠了一件东西的话,设置页每一组下面都会多出同一件(M196 那张\
|
||||
「没有逐实例覆盖」的卡就是这么来的)"
|
||||
);
|
||||
assert!(
|
||||
body.contains(" frame::scroll(ui, stack)\n}"),
|
||||
"`form()` 必须以 `frame::scroll(ui, stack)` 收尾(后面紧跟函数右花括号):\
|
||||
正文到最后一组就结束,别再往后叠东西"
|
||||
);
|
||||
}
|
||||
|
||||
/// 判据 ②(**生产**):真跑 `section_columns`,量它到底交出几列。
|
||||
///
|
||||
/// 这是行为断言:调的是产品路径上那个真函数,**不认任何名字**。谁往正文里多塞一件(M196 那张
|
||||
/// 「没有逐实例覆盖」的卡当初就是这么塞的),列数就不等于组数。
|
||||
///
|
||||
/// 为什么不走 `tests/scroll_fit.rs` 那条「`()` 渲染器真跑布局」的路:`form` / `section_columns`
|
||||
/// 交出的是**产品渲染器**的 `Element`,要按那条路量子节点数,得把 `general` / `update` / `ai`
|
||||
/// 三条线的 `body` 一并泛型化(它们不在本 mission 的 scope 内)。量列数拿到的是同一个不变量,
|
||||
/// 且不需要泛型。
|
||||
#[test]
|
||||
fn the_form_holds_exactly_one_column_per_group() {
|
||||
let ui = ui();
|
||||
let update = Update::default();
|
||||
|
||||
let narrow = section_columns(&ui, None, states(), Language::default(), &update);
|
||||
assert_eq!(
|
||||
narrow.len(),
|
||||
GROUPS.len(),
|
||||
"窄窗(按组堆叠)的正文应当是「每组一件」:多出来的那一件就是 M196 那张卡的位置"
|
||||
);
|
||||
|
||||
for index in 0..GROUPS.len() {
|
||||
let single = section_columns(&ui, Some(index), states(), Language::default(), &update);
|
||||
assert_eq!(
|
||||
single.len(),
|
||||
1,
|
||||
"supporting-pane 只该画被选中的那一组(下标 {index}),实际拿到 {} 列",
|
||||
single.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,11 @@
|
||||
//!
|
||||
//! ## 这一组为什么特殊
|
||||
//!
|
||||
//! 它**不走** `Group::body`:那个渲染器的签名是 `fn(&Ui, States) -> Option<Element>`,`States` 只有
|
||||
//! `appearance` + `groups` 两格,装不下「当前语言档位 + 生效语言」。语言行要的两件在
|
||||
//! `pages::Language` 里,由 `pages::settings::view` 的 `language` 形参一路递进来、`form` 按下标
|
||||
//! 转交 [`body`];所以 `GROUP.body` 仍是 `None`,`tests/mod.rs` 那条「只有外观组带真控件」照样成立。
|
||||
//! 它**不走** `Group::body`:那个渲染器拿的是 [`States`](super::States) 与**生效语言** `Lang`
|
||||
//! (M179 起;此前连 `Lang` 都没有),而语言行要的是「当前语言**档位** `LangChoice` + 生效语言」
|
||||
//! 这一对——档位不在 `States` 里。所以它由 `pages::settings::view` 的 `language` 形参一路递进来、
|
||||
//! `form` 按下标转交 [`body`];`GROUP.body` 仍是 `None`,`tests/mod.rs` 那条「只有外观组带真控件」
|
||||
//! 照样成立。
|
||||
//!
|
||||
//! ## 语言切换**没有**走 M101 的 `GroupMessage` 投递口(偏离,已登记)
|
||||
//!
|
||||
@@ -77,6 +78,11 @@ pub(super) fn body<'a>(ui: &'a Ui, language: Language) -> Element<'a, Message> {
|
||||
stacked = stacked.push(divider(ui));
|
||||
stacked = stacked.push(font_row(ui, language));
|
||||
|
||||
// M188:日志文件夹(路径 + 打开 + 复制路径)。放在「主题/启动页」之前——用户投诉
|
||||
// 「找不到崩溃日志」要的答案,不该埋在只读行后面。
|
||||
stacked = stacked.push(divider(ui));
|
||||
stacked = stacked.push(super::logs_row::folder_row(ui, &catalog));
|
||||
|
||||
// 主题 / 启动页 / 关闭按钮:还没有后端,只读;文案已经进 catalog。
|
||||
for (label, value) in [
|
||||
(Key::SettingsTheme, Key::SettingsThemeFollowSystem),
|
||||
@@ -284,7 +290,7 @@ fn setting_row<'a>(ui: &'a Ui, label: &'a str, value: &'a str) -> Element<'a, Me
|
||||
|
||||
/// 一个互斥按钮(选中态 `SecondaryContainer`,未选中透明 + `OnSurfaceVariant`)。
|
||||
///
|
||||
/// 与 `pages/appearance/section.rs` 的 `choice` 同款,但文案来自 catalog(那份是英文基线)。
|
||||
/// 与 `pages/appearance/section/mod.rs` 的 `choice` 同款,但文案来自 catalog(那份是英文基线)。
|
||||
fn chip<'a>(ui: &'a Ui, text: &'a str, selected: bool, choice: LangChoice) -> Element<'a, Message> {
|
||||
let height = ui.metrics().control_height;
|
||||
let content_color = if selected {
|
||||
|
||||
@@ -11,12 +11,11 @@
|
||||
//! | `off` / `none` / `direct` / `disable` | [`GhProxy::Off`] | `RegionProfile::with_gh_proxy(None)` —— 一个 gh-proxy 候选都不生成 |
|
||||
//! | 其它非空值 | [`GhProxy::Prefix`] | 用这个前缀覆盖(仍然只在镜像优先/国内判定时启用) |
|
||||
//!
|
||||
//! **为什么只改 `RegionProfile`、不碰 `MirrorTable::set_gh_proxy`**:真正决定「要不要生成
|
||||
//! gh-proxy 候选」的是 `MirrorTable::gh_proxy_candidates`,它读的是 `region.gh_proxy` 与
|
||||
//! `region.mode`(`mirrors.rs:222-241`);表里的 `gh_proxies` 只是「已知可用前缀」的目录
|
||||
//! (`default_gh_proxy()` 用它给设置做默认值)。改表不改档案 = 界面上改了、下载时没变,
|
||||
//! 那是最坏的一种「可配置」。所以本文件只动档案,并在 `downloads/tests.rs` 里用**候选链出现
|
||||
//! 该前缀**来证明它真的生效。
|
||||
//! **为什么只改 `RegionProfile`**:真正决定「要不要生成 gh-proxy 候选」的是
|
||||
//! `MirrorTable::gh_proxy_candidates`,它读的是 `region.gh_proxy` 与 `region.mode`;表里的
|
||||
//! `gh_proxies` 只是「已知可用前缀」的留档(**无生产读者**)。M193 已经把 `set_gh_proxy` /
|
||||
//! `default_gh_proxy` 从表上删掉了,原因正是它们改的是那张没人读的表。所以本文件只动档案,
|
||||
//! 并在 `downloads/tests.rs` 里用**候选链出现该前缀**来证明它真的生效。
|
||||
//!
|
||||
//! 内置默认值本身没有动(`mirrors.json` 仍是 `https://gh-proxy.com/`):空串这一档等于
|
||||
//! 「老文件没有这个键」的行为。
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//! | `crates/i18n/src/catalog/settings/general.rs` / `appearance.rs` | 两组的文案 key |
|
||||
//! | `crates/app-iced/src/app/update/settings/language_appearance.rs` | 消息的落点 |
|
||||
//!
|
||||
//! 真控件那半边在 `pages/appearance/section.rs`(`appearance.rs` 只负责把 `body` 挂上去)。
|
||||
//! 真控件那半边在 `pages/appearance/section/`(`appearance.rs` 只负责把 `body` 挂上去)。
|
||||
|
||||
/// 语言 + 外观线的状态(**只归本线**)。
|
||||
///
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//! 设置页「General」组的**日志文件夹**一行(M188)。
|
||||
//!
|
||||
//! 用户投诉原话:「我们的启动日志放在哪?…他找半天没找到崩溃日志」「只有一个安装日志」。
|
||||
//! 这一行是对那句投诉的直接回答,三件事缺一不可:
|
||||
//!
|
||||
//! 1. **路径永远显示**(不是只在打开成功时才显示):`<数据根>/logs`,三模式解析出来的真值;
|
||||
//! 2. **两个入口**:「打开文件夹」走平台打开器(`app::dialog::export::open_dir`,与崩溃弹窗同一个,
|
||||
//! 不另写一套 `cfg`),「复制路径」把路径送进剪贴板——打开失败时用户至少能照着路径自己去找;
|
||||
//! 3. **说清里面有什么**:`launcher.log`(滚动上限写进文案)与 `logs/game/` 的会话 jsonl,
|
||||
//! 以及旧目录「仍能读、没搬动」——否则用户换了版本会以为旧日志丢了。
|
||||
//!
|
||||
//! 为什么单独一个文件:`pages/settings/general.rs` 贴着 400 行硬线(语言行 + 字体行 + 三行只读),
|
||||
//! 这一行自带按钮与样式,塞回去必破线。
|
||||
|
||||
use iced::widget::{button, column, container, row as row_widget};
|
||||
use iced::{Alignment, Background, Border, Element, Length, Padding, Shadow, Theme as IcedTheme};
|
||||
|
||||
use unlucky_design::color::{Role, StateLayer};
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::catalog::{Catalog, Key};
|
||||
|
||||
use crate::pages::{frame, Action, Message};
|
||||
use crate::theme::Ui;
|
||||
|
||||
/// 这一行(竖排:标签 / 路径 / 两个按钮 / 一句说明)。
|
||||
pub(super) fn folder_row<'a>(ui: &'a Ui, catalog: &Catalog) -> Element<'a, Message> {
|
||||
let path = crate::app::logs_folder::logs_dir()
|
||||
.map(|dir| dir.display().to_string())
|
||||
.unwrap_or_else(|| catalog.text(Key::SettingsLogsUnknown).to_string());
|
||||
|
||||
let hint = catalog.text(Key::SettingsLogsFolderHint);
|
||||
let actions = row_widget![
|
||||
action_button(
|
||||
ui,
|
||||
catalog.text(Key::LogsOpenFolder),
|
||||
Message::Action(Action::OpenLogs),
|
||||
),
|
||||
action_button(
|
||||
ui,
|
||||
catalog.text(Key::SettingsLogsCopy),
|
||||
Message::CopySummary(path.clone()),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
column![
|
||||
frame_text(
|
||||
ui,
|
||||
TypeRole::BodyMedium,
|
||||
catalog.text(Key::SettingsLogsFolder),
|
||||
ui.color(Role::OnSurface),
|
||||
),
|
||||
container(frame_text_owned(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
path,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
))
|
||||
.width(Length::Fill),
|
||||
actions,
|
||||
frame_text(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
hint,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Space::X1))
|
||||
.width(Length::Fill)
|
||||
.padding(Padding::from([ui.space(Space::X2), 0.0]))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn frame_text<'a>(
|
||||
ui: &'a Ui,
|
||||
role: TypeRole,
|
||||
text: &'a str,
|
||||
color: iced::Color,
|
||||
) -> Element<'a, Message> {
|
||||
frame::text_role(ui, role, false, text, color)
|
||||
}
|
||||
|
||||
fn frame_text_owned<'a>(
|
||||
ui: &'a Ui,
|
||||
role: TypeRole,
|
||||
text: String,
|
||||
color: iced::Color,
|
||||
) -> Element<'a, Message> {
|
||||
frame::text_role_owned(ui, role, false, text, color)
|
||||
}
|
||||
|
||||
/// 描边小按钮(与设置页其他「次级动作」同款:透明底、hover 出状态层、全圆角)。
|
||||
fn action_button<'a>(ui: &'a Ui, label: &'a str, message: Message) -> Element<'a, Message> {
|
||||
let height = ui.metrics().control_height;
|
||||
let content = ui.color(Role::OnSurface);
|
||||
let outline = ui.color(Role::OutlineVariant);
|
||||
button(
|
||||
container(frame_text(ui, TypeRole::LabelLarge, label, content)).align_y(Alignment::Center),
|
||||
)
|
||||
.height(height)
|
||||
.padding(Padding::from([0.0, ui.space(Space::X3)]))
|
||||
.style(move |_theme: &IcedTheme, status| {
|
||||
let background =
|
||||
matches!(status, button::Status::Hovered | button::Status::Pressed).then(|| {
|
||||
Background::Color(ui.state_over(StateLayer::Hover, Role::OnSurface, Role::Surface))
|
||||
});
|
||||
button::Style {
|
||||
background,
|
||||
text_color: content,
|
||||
border: Border {
|
||||
color: outline,
|
||||
width: 1.0,
|
||||
radius: ui.radius(ShapeScale::Full, height),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
snap: true,
|
||||
}
|
||||
})
|
||||
.on_press(message)
|
||||
.into()
|
||||
}
|
||||
@@ -25,12 +25,12 @@
|
||||
//! | 分组 | 文件(正文 / 状态 / 消息) | 谁会动它 |
|
||||
//! |---|---|---|
|
||||
//! | General | [`general`] | language+appearance 线(语言下拉 + 主题 / 启动页) |
|
||||
//! | Appearance | [`appearance`] | language+appearance 线(真控件已在 `pages/appearance/section.rs`) |
|
||||
//! | Appearance | [`appearance`] | language+appearance 线(真控件已在 `pages/appearance/section/`) |
|
||||
//! | Java | [`java`] | java 线 |
|
||||
//! | Downloads | [`download`] | download 线 |
|
||||
//! | Updates | [`update`] | update 线(M159,按下标分发) |
|
||||
//! | Assistant | [`ai_about`](正文在 [`ai`]) | ai 线(M174,按下标分发:本组要 `Lang`) |
|
||||
//! | 页头 / 侧列 / 滚动区 / 演示卡片 / 空态位 | [`frame`] | 谁都不该改(改了就是改版面) |
|
||||
//! | 页头 / 侧列 / 滚动区 / 演示卡片 | [`frame`] | 谁都不该改(改了就是改版面) |
|
||||
//!
|
||||
//! ## 测试也按 section 分(M100)
|
||||
//!
|
||||
@@ -106,18 +106,11 @@
|
||||
//! `app/view.rs:70`),四处都要补字段 ⇒ **3 个共享文件**:`settings/state.rs`、`pages/mod.rs`、
|
||||
//! **`app/view.rs`**(最后这个正是 M104 的落点,本 mission 反复设锚点要避开的就是它);
|
||||
//! - 给 `settings::view` **加一个 `model: &Model` 形参**(`pages/mod.rs` 的 `render` 本来就拿着
|
||||
//! `model`,直接往下传)⇒ **2 个共享文件**(`settings/mod.rs` + `pages/mod.rs`),
|
||||
//! `app/view.rs` 不用动 —— **推荐这个形状**。
|
||||
//!
|
||||
//! 顺带订正 r3 之前那句「在 `app/state.rs` 用真值初始化 `Groups.language_appearance`(1 个文件)」:
|
||||
//! **M106 起语言已经有真值源**——`App::lang: LangState`(`crates/app-iced/src/ui/lang.rs`,用户档位
|
||||
//! `LangChoice` + 探测结论 `Detection`),由 `App::refresh_view` 经 `view::model(…, lang)` 灌进
|
||||
//! `pages::Model.lang`,设置页再从 `settings::view` 的 `language` 形参拿到它。所以下一条线要给某一组
|
||||
//! 读「当前语言」时,**不需要**再造一层状态:要么走 `Model.lang`(给 `settings::view` 加形参,
|
||||
//! 与 M106 同形状),要么读 `Groups` 里本线的字段。两种形状都是「谁先撞谁付一次钱」的一次性代价,
|
||||
//! 不是永久性四路撞车。
|
||||
//! `model`,直接往下传)⇒ **2 个共享文件**(`settings/mod.rs` + `pages/mod.rs`),`app/view.rs`
|
||||
//! 不用动 —— **推荐这个形状**。语言也已有真值源(`ui/lang.rs` 的 `App::lang` → `view::model(…)`
|
||||
//! → `Model.lang`):某组读「当前语言」走它或 `Groups` 里本线字段,别再另造一层状态。
|
||||
//! 5. **首帧取数 / 异步发起**(同上,r2 复审 P1 补)—— [`Body`] 是
|
||||
//! `fn(&Ui, States) -> Option<Element>`,返回不了 `Task`;而且 `settings::view` → `pages::render`
|
||||
//! `fn(&Ui, States, Lang) -> Option<Element>`,返回不了 `Task`;而且 `settings::view` → `pages::render`
|
||||
//! → `App::view` **一路都返回 `Element`**(iced 0.14 的 `view` 也只返回 `Element`,`view(&self)`
|
||||
//! 又改不了状态),**view 期间产出的 `Task` 没有出口**。所以只有一条路:在 `app/update.rs` 的
|
||||
//! `Message::Boot` 分支补一次首帧派发(**1 个共享文件**——`on_boot` 本来就在派
|
||||
@@ -143,6 +136,7 @@ mod frame;
|
||||
mod general;
|
||||
pub(crate) mod java;
|
||||
pub(crate) mod language_appearance;
|
||||
mod logs_row;
|
||||
mod message;
|
||||
mod state;
|
||||
pub(crate) mod update;
|
||||
@@ -193,13 +187,16 @@ impl Default for Language {
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个分组正文的**真控件渲染器**:入参是「画一帧时那份只读状态」(组状态 + 外观状态),
|
||||
/// 返回 `None` = 这一组现在没东西可画(例如外观分组在 harness 路径下没有外观状态),
|
||||
/// 由 [`form`] 退回演示行。
|
||||
/// 一个分组正文的**真控件渲染器**:入参是「画一帧时那份只读状态」(组状态 + 外观状态)
|
||||
/// 与**这一次生效的语言**,返回 `None` = 这一组现在没东西可画(例如外观分组在 harness 路径下
|
||||
/// 没有外观状态),由 [`form`] 退回演示行。
|
||||
///
|
||||
/// 各组自己的渲染器**定义在自己文件里**,签名是这一条:于是「这一组要读哪些状态」由它自己决定,
|
||||
/// 本文件不需要认识任何一组的字段(r1 复审 P1 要的正是这一点)。
|
||||
type Body = for<'a> fn(&'a Ui, States<'a>) -> Option<Element<'a, Message>>;
|
||||
///
|
||||
/// `lang` 是 M179 加的:槽位此前拿不到语言,唯一挂了真控件的外观组只能写死英文;接进**唯一的那个
|
||||
/// 槽位**比再给 [`form`] 加一条按下标分发的 `else if` 更省行数(本文件贴着 400 行硬顶)。
|
||||
type Body = for<'a> fn(&'a Ui, States<'a>, Lang) -> Option<Element<'a, Message>>;
|
||||
|
||||
/// 一个设置分组:名字、图标、演示行、(可选)把这一组正文画成真控件的渲染器。
|
||||
///
|
||||
@@ -236,9 +233,9 @@ const SELECTED: usize = 0;
|
||||
|
||||
/// General 分组在 [`GROUPS`] 里的下标(0)。它与 [`SELECTED`] 同值**是巧合,不是依赖**。
|
||||
///
|
||||
/// 为什么需要它:语言行要的是「当前档位 + 生效语言」([`Language`]),而 `Group.body` 的签名只带
|
||||
/// [`States`](外观 + 组状态)——给 `States` 加字段要动 `settings/state.rs` 与 4 处 struct literal
|
||||
/// (见本文件文档「仍然共享」第 4 条),给 `Body` 加形参会令另外三组各自的 `body` 函数编译不过。
|
||||
/// 为什么需要它:语言行要的是「当前档位 + 生效语言」([`Language`]),而 `Group.body` 只带
|
||||
/// [`States`] 与**生效语言**(`Lang`,M179)——档位 `LangChoice` 装不进去(`States` 也装不下:
|
||||
/// 加字段要动 `settings/state.rs` 与 4 处 struct literal,见本文件文档「仍然共享」第 4 条)。
|
||||
/// 所以这里做一次**按下标的分发**:代价是一行 `if`,换来 `general::GROUP.body` 仍是 `None`
|
||||
/// (`tests/mod.rs` 的「只有外观组带真控件 body」照样成立)。
|
||||
const GENERAL_GROUP: usize = 0;
|
||||
@@ -266,9 +263,9 @@ const UPDATE_GROUP: usize = update::UPDATE_GROUP;
|
||||
|
||||
/// 「助手(AI)」分组在 [`GROUPS`] 里的下标(真源在 `settings/ai_about.rs`,这里只转发)。
|
||||
///
|
||||
/// 与 [`GENERAL_GROUP`] / [`UPDATE_GROUP`] 同一条做法:本组要画中英两套文案(`Lang`)与
|
||||
/// 「测试连接」的结论,`Group.body` 的签名里没有 `Lang`,所以由 `view` 的 `language` 形参一路
|
||||
/// 递到 [`form`] 的分发点,`GROUP.body` 保持 `None`。
|
||||
/// 与 [`GENERAL_GROUP`] / [`UPDATE_GROUP`] 同一条做法:由 `view` 的 `language` 形参递到 [`form`]
|
||||
/// 按本下标分发,`GROUP.body` 保持 `None`(本组**尚未**迁到 [`Body`] 槽位:槽位现在带 `Lang` 了,
|
||||
/// 迁它要把 `ai::view::body` 的返回值改成 `Option<Element>`,是一次独立重构——M179 不做)。
|
||||
const AI_GROUP: usize = ai_about::AI_GROUP;
|
||||
|
||||
/// 「Java」分组在 [`GROUPS`] 里的下标(真源在 `settings/java.rs`,这里只转发)。
|
||||
@@ -310,18 +307,17 @@ pub fn view<'a>(
|
||||
|
||||
// --------------------------------------------------------------- 内容分发
|
||||
|
||||
/// 表单列(正文列宽 ≤424)。`only = Some(i)` 只渲染一组(supporting-pane),
|
||||
/// `None` 渲染全部(窄窗:分组导航退化成按组堆叠)。
|
||||
/// 表单列(正文列宽 ≤424)。`only = Some(i)` 只渲染一组(supporting-pane),`None` 渲染全部(窄窗按组堆叠)。
|
||||
///
|
||||
/// **分发点(两条路,M106 起就并存)**:
|
||||
/// **分发点(两条路,M106 起就并存)**:①`GROUP.body`——`Some(f)` 自己去 [`States`] 取数,并从形参拿
|
||||
/// **生效语言**(`None` 退回演示行);②**按下标分发**——General(语言档位)/ Update(presenter 快照)/
|
||||
/// Assistant(`Lang` + 测试结论)/ Java(`Lang` + 扫描槽)在 [`section_columns`] 里各有 `else if`,它们
|
||||
/// 的 `GROUP.body` 仍是 `None`(理由见各自的 `<组>_GROUP` 常量);分组名只作 `GROUPS` 元素的 id。
|
||||
///
|
||||
/// 1. `GROUP.body`——`Some(f)` 让 `f` 自己去 [`States`] 里取它要的数据(`None` 返回值 ⇒ 退回
|
||||
/// 演示行),`None` 直接画演示行([`frame::demo_card`]);
|
||||
/// 2. **按下标分发**——General(要语言档位)、Update(要 presenter 快照)、Assistant(要 `Lang`
|
||||
/// 与测试结论)、Java(要 `Lang` 与扫描槽)四组在下面各有 `else if`,它们的 `GROUP.body`
|
||||
/// 仍是 `None`(理由见各自的 `<组>_GROUP` 常量)。
|
||||
///
|
||||
/// 除此之外这里不认识任何一组的字段:分组名只作为 `GROUPS` 元素的 id 出现。
|
||||
/// **尾部不许再挂东西(M196)**:循环之后原先无条件叠了一张「没有逐实例覆盖」的空态卡(来历见
|
||||
/// `frame.rs`),于是**每一组**下面都长出同一张卡。它**放错了地方**:真控件在**实例详情页**的「启动
|
||||
/// 参数」区(M157 `pages/instances/launch_args.rs` → `AppCommand::set_launch_args`),设置页拿不到那份
|
||||
/// 快照([`view`] 没有 `Model`);无人产出的只有 `Action::AddOverride`。删掉,守门见 `frame.rs` 末尾。
|
||||
fn form<'a>(
|
||||
ui: &'a Ui,
|
||||
only: Option<usize>,
|
||||
@@ -329,9 +325,27 @@ fn form<'a>(
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
) -> Element<'a, Message> {
|
||||
let catalog = language.effective.catalog();
|
||||
let mut stack = column![].spacing(ui.space(Space::X6)).width(Length::Fill);
|
||||
|
||||
for section in section_columns(ui, only, states, language, update) {
|
||||
stack = stack.push(section);
|
||||
}
|
||||
|
||||
frame::scroll(ui, stack)
|
||||
}
|
||||
|
||||
/// 正文里**每组一件**的列(顺序 = [`GROUPS`],只含被选中的那些;分发点见 [`form`] 的文档)。抽出来是
|
||||
/// 为了能被**行为断言**量到:`frame.rs` 的 `the_form_holds_exactly_one_column_per_group` 数它的列数。
|
||||
fn section_columns<'a>(
|
||||
ui: &'a Ui,
|
||||
only: Option<usize>,
|
||||
states: States<'a>,
|
||||
language: Language,
|
||||
update: &'a Update,
|
||||
) -> Vec<Element<'a, Message>> {
|
||||
let catalog = language.effective.catalog();
|
||||
let mut columns = Vec::new();
|
||||
|
||||
for (index, group) in GROUPS.iter().enumerate() {
|
||||
if only.is_some_and(|selected| selected != index) {
|
||||
continue;
|
||||
@@ -357,11 +371,11 @@ fn form<'a>(
|
||||
} else {
|
||||
group
|
||||
.body
|
||||
.and_then(|body| body(ui, states))
|
||||
.and_then(|body| body(ui, states, language.effective))
|
||||
.unwrap_or_else(|| frame::demo_card(ui, group.rows))
|
||||
};
|
||||
|
||||
stack = stack.push(
|
||||
columns.push(
|
||||
column![
|
||||
frame::text_role(
|
||||
ui,
|
||||
@@ -376,11 +390,10 @@ fn form<'a>(
|
||||
card,
|
||||
]
|
||||
.spacing(ui.space(Space::X2))
|
||||
.width(Length::Fill),
|
||||
.width(Length::Fill)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
stack = stack.push(frame::overrides_empty(ui, language.effective));
|
||||
|
||||
frame::scroll(ui, stack)
|
||||
columns
|
||||
}
|
||||
@@ -2,12 +2,25 @@
|
||||
//! 一条 section 断言都没有;这 3 条是新增,不是搬移)。
|
||||
//!
|
||||
//! **归 language+appearance 线**:这是设置页唯一接了真控件的一组,真控件实现全在
|
||||
//! `pages/appearance/section.rs`;本文件只守「注册表这一侧的接线」。
|
||||
//! `pages/appearance/section/`;本文件只守「注册表这一侧的接线」。
|
||||
//! 本组已经把 `body` 接上了;`tests/mod.rs` 那条「只有一组有真控件」的**聚合绊线已在 M101 r2 删除**
|
||||
//! (被四条 per-group 断言完全蕴含——代价是「唯一性」从此没人守,那段说明写在 `tests/mod.rs` 同处)。
|
||||
//!
|
||||
//! M179 `I18NSEAM` 加的第 4 条守的是**语言真的进到了渲染层**(此前这一组在中文界面上 0/27 条本地化)。
|
||||
|
||||
use super::super::appearance;
|
||||
use super::super::{demo_groups, view, Language, States};
|
||||
use unlucky_design::color::SchemeFlavor;
|
||||
use unlucky_design::icons::IconKind;
|
||||
use unlucky_i18n::catalog::Key;
|
||||
use unlucky_i18n::{Lang, LangChoice};
|
||||
use unlucky_presenter::prefs::{AppearanceBase, AppearancePrefs};
|
||||
|
||||
use crate::cli::Backend;
|
||||
use crate::pages::appearance::{probe, State as AppearanceState};
|
||||
use crate::pages::layout::{frame_metrics, PaneKit};
|
||||
use crate::pages::Update;
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
/// 这一组的**演示行**(没有外观运行期状态时才画)。
|
||||
///
|
||||
@@ -50,3 +63,109 @@ fn the_appearance_group_keeps_a_real_control_body() {
|
||||
`body` 是本文件那个本地 fn;具体渲染器由那一行源码 + 截图担保,函数指针相等性断言不了)"
|
||||
);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- 语言(M179 I18NSEAM)
|
||||
|
||||
/// **本组的语言绊线**:中文界面下,外观分组渲染出来的字里**必须含汉字**。
|
||||
///
|
||||
/// 走的是真路:`settings::view`(真语言分发)→ `Group.body(lang)` → `pages::appearance::section`
|
||||
/// → `section::t(key, lang)`;取词口 `t` 把每条译文记进 `pages::appearance::probe`,测试断言这批字。
|
||||
///
|
||||
/// 为什么不在 `Element` 上遍历:iced 0.14 **没有**可遍历控件树的公开接口(`Widget::children()`
|
||||
/// 返回的是状态 `Tree`;走 `operate` 要 `Tree` + 布局 + `Renderer`,单测里没有渲染器)。取证点
|
||||
/// 因此退到「字进树之前」的唯一取词口,代价是**看不见像素**——字体能不能画出汉字要截图判
|
||||
/// (M179 报告「没验的部分」列了这条)。
|
||||
///
|
||||
/// 抓得住:① `section::t` 退回写死 `Lang::En`(M179 之前正是这一行 ⇒ 中文界面上这一组 0/27
|
||||
/// 条本地化);② `settings::view` 不把 `language.effective` 递给 `Group.body`(语言到不了渲染层);
|
||||
/// ③ 五个 key 的译文在金表里丢了中文。
|
||||
/// 抓不住:本组其余文案仍是英文**字面量**(没有 key,M172 口径的 27 条里占 22 条)——这条守的是
|
||||
/// 语言的**管子**,不是文案;补文案是下一条 mission(M179 报告逐行列了那些字面量)。
|
||||
#[test]
|
||||
fn the_appearance_group_speaks_chinese_when_the_interface_does() {
|
||||
let zh = appearance_texts(Lang::ZhCn);
|
||||
assert!(
|
||||
!zh.is_empty(),
|
||||
"外观分组一条词表文案都没取到:渲染路径没走到 `section::t`(语言管子断了?)"
|
||||
);
|
||||
assert!(
|
||||
zh.iter()
|
||||
.any(|(key, _)| *key == Key::SettingsAppearanceCorner),
|
||||
"渲染路径没画到「窗口圆角」那一行:取词记录里没有它的 key,{zh:?}"
|
||||
);
|
||||
|
||||
let han: Vec<&str> = zh
|
||||
.iter()
|
||||
.map(|(_, text)| *text)
|
||||
.filter(|text| text.chars().any(is_han))
|
||||
.collect();
|
||||
assert!(
|
||||
!han.is_empty(),
|
||||
"中文界面下外观分组渲染出来的字全是非汉字(`section::t` 没跟随语言):{zh:?}"
|
||||
);
|
||||
|
||||
// 反向:英文界面下同一批字里不许出现汉字(否则「含汉字」可能来自写死的中文,而不是语言管子)。
|
||||
let en = appearance_texts(Lang::En);
|
||||
assert!(
|
||||
en.iter().all(|(_, text)| !text.chars().any(is_han)),
|
||||
"英文界面下外观分组出现了汉字:{en:?}"
|
||||
);
|
||||
let corner = |texts: &[(Key, &'static str)]| {
|
||||
texts
|
||||
.iter()
|
||||
.find(|(key, _)| *key == Key::SettingsAppearanceCorner)
|
||||
.map(|(_, text)| *text)
|
||||
};
|
||||
assert_ne!(
|
||||
corner(&zh),
|
||||
corner(&en),
|
||||
"同一个 key 在中英两种语言下取到了同一条译文:语言没生效"
|
||||
);
|
||||
}
|
||||
|
||||
/// 画一帧设置页(选中「外观」分组)并取走这一帧从词表取到的全部译文。
|
||||
fn appearance_texts(lang: Lang) -> Vec<(Key, &'static str)> {
|
||||
const FAMILIES: Families = Families {
|
||||
text: "Segoe UI",
|
||||
display: "Segoe UI",
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
let ui = Ui::new(SchemeFlavor::Light, FAMILIES);
|
||||
let state = AppearanceState::new(
|
||||
&AppearancePrefs::factory(),
|
||||
&AppearanceBase::factory(),
|
||||
Backend::Auto,
|
||||
SchemeFlavor::Light,
|
||||
);
|
||||
assert_eq!(
|
||||
state.selected_group,
|
||||
crate::pages::appearance::APPEARANCE_GROUP,
|
||||
"工厂态默认选中「外观」分组(否则这一帧根本画不到本组)"
|
||||
);
|
||||
// 前面几条用例不取词,这里先清一次,避免把别的渲染留下的记录算进来。
|
||||
probe::take();
|
||||
|
||||
let metrics = frame_metrics(PaneKit::Supporting, 1180.0);
|
||||
let update = Update::default();
|
||||
let _tree = view(
|
||||
&ui,
|
||||
metrics,
|
||||
States {
|
||||
appearance: Some(&state),
|
||||
groups: demo_groups(),
|
||||
},
|
||||
Language {
|
||||
choice: LangChoice::System,
|
||||
effective: lang,
|
||||
},
|
||||
&update,
|
||||
);
|
||||
|
||||
probe::take()
|
||||
}
|
||||
|
||||
/// 有没有汉字(判据与 `app/view/tests/launch.rs` 的 `is_cjk` 同一段码位)。
|
||||
fn is_han(ch: char) -> bool {
|
||||
('\u{4e00}'..='\u{9fff}').contains(&ch)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
//! 一条 section 断言都没有;这 3 条是新增,不是搬移)。
|
||||
//!
|
||||
//! **归 download 线**。接真控件那天要改**两个**文件(M101 r2 删掉了原先的第 3 处跨 section 绊线):
|
||||
//! `settings/download.rs` + 本文件 `the_downloads_group_is_still_demo_rows`。
|
||||
//! `settings/download.rs` + 本文件。
|
||||
//!
|
||||
//! 别把它和下载页(`pages/download*`)搞混:那是「装哪个版本」,这一组是「怎么下」的默认值。
|
||||
|
||||
@@ -11,18 +11,20 @@ use unlucky_design::icons::IconKind;
|
||||
|
||||
/// 这一组的行集合(标签 + 值)。
|
||||
///
|
||||
/// 抓得住:① 四条线并行合并时把别人的行粘到本文件(四份 `GROUP` 长得几乎一样,最容易整块贴错);
|
||||
/// ② 漏掉/多出一条行;③ 标签与值错位(`("Concurrency", "8 threads")` 被写成 `"Off"`)。
|
||||
/// 抓不住:值对不对(`Automatic (region)` 是不是本机真选中的源)——那是接线后真数据源的事。
|
||||
/// 抓得住:① 四条线并行合并时把别人的行粘到本文件(六份 `GROUP` 长得几乎一样,最容易整块贴错);
|
||||
/// ② 漏掉/多出一条行;③ 标签与值错位。
|
||||
/// 抓不住:值对不对——这三行**本来就不是值**(M193 起改成语义行,见下一条用例)。
|
||||
#[test]
|
||||
fn the_downloads_group_shows_source_concurrency_and_checksums() {
|
||||
fn the_downloads_group_shows_concurrency_retries_and_the_gh_proxy_key() {
|
||||
let rows: Vec<(&str, &str)> = download::GROUP.rows.to_vec();
|
||||
assert_eq!(
|
||||
rows,
|
||||
vec![
|
||||
("Preferred source", "Automatic (region)"),
|
||||
("Concurrency", "8 threads"),
|
||||
("Verify checksums", "On"),
|
||||
("并发数", "出厂 8 · 可调 1~64"),
|
||||
("单源尝试次数", "出厂 3 · 含首次 · 0/1 = 只试一次"),
|
||||
("GitHub 加速前缀", "留空 = 跟随内置默认"),
|
||||
("关掉它", "填 off = 关闭"),
|
||||
("换掉它", "填 URL 前缀 = 覆盖"),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -33,17 +35,61 @@ fn the_downloads_group_keeps_its_icon() {
|
||||
assert_eq!(download::GROUP.icon, IconKind::Download);
|
||||
}
|
||||
|
||||
/// **绊线**:这一组还没接真控件,画的是演示行。
|
||||
/// **M193 换了绊线的内容,但没撤绊线**。
|
||||
///
|
||||
/// 抓得住:download 线接上了 `body` 却没更新这里的期望(那会让本文件静默变成假绿)。
|
||||
/// 抓不住:界面上真控件长什么样。
|
||||
/// 原绊线是 `the_downloads_group_is_still_demo_rows`:断言 `body.is_none()`(这一组还没接真控件)。
|
||||
/// 它存在的意义是「防止演示值回潮」。M193 撤掉的只是**演示值**(`("Concurrency", "8 threads")`
|
||||
/// 这类):那个 8 与引擎真实的 64 对不上,用户按它调设置自然没效果。
|
||||
///
|
||||
/// 接真控件时:把 `is_none()` 改成 `is_some()`,**只改这一行**(M101 r2 起不必再去
|
||||
/// `tests/mod.rs` 报到:那条跨 section 聚合绊线已删)。
|
||||
/// 现在改成两条更硬的断言:
|
||||
///
|
||||
/// 1. **仍然没有真控件**(`body.is_none()`)——真值的通路还没接(`State` 是空 struct、
|
||||
/// iced 侧没有设置快照),所以这一组还是静态行;接上那天把这条改成 `is_some()` 并接数据。
|
||||
/// 2. **行里不许再出现「当前值」形态的字面量**:三行必须把「出厂值 / 区间 / 三态」讲清楚,
|
||||
/// 且 gh-proxy 那一行必须**同时**说清「留空 = 跟随内置默认」与「填 off = 关闭」——
|
||||
/// 这两者最容易被混(一个「我没决定」,一个是「我明确不要」)。
|
||||
///
|
||||
/// 变异证明:把 `download.rs` 的 gh_proxy 行改回 `("Preferred source", "Automatic (region)")`
|
||||
/// ⇒ 第 2 条红;把 `enabled` 语义写成「留空 = 关闭」⇒ 第 2 条红。
|
||||
#[test]
|
||||
fn the_downloads_group_is_still_demo_rows() {
|
||||
fn the_downloads_rows_state_semantics_and_never_fake_a_current_value() {
|
||||
assert!(
|
||||
download::GROUP.body.is_none(),
|
||||
"Downloads 还没接真控件:接了就改本断言"
|
||||
"Downloads 还没接真控件:接了就改成 is_some() 并接数据通路"
|
||||
);
|
||||
|
||||
let joined = download::GROUP
|
||||
.rows
|
||||
.iter()
|
||||
.map(|(label, value)| format!("{label} {value}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ");
|
||||
|
||||
for required in [
|
||||
// 三态里最容易被混的两档,必须都在(缺一个就是「设置项说假话」的种子)。
|
||||
"留空 = 跟随内置默认",
|
||||
"填 off = 关闭",
|
||||
"填 URL 前缀 = 覆盖",
|
||||
// 硬上限必须在界面上说清(任务书的边界条款)。
|
||||
"1~64",
|
||||
// P2-1:0 是旧版提供过的档位,界面必须说清它 = 只试一次(不是「不下载」)。
|
||||
"0/1 = 只试一次",
|
||||
// 三个键各自是哪一行(标签本身也是契约:用户按它认设置)。
|
||||
"并发数",
|
||||
"单源尝试次数",
|
||||
"GitHub 加速前缀",
|
||||
] {
|
||||
assert!(
|
||||
joined.contains(required),
|
||||
"行里缺少「{required}」:{joined}"
|
||||
);
|
||||
}
|
||||
|
||||
// 「当前值」形态的假数据:`8 threads` / `On` / `Automatic (region)` 都是当年被投诉的那类。
|
||||
for forbidden in ["threads", "Automatic (region)", "Verify checksums"] {
|
||||
assert!(
|
||||
!joined.contains(forbidden),
|
||||
"又出现假的当前值「{forbidden}」:{joined}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
//! 假值不许回来,语言行的三档必须真的能在两套目录里取到词。
|
||||
//!
|
||||
//! 接线形状:语言行的正文由 [`general::body`] 画(入口是 `pages::settings` 的 `GENERAL_GROUP`
|
||||
//! 按下标分发),`GROUP.body` 仍是 `None`——那个槽位的签名是 `fn(&Ui, States) -> Option<Element>`,
|
||||
//! `States` 只有 `appearance` + `groups` 两格,装不下「当前语言档位」。所以 M101 r2 的
|
||||
//! 按下标分发),`GROUP.body` 仍是 `None`——那个槽位现在带**生效语言**(`Lang`,M179)但仍是
|
||||
//! `States` + 语言,装不下「当前语言档位」(`LangChoice`)。所以 M101 r2 的
|
||||
//! 「本组接真控件只需改两个文件」这条口径,在语言行上**仍然是两个文件**。
|
||||
|
||||
use super::super::general;
|
||||
@@ -187,8 +187,8 @@ fn the_language_row_offers_three_choices_in_both_languages() {
|
||||
/// **绊线**:这一组仍不走 `Group.body`。
|
||||
///
|
||||
/// 为什么不是「接了真控件」:语言行的入口是 `pages::Language`(`GENERAL_GROUP` 那条分发),
|
||||
/// 而 `Group.body` 那个槽位的签名是 `fn(&Ui, &pages::appearance::State)`——装不下「当前语言档位」。
|
||||
/// 谁把语言行塞进 `body`,就得先给它换签名(那会改到另外三组的文件),这条断言逼他先想一遍。
|
||||
/// 而 `Group.body` 那个槽位只拿到 `States` 与**生效语言**(`Lang`,M179)——装不下「当前语言档位」。
|
||||
/// 谁把语言行塞进 `body`,就得先给它换签名(那会改到别人那个组的文件),这条断言逼他先想一遍。
|
||||
#[test]
|
||||
fn the_general_group_does_not_use_the_appearance_body_slot() {
|
||||
assert!(
|
||||
@@ -196,3 +196,52 @@ fn the_general_group_does_not_use_the_appearance_body_slot() {
|
||||
"General 的真控件走 pages::Language 入口(见 settings/mod.rs 的 GENERAL_GROUP),不是 body"
|
||||
);
|
||||
}
|
||||
|
||||
/// **M188:日志文件夹那一行**的文案必须与实现一致(用户投诉「找不到崩溃日志」的直接判据)。
|
||||
///
|
||||
/// 抓得住:①文案里的路径写错(写成 `data/logs`、`cache/game`);②滚动上限与
|
||||
/// `core::launch::launcher_log::MAX_BYTES` 脱钩(文案说 1 MiB、实现改成 4 MiB);
|
||||
/// ③旧目录被静默改成「已迁移」(那会让用户以为旧日志没了);
|
||||
/// ④某条 key 只进了英文表(中文界面上是 `⟪missing⟫`)。
|
||||
/// 抓不住:界面画出来长什么样(截图验收管那一层)。
|
||||
#[test]
|
||||
fn the_logs_row_names_the_real_paths_and_the_real_limit() {
|
||||
use unlucky_core::launch::launcher_log;
|
||||
use unlucky_core::platform::layout::{LauncherLayout, GAME_LOG_DIR, LOGS_DIR};
|
||||
|
||||
// 实现侧的真值:分区名与上限都只从 core 来。
|
||||
assert_eq!(LOGS_DIR, "logs");
|
||||
assert_eq!(GAME_LOG_DIR, "game");
|
||||
assert_eq!(launcher_log::MAX_BYTES, 1024 * 1024, "文案说 1 MiB");
|
||||
let layout = LauncherLayout::new(std::path::Path::new("/tmp/ull-home"));
|
||||
assert_eq!(
|
||||
layout.game_logs(),
|
||||
std::path::Path::new("/tmp/ull-home/logs/game")
|
||||
);
|
||||
|
||||
for lang in Lang::ALL {
|
||||
let catalog = lang.catalog();
|
||||
let hint = catalog.text(Key::SettingsLogsFolderHint);
|
||||
for needle in [
|
||||
"launcher.log",
|
||||
"1 MiB",
|
||||
&format!("{LOGS_DIR}/{GAME_LOG_DIR}/"),
|
||||
"data/agent/sessions",
|
||||
] {
|
||||
assert!(
|
||||
hint.contains(needle),
|
||||
"{} 缺 `{needle}`:{hint}",
|
||||
lang.tag()
|
||||
);
|
||||
}
|
||||
assert!(!hint.contains('⟪'), "{} 目录里缺词:{hint}", lang.tag());
|
||||
assert!(
|
||||
!catalog.text(Key::SettingsLogsFolder).contains('⟪'),
|
||||
"{} 缺「日志文件夹」:{}",
|
||||
lang.tag(),
|
||||
catalog.text(Key::SettingsLogsFolder)
|
||||
);
|
||||
assert!(!catalog.text(Key::SettingsLogsCopy).contains('⟪'));
|
||||
assert!(!catalog.text(Key::SettingsLogsUnknown).contains('⟪'));
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,8 @@ fn the_java_group_keeps_the_folder_icon_from_the_visual_review() {
|
||||
|
||||
/// `body` 槽位仍是 `None`:本组靠 `settings/mod.rs` 的按下标分发([`java::JAVA_GROUP`])画正文。
|
||||
///
|
||||
/// 抓得住:谁把 `body: Some(..)` 挂上来却不给它 `Lang`(那样中文界面上这一组会退回英文)。
|
||||
/// 抓得住:谁把 `body: Some(..)` 挂上来(M179 之后槽位自带 `Lang`,所以「挂了 body 却没语言」
|
||||
/// 这种漂移不再可能——挂上来就必然拿到语言;这条守的是**分发形状**别变)。
|
||||
/// 抓不住:分发那一行本身有没有被删(`settings/mod.rs` 的 `form` 里有 `else if`;删了这组就空白)。
|
||||
#[test]
|
||||
fn the_java_group_is_dispatched_by_index_not_by_body() {
|
||||
|
||||
@@ -163,9 +163,10 @@ fn the_split_kept_the_left_column_order() {
|
||||
/// 抓不住:各组的**字段**(那些活在各组自己的文件里,本文件本来就不该知道,也就没有可断言的东西)。
|
||||
///
|
||||
/// **M106 的例外要读清楚**:General 已经接了真控件(语言行),但它**不走 `Group.body`**——
|
||||
/// 那个槽位的签名是 `fn(&Ui, States) -> Option<Element>`,`States` 只有 `appearance` + `groups`
|
||||
/// 两格,装不下「当前语言档位 + 生效语言」。它走 `settings::view` 的 `language` 形参 +
|
||||
/// `GENERAL_GROUP` 那条按下标分发,`body` 仍是 `None`,所以本断言不动。
|
||||
/// 那个槽位的签名是 `fn(&Ui, States, Lang) -> Option<Element>`(`Lang` 是 M179 加的),
|
||||
/// `States` 只有 `appearance` + `groups` 两格、槽位只有**生效语言**,都装不下「当前语言档位 +
|
||||
/// 生效语言」这一对。它走 `settings::view` 的 `language` 形参 + `GENERAL_GROUP` 那条按下标分发,
|
||||
/// `body` 仍是 `None`,所以本断言不动。
|
||||
/// 判据没变:**`body: Some(..)` 仍然只有外观组**。
|
||||
#[test]
|
||||
fn the_settings_module_does_not_name_any_group_state() {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
//!
|
||||
//! ## 为什么这一组不走 `Group.body`
|
||||
//!
|
||||
//! `Group.body` 的签名是 `fn(&Ui, States) -> Option<Element>`,而 `States` 只有「外观状态 + 各组
|
||||
//! 自己的状态」两格。更新状态是 **presenter 快照**(不是某一组的字段),与 M106 的语言行同一种
|
||||
//! 情形:`settings/mod.rs` 按下标([`UPDATE_GROUP`])把 `settings::view` 的 `update` 形参交给
|
||||
//! [`body`],`GROUP.body` 仍是 `None`。
|
||||
//! `Group.body` 的槽位拿的是 [`States`](super::States) 与**生效语言** `Lang`(M179 起),而更新状态
|
||||
//! 是 **presenter 快照**(`pages::Update`,不是某一组的字段、也不在 `States` 里),槽位装不下它。
|
||||
//! 与 M106 的语言行同一种情形:`settings/mod.rs` 按下标([`UPDATE_GROUP`])把 `settings::view` 的
|
||||
//! `update` 形参交给 [`body`],`GROUP.body` 仍是 `None`。
|
||||
//!
|
||||
//! 消息也**不**走 `GroupMessage`:更新动作是「跨页的一次性意图」(检查 / 下载安装 / 忽略 /
|
||||
//! 重启),落点是 `app/update/update_action.rs`,`pages::Message::Update(UpdateAction)` 一支就够。
|
||||
|
||||
Reference in new issue
Block a user