M201: 接通 iced 设置页的数据通路(下载组:真值 + 真控件 + 写回闭环)
M193 的用户投诉「设置页里改了不生效」的结构性根因是 iced 侧没有设置数据通路:
`CoreEvent::SettingsChanged` 在 `crates/app-iced/src` 零命中、`pages::Model` 没有设置快照、
`download::State` 是空 struct。presenter 那套设置组早就是真值来源(`host::settings::groups`
由 `refresh_all` 与 `dispatch` 的 `SetSetting` 分支发成 `SettingsChanged`),只是没有接到 iced。
这条线现在通了:
host::settings::groups → SettingsChanged → AppState::settings_groups
→ pages::Model.settings → settings::States.settings → 下载组控件
→ AppCommand::SetSetting → 写盘 + 重新发 SettingsChanged(闭环)
主要改动
- presenter `host/settings/download.rs`:并发/重试两行改成画**引擎生效值**(`DownloadTuning`),
文件原值与生效值不一致时写明「原值 X 已按出厂 N 生效」(P3-5,两个数都现读);
gh-proxy 行补「当前生效:跟随内置默认/已关闭/覆盖为 X」。
- app-iced `pages/settings/download.rs`:`GROUP.body = Some(body)`,按 `SettingControl`
画 choice(filter chip)/ text(输入框 + 应用)/ action(按钮)/ toggle / readonly;写回走
`app/update/settings/download.rs` → `AppCommand::SetSetting`(不做乐观回显)。
- `pages::Model.settings` + `settings::States.settings`:快照进页面的唯一通道。
- i18n:新增 `SettingsDownloadsApply`(金表锚点追加,`split_keeps_every_translation` 绿)。
- 取证:`ULL_SCREENSHOT_REAL_HOST=1` 让 `--screenshot` 接真 host(截图通道默认强制 mock,
而 mock 不发 `SettingsChanged`,真值屏拍不出来)。
测试(新增 11 条)
- presenter `download_tests.rs`:P3-5 的越界回落 / 0→1 / 无差异不出提示 + P3-3 的
gh-proxy 文件→三态端到端链。
- app-iced `app/update/settings/download_tests.rs`:假后端闭环 ×4 + **真 `CoreHost` 端到端**
(点 16 ⇒ 磁盘上 `launcher.json` 真的有 16)。
- `pages/settings/tests/download_section.rs`:`body.is_none()` 绊线翻成 `is_some()` +
控件形状 / 空快照不编值。
This commit is contained in:
1 parent
0cc1a35420
commit
ab573838f3
18 files changed
+1257
-74
No files matched your search
@@ -22,14 +22,15 @@
|
||||
//! 「读起来是纯读、实际改全局状态」正是本仓吃过亏的那类形状。
|
||||
|
||||
use unlucky_core::download::config::{
|
||||
DownloadTuning, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY, MAX_CONCURRENCY,
|
||||
DownloadTuning, GhProxyOverride, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY,
|
||||
MAX_CONCURRENCY,
|
||||
};
|
||||
use unlucky_core::download::shared;
|
||||
|
||||
use crate::dto::{SettingGroupView, SettingRowView};
|
||||
|
||||
use super::super::env::HostEnv;
|
||||
use super::{int, selected, text};
|
||||
use super::{selected, text};
|
||||
|
||||
/// 并发数的推荐档位(`selected()` 找不到时回 0;手改出来的非档位值会被插进选项表,见 [`group`])。
|
||||
const CONCURRENCY_LADDER: [&str; 5] = ["4", "8", "16", "32", "64"];
|
||||
@@ -37,19 +38,28 @@ const CONCURRENCY_LADDER: [&str; 5] = ["4", "8", "16", "32", "64"];
|
||||
const RETRY_LADDER: [&str; 6] = ["0", "1", "2", "3", "5", "10"];
|
||||
|
||||
/// 下载(D7):并发/重试/地区/镜像偏好/代理/GitHub 加速前缀。
|
||||
///
|
||||
/// ## P3-5:这两行显示的是**引擎生效值**,不是文件原值
|
||||
///
|
||||
/// `download.concurrency` / `download.retry_limit` 的生效判定在 core([`DownloadTuning`]):
|
||||
/// 越界、负数、手改坏值一律**回落出厂值**(不夹到边界——「想 9999 给 64」仍然是界面说谎)。
|
||||
/// 所以这一组必须画 [`tuning_of`] 的结论;文件里的原值只在**两者不一致**时作为一句解释出现
|
||||
/// (「原值 9999 已按出厂 8 生效」)。判定的两个输入都是**现读**的:文件原值取自
|
||||
/// `doc().get(..)`(显式写过的值),生效值取自 core 的翻译层 —— 没有一个数字来自常量或参数。
|
||||
pub(super) fn group(env: &HostEnv) -> SettingGroupView {
|
||||
let concurrency = int(
|
||||
env,
|
||||
unlucky_core::download::config::KEY_CONCURRENCY,
|
||||
let tuning = tuning_of(env);
|
||||
let concurrency = (tuning.max_concurrency as i64).to_string();
|
||||
let retry = i64::from(tuning.max_attempts_per_source).to_string();
|
||||
let concurrency_note = applied_note(
|
||||
raw_int(env, unlucky_core::download::config::KEY_CONCURRENCY),
|
||||
tuning.max_concurrency as i64,
|
||||
DEFAULT_MAX_CONCURRENCY as i64,
|
||||
)
|
||||
.to_string();
|
||||
let retry = int(
|
||||
env,
|
||||
unlucky_core::download::config::KEY_RETRY_LIMIT,
|
||||
);
|
||||
let retry_note = applied_note(
|
||||
raw_int(env, unlucky_core::download::config::KEY_RETRY_LIMIT),
|
||||
i64::from(tuning.max_attempts_per_source),
|
||||
i64::from(DEFAULT_MAX_ATTEMPTS_PER_SOURCE),
|
||||
)
|
||||
.to_string();
|
||||
);
|
||||
let concurrency_options = ladder(&CONCURRENCY_LADDER, &concurrency);
|
||||
let retry_options = ladder(&RETRY_LADDER, &retry);
|
||||
let concurrency_options: Vec<&str> = concurrency_options.iter().map(String::as_str).collect();
|
||||
@@ -64,8 +74,11 @@ pub(super) fn group(env: &HostEnv) -> SettingGroupView {
|
||||
.with(SettingRowView::choice(
|
||||
unlucky_core::download::config::KEY_CONCURRENCY,
|
||||
"并发数",
|
||||
&format!(
|
||||
"同时下载的文件数:出厂 {DEFAULT_MAX_CONCURRENCY},可调 1~{MAX_CONCURRENCY}(引擎硬上限,防止填 9999 把机器拖死);越界或手改坏值按出厂值生效"
|
||||
&with_note(
|
||||
format!(
|
||||
"同时下载的文件数:出厂 {DEFAULT_MAX_CONCURRENCY},可调 1~{MAX_CONCURRENCY}(引擎硬上限,防止填 9999 把机器拖死);越界或手改坏值按出厂值生效"
|
||||
),
|
||||
concurrency_note.as_deref(),
|
||||
),
|
||||
&concurrency_options,
|
||||
selected(&concurrency_options, &concurrency),
|
||||
@@ -73,8 +86,11 @@ pub(super) fn group(env: &HostEnv) -> SettingGroupView {
|
||||
.with(SettingRowView::choice(
|
||||
unlucky_core::download::config::KEY_RETRY_LIMIT,
|
||||
"单源尝试次数",
|
||||
&format!(
|
||||
"同一个源最多试几次(含首次;出厂 {DEFAULT_MAX_ATTEMPTS_PER_SOURCE})。0 与 1 都等于「只试一次、不重试」——0 是旧版提供的档位,不代表「不下载」。换源不受此限"
|
||||
&with_note(
|
||||
format!(
|
||||
"同一个源最多试几次(含首次;出厂 {DEFAULT_MAX_ATTEMPTS_PER_SOURCE})。0 与 1 都等于「只试一次、不重试」——0 是旧版提供的档位,不代表「不下载」。换源不受此限"
|
||||
),
|
||||
retry_note.as_deref(),
|
||||
),
|
||||
&retry_options,
|
||||
selected(&retry_options, &retry),
|
||||
@@ -129,10 +145,15 @@ pub(super) fn group(env: &HostEnv) -> SettingGroupView {
|
||||
))
|
||||
// M193:gh-proxy 三态。**留空与 off 是两件事**,描述里必须都写出来——用户最容易把
|
||||
// 「我还没决定」当成「我不要加速」,于是以为自己关掉了、实际还在套前缀。
|
||||
//
|
||||
// P3-5:末尾那半句是**引擎生效档位**(`tuning.gh_proxy` 的翻译结论),不是文件原文。
|
||||
.with(SettingRowView::text(
|
||||
unlucky_core::download::config::KEY_GH_PROXY,
|
||||
"GitHub 加速前缀(gh-proxy)",
|
||||
"留空 = 跟随内置默认(镜像优先/国内判定时自动套 gh-proxy.com);填 off = 关闭(不生成任何 gh-proxy 候选);填一个 URL 前缀 = 覆盖它",
|
||||
&format!(
|
||||
"留空 = 跟随内置默认(镜像优先/国内判定时自动套 gh-proxy.com);填 off = 关闭(不生成任何 gh-proxy 候选);填一个 URL 前缀 = 覆盖它;当前生效:{}",
|
||||
gh_proxy_state(&tuning.gh_proxy)
|
||||
),
|
||||
&text(env, unlucky_core::download::config::KEY_GH_PROXY),
|
||||
))
|
||||
.with(SettingRowView::action(
|
||||
@@ -153,6 +174,45 @@ fn ladder(ladder: &[&str], current: &str) -> Vec<String> {
|
||||
options
|
||||
}
|
||||
|
||||
/// 文件里**显式写过**的原值(`None` = 文件没写这个键)。
|
||||
///
|
||||
/// 与 `super::int` 的区别:那个走 `effective()`(文件值 → schema 默认值),本函数只问文件本身。
|
||||
/// 「原值 X 已按出厂 N 生效」这句话的两个数必须现读(一个来自文件、一个来自 core 的翻译层),
|
||||
/// **不许**拿常量或请求参数冒充——那是 U1 明令禁止的假证据。
|
||||
fn raw_int(env: &HostEnv, key: &str) -> Option<i64> {
|
||||
env.store().doc().get(key).and_then(|value| value.as_i64())
|
||||
}
|
||||
|
||||
/// 文件原值 ≠ 生效值时的说明;`None` = 没什么可说的(没写过,或写的就是生效值)。
|
||||
fn applied_note(raw: Option<i64>, effective: i64, factory: i64) -> Option<String> {
|
||||
let raw = raw?;
|
||||
if raw == effective {
|
||||
return None;
|
||||
}
|
||||
Some(if effective == factory {
|
||||
format!("原值 {raw} 已按出厂 {factory} 生效")
|
||||
} else {
|
||||
format!("原值 {raw} 已按 {effective} 生效")
|
||||
})
|
||||
}
|
||||
|
||||
/// 说明 +(有差异时)那一句「原值 … 生效」。
|
||||
fn with_note(base: String, note: Option<&str>) -> String {
|
||||
match note {
|
||||
Some(note) => format!("{base};{note}"),
|
||||
None => base,
|
||||
}
|
||||
}
|
||||
|
||||
/// gh-proxy 三态的上屏短句(与 [`GhProxyOverride`] 一一对应)。
|
||||
fn gh_proxy_state(state: &GhProxyOverride) -> String {
|
||||
match state {
|
||||
GhProxyOverride::Builtin => "跟随内置默认".to_string(),
|
||||
GhProxyOverride::Off => "已关闭(off)".to_string(),
|
||||
GhProxyOverride::Prefix(prefix) => format!("覆盖为 {prefix}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 `launcher.json` 读出下载调优(默认值与夹取口径在 core,本函数只做「取原始值」)。
|
||||
#[must_use]
|
||||
pub(crate) fn tuning_of(env: &HostEnv) -> DownloadTuning {
|
||||
@@ -176,3 +236,7 @@ pub(crate) fn tuning_of(env: &HostEnv) -> DownloadTuning {
|
||||
pub(crate) fn apply_tuning(env: &HostEnv) -> bool {
|
||||
shared::set_tuning(env.layout.cache(), tuning_of(env))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "download_tests.rs"]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user