fix(m161): 收更新键/按钮/矮窗口/启动错误/导入结果五笔债
① update.auto_check / update.channel 登记进 core setting schema(1.6.0)
- 新增 setting/keys/update.rs(两键 + UPDATE_CHANNELS,只允许 stable);
SettingSchema 加 with_all 供域键表批量登记。
- 新增 setting/update_prefs.rs:cache/update/prefs.json → launcher.json 的一次性迁移。
四条硬约束:源指纹闸门(幂等)、不丢 false、新值胜出、旧文件改名 .migrated 后不再被读;
主配置不可写时不动旧文件。收据 config/update-prefs-migration.json。
- presenter Runtime 建会话时迁一次并把结局留在 migration();prefs 字段换成
auto_check + 每次派发从主配置读 channel(不再硬编码 Channel::Stable)。
保留旧字段 auto_check 的真实来源(原有测试 cases 会断言)。
- CLI config get/set/dump 先跑同一次迁移(幂等),输出里给 migration 段与收据路径。
② presenter/dto/events.rs 的「本次会话不再弹提示条」注释改成真实语义
(记的是版本号:同版本压住、更高版本照弹、手动检查清掉)。
③ 检查/下载进行中按钮禁用:pages/update/busy.rs 的 busy/can_check/can_apply;
设置页两颗按钮改画禁用(不是消失),app 层 on_update_action 再挡一道
(忙碌时 CheckNow/Apply 不派发);新增两条用例(含「忙完恢回可点」)。
④ 内容库低高度:content_layout::MIN_BODY_HEIGHT=460 + BodyMode(按固定件+三整行算出来,
用例守着算式);frame::body_height 从 Ui::metrics 算正文可用高,pages::Model 带 window_height。
≥460 列表自己滚;<460 主列整列一个滚动条、列表按自然高度铺开。
⑤ 启动错误文案本地化:presenter 删掉 launch_summary / 「启动失败:」前缀 / 五条中文阶段前缀,
改发 LaunchFailureView{stage,detail};LaunchFinished 改带 exit_code/uptime_ms/failure,
Toast 由 app 层按 i18n key 现求(与 install_cancelled 同构)。InstancesFailed 拆成
InstancesFailedStage(列表单行)+ LaunchStageFailed(详情/Toast 全文)。
⑥ 导入结果块:ImportReportView 加 failure_stage(NotStarted/RolledBack/RollbackIncomplete,
由 core 的 rollback 机器位推出);标题、图标、底色三处分档,预检失败不再说「已回滚」。
顺带:六处注释/文档漂移(update.rs 的分工表与偏好落点、pack 的「已整包回滚」、
build_request 的 Errors、settings/update 的按钮禁用说明等),
以及为守 400 行把 update/cli-config/core-keys/presenter-dto/presenter-launch/i18n-golden
里贴线或越线的文件按模块拆开(纯搬移,行为不变)。
This commit is contained in:
1 parent
b2d9f3927f
commit
03741ecd2d
69 files changed
+2653
-707
No files matched your search
@@ -14,8 +14,8 @@
|
||||
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
use unlucky_presenter::dto::{
|
||||
ConflictChoiceView, ConflictFileView, ConflictReasonView, ImportErrorView, ImportReportView,
|
||||
ImportStatusView, MissingFileView, MissingReasonView,
|
||||
ConflictChoiceView, ConflictFileView, ConflictReasonView, ImportErrorView,
|
||||
ImportFailureStageView, ImportReportView, ImportStatusView, MissingFileView, MissingReasonView,
|
||||
};
|
||||
|
||||
use super::PackPageState;
|
||||
@@ -48,10 +48,40 @@ pub struct ConflictRow {
|
||||
pub expanded: bool,
|
||||
}
|
||||
|
||||
/// 致命失败块(**已整包回滚**)。
|
||||
/// 致命失败块(**两种措辞,不共用**)。
|
||||
///
|
||||
/// 为什么不是一句「失败」:`NotStarted`(预检就没过,一个字节都没写)与 `RolledBack`
|
||||
/// (写进去了,又整包回滚干净)对用户是不同的消息——前者「去修包再试」,后者「刚才动过你的目录,
|
||||
/// 已经复原」。M161 之前两者共用一句「已回滚 —— 目标目录零残留」,预检失败时那句话是假的。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FatalKind {
|
||||
/// 预检/装配阶段失败:目标目录**一个字节都没写**
|
||||
NotStarted,
|
||||
/// 写过目标目录,回滚**完整**完成
|
||||
RolledBack,
|
||||
/// 写过,但回滚有失败项(目标目录可能与导入前不同)
|
||||
RollbackIncomplete,
|
||||
}
|
||||
|
||||
impl FatalKind {
|
||||
/// 图标(与 [`Fatal::title`] 各说一遍:颜色/图标关掉时标题仍然分得清)。
|
||||
#[must_use]
|
||||
pub const fn icon(self) -> unlucky_design::icons::IconKind {
|
||||
match self {
|
||||
// 预检没过 = 「还没开始」,用警示而不是「已出错」:目录是干净的
|
||||
Self::NotStarted => unlucky_design::icons::IconKind::Warn,
|
||||
Self::RolledBack => unlucky_design::icons::IconKind::Restore,
|
||||
Self::RollbackIncomplete => unlucky_design::icons::IconKind::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 致命失败块。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Fatal {
|
||||
/// 「已回滚 —— 目标目录零残留」
|
||||
/// 三种阶段之一(决定标题/图标/底色)
|
||||
pub kind: FatalKind,
|
||||
/// 标题(预检未通过 / 已回滚 / 回滚不完整)
|
||||
pub title: String,
|
||||
/// 大类名(`路径穿越` / `清单损坏` …)
|
||||
pub label: String,
|
||||
@@ -124,7 +154,7 @@ pub(super) fn report_of(
|
||||
|
||||
Report {
|
||||
status: report.status,
|
||||
title: status_title(report.status, catalog).to_owned(),
|
||||
title: status_title(report, catalog).to_owned(),
|
||||
subtitle: subtitle(report, catalog),
|
||||
written_line: catalog.render(
|
||||
Key::PackReportWritten,
|
||||
@@ -133,7 +163,10 @@ pub(super) fn report_of(
|
||||
("bytes", &human(report)),
|
||||
],
|
||||
),
|
||||
fatal: report.error.as_ref().map(|error| fatal_of(error, catalog)),
|
||||
fatal: report
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|error| fatal_of(error, report.failure_stage, catalog)),
|
||||
missing_title,
|
||||
missing: report
|
||||
.missing
|
||||
@@ -177,11 +210,22 @@ fn human(report: &ImportReportView) -> String {
|
||||
unlucky_presenter::dto::human_bytes(report.bytes_written)
|
||||
}
|
||||
|
||||
fn status_title(status: ImportStatusView, catalog: Catalog) -> &'static str {
|
||||
match status {
|
||||
/// 标题:三态 + **失败阶段**(预检未通过 ≠ 已回滚,M161)。
|
||||
fn status_title(report: &ImportReportView, catalog: Catalog) -> &'static str {
|
||||
match report.status {
|
||||
ImportStatusView::Success => catalog.text(Key::PackStatusSuccess),
|
||||
ImportStatusView::Partial => catalog.text(Key::PackStatusPartial),
|
||||
ImportStatusView::Failed => catalog.text(Key::PackStatusFailed),
|
||||
ImportStatusView::Failed => match report.failure_stage {
|
||||
Some(ImportFailureStageView::NotStarted) => {
|
||||
catalog.text(Key::PackStatusFailedNotStarted)
|
||||
}
|
||||
Some(ImportFailureStageView::RollbackIncomplete) => {
|
||||
catalog.text(Key::PackStatusFailedIncomplete)
|
||||
}
|
||||
// `None` 不该出现(失败必有阶段);退回「已回滚」这一句是旧行为,
|
||||
// 只有拿不到阶段时才会走到(例如未来的旧版对端)。
|
||||
_ => catalog.text(Key::PackStatusFailed),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,9 +247,24 @@ fn subtitle(report: &ImportReportView, catalog: Catalog) -> String {
|
||||
parts.join(" · ")
|
||||
}
|
||||
|
||||
fn fatal_of(error: &ImportErrorView, catalog: Catalog) -> Fatal {
|
||||
fn fatal_of(
|
||||
error: &ImportErrorView,
|
||||
stage: Option<ImportFailureStageView>,
|
||||
catalog: Catalog,
|
||||
) -> Fatal {
|
||||
let kind = match stage {
|
||||
Some(ImportFailureStageView::NotStarted) => FatalKind::NotStarted,
|
||||
Some(ImportFailureStageView::RollbackIncomplete) => FatalKind::RollbackIncomplete,
|
||||
_ => FatalKind::RolledBack,
|
||||
};
|
||||
let title = match kind {
|
||||
FatalKind::NotStarted => catalog.text(Key::PackFatalTitleNotStarted),
|
||||
FatalKind::RolledBack => catalog.text(Key::PackFatalTitle),
|
||||
FatalKind::RollbackIncomplete => catalog.text(Key::PackFatalTitleIncomplete),
|
||||
};
|
||||
Fatal {
|
||||
title: catalog.text(Key::PackFatalTitle).to_owned(),
|
||||
kind,
|
||||
title: title.to_owned(),
|
||||
label: error.label.clone(),
|
||||
message: error.message.clone(),
|
||||
code: error.code.clone(),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! 哪一态该出现哪一块。所有断言都能在没有图形环境的情况下跑。
|
||||
|
||||
mod pick;
|
||||
mod stage;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -226,6 +227,7 @@ fn report_keeps_tolerable_and_fatal_apart() {
|
||||
registered: None,
|
||||
bytes_written: 0,
|
||||
rolled_back: true,
|
||||
failure_stage: None,
|
||||
};
|
||||
let block = report_of(&report, &PackPageState::default(), catalog());
|
||||
assert!(block.fatal.is_some(), "致命块必须单独存在");
|
||||
@@ -262,6 +264,7 @@ fn conflicts_default_to_keeping_the_user_copy() {
|
||||
registered: None,
|
||||
bytes_written: 0,
|
||||
rolled_back: true,
|
||||
failure_stage: None,
|
||||
};
|
||||
// 「以包为准」只在导入到**已有实例**时给(复审 r1 P2-2),所以这里选一个实例
|
||||
let page = PackPageState {
|
||||
@@ -359,6 +362,7 @@ fn overwrite_is_offered_only_when_importing_into_an_existing_instance() {
|
||||
registered: None,
|
||||
bytes_written: 0,
|
||||
rolled_back: true,
|
||||
failure_stage: None,
|
||||
};
|
||||
|
||||
let new_instance = PackPageState::default();
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
//! M161:导入失败的**两种阶段**不许共用一句话。
|
||||
//!
|
||||
//! 从 `tests.rs` 拆出(那边贴着 400 行硬线):本文件只守「预检没过(一个字节都没写)」与
|
||||
//! 「已回滚」的措辞/图标分野。夹具在父模块(`use super::*`)。
|
||||
|
||||
use unlucky_presenter::dto::ImportFailureStageView;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// **「预检没过」与「已回滚」必须各说各的话**(M161 收的债)。
|
||||
///
|
||||
/// 此前两者共用 `rolled_back: true`,于是预检失败(一个字节都没写)也被写成
|
||||
/// 「已回滚 —— 目标目录零残留」——一个没发生的回滚。现在标题、图标、底色三处都分开,
|
||||
/// 且 `NotStarted` 的标题**不许**出现「回滚 / rolled back」这类词(那才是真正的谎)。
|
||||
#[test]
|
||||
fn the_two_failure_stages_never_share_one_sentence() {
|
||||
fn failed(stage: Option<ImportFailureStageView>) -> ImportReportView {
|
||||
ImportReportView {
|
||||
status: ImportStatusView::Failed,
|
||||
format: Some(PackFormatView::Modrinth),
|
||||
pack_name: Some("Broken".to_string()),
|
||||
pack_version: None,
|
||||
game_version: None,
|
||||
loader: None,
|
||||
version_id: None,
|
||||
target: PathBuf::from("C:/tmp"),
|
||||
files: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
missing: Vec::new(),
|
||||
rejected: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
error: Some(ImportErrorView {
|
||||
code: "pack_traversal".to_string(),
|
||||
label: "path traversal".to_string(),
|
||||
message: "../etc/passwd".to_string(),
|
||||
hint: None,
|
||||
}),
|
||||
registered: None,
|
||||
bytes_written: 0,
|
||||
rolled_back: true,
|
||||
failure_stage: stage,
|
||||
}
|
||||
}
|
||||
|
||||
let zh = Lang::ZhCn.catalog();
|
||||
let not_started = report_of(
|
||||
&failed(Some(ImportFailureStageView::NotStarted)),
|
||||
&PackPageState::default(),
|
||||
zh,
|
||||
);
|
||||
let rolled_back = report_of(
|
||||
&failed(Some(ImportFailureStageView::RolledBack)),
|
||||
&PackPageState::default(),
|
||||
zh,
|
||||
);
|
||||
let incomplete = report_of(
|
||||
&failed(Some(ImportFailureStageView::RollbackIncomplete)),
|
||||
&PackPageState::default(),
|
||||
zh,
|
||||
);
|
||||
|
||||
// 三种标题互不相同(三态压力:谁把它们折叠回同一句,这里就红)。
|
||||
let titles = [
|
||||
not_started.title.as_str(),
|
||||
rolled_back.title.as_str(),
|
||||
incomplete.title.as_str(),
|
||||
];
|
||||
assert_eq!(
|
||||
titles
|
||||
.iter()
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
.len(),
|
||||
3,
|
||||
"三种失败阶段的标题撞了:{titles:?}"
|
||||
);
|
||||
|
||||
// 「预检没过」那一句里不许出现「回滚」(那是没发生的事)。
|
||||
let fatal = not_started.fatal.expect("致命块");
|
||||
assert_eq!(
|
||||
fatal.kind,
|
||||
crate::pages::content::packui::model::FatalKind::NotStarted
|
||||
);
|
||||
assert!(
|
||||
!fatal.title.contains("回滚"),
|
||||
"预检失败不该说「回滚」:{}",
|
||||
fatal.title
|
||||
);
|
||||
assert!(!not_started.title.contains("回滚"), "{}", not_started.title);
|
||||
// 已回滚那一句必须说清「回滚」这件事。
|
||||
assert!(
|
||||
rolled_back.title.contains("回滚"),
|
||||
"已回滚那句要说明回滚:{}",
|
||||
rolled_back.title
|
||||
);
|
||||
// 图标分开:预检没过不是「错误」字形(目录没动过),回滚不完整才是。
|
||||
assert_ne!(
|
||||
crate::pages::content::packui::model::FatalKind::NotStarted.icon(),
|
||||
crate::pages::content::packui::model::FatalKind::RolledBack.icon(),
|
||||
"两种阶段的图标不许一样"
|
||||
);
|
||||
|
||||
// 成功/部分成功没有「失败阶段」可言。
|
||||
let mut ok = failed(None);
|
||||
ok.status = ImportStatusView::Success;
|
||||
ok.error = None;
|
||||
assert!(report_of(&ok, &PackPageState::default(), zh)
|
||||
.fatal
|
||||
.is_none());
|
||||
}
|
||||
Reference in new issue
Block a user