fix(app-iced): 复审 r1 三条(在飞守卫补 Back/Target、预检按版本过期、shell 文档如实)
P2-1(行为):`Busy::Importing` 的早退只盖了 Close。「返回」会派 Scan、「换目标档」会派 零写盘 Preview,两条都把 `adopt_epoch` +1,于是在飞的 Confirm 交卷时 Done 被代次丢掉—— 实例写进去了,界面什么都没说(正是 Close 守卫要防的形状)。两处补同款早退,且守卫写在 改本地状态**之前**:挡下来的换档不该把界面改成 B 档而盘上写的是 A 档。 P2-2(显示陈旧):`Form::fresh_plan` 原来只按目标档过期。先选 1.20.1 + 版本目录档,再改选 1.19.4,目标档没变,旧预检的 notes(说的是 `versions/1.20.1`)会继续当新版本的后果原文画。 现在连 `plan.version_id == selected` 一起判;没选中版本时也返回 None。同处那句 「版本一致性由 Self::candidate 那一路保证(选中即重扫)」与代码不符(Select 只改本地字段、 不发命令),改成如实说明过期判定只能在这里做。 P2-3(文档):`app/adopt/shell.rs` 的「纯搬移:函数体一行未改」不成立——同一提交里卡壳几何、 页脚去重、预览摘要都改过。改成逐条列出改了什么与为什么。 用例(两条都是先写红再修,红时 panic 信息就是错在哪): - back_and_target_are_ignored_while_the_registration_is_in_flight(记账后端断言「不许派 Scan / 不许派 Preview」);红:`登记在飞时 Back 不许派 Scan(会把 Done 的代次顶掉)`; - fresh_plan_expires_when_the_selected_version_changes;红:`换了版本之后旧预检必须过期 (否则画的是 1.20.1 的后果原文)`。 顺带补图:`ULL_ADOPT_STATE` 加 `pick-selected` 一档(选中行底色 + 预检启用态), 出图 `DevDocs/artifacts/m223/adopt-pick-selected-light.png`(r1 复审说这格只有代码没截图)。
This commit is contained in:
1 parent
6ffbd51f60
commit
fa3fab9086
5 files changed
+183
-8
No files matched your search
@@ -144,16 +144,23 @@ impl Form {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 后台算出来、且**对得上当前选择**的预检(对不上 = 上一档的过期结果,一律不画)。
|
/// 后台算出来、且**对得上当前选择**的预检(对不上 = 过期的结果,一律不画)。
|
||||||
///
|
///
|
||||||
/// 判据是「目标档一致」:用户改了目标档之后,旧 plan 的 `notes` 说的是别的目录,
|
/// 两个判据缺一不可:
|
||||||
/// 拿它当后果原文就是撒谎。版本一致性由 [`Self::candidate`] 那一路保证(选中即重扫)。
|
/// - **目标档一致**:改了档之后旧 plan 的 `notes` 说的是别的目录;
|
||||||
|
/// - **版本 id 一致**:换了版本之后旧 plan 的 `notes` 说的是别的版本目录
|
||||||
|
/// (版本目录档的后果原文里带 `versions/<id>`,拿它当新版本的后果就是撒谎)。
|
||||||
|
///
|
||||||
|
/// 没有选中版本时也返回 `None`:那一刻没有任何版本可言,画谁的预检都不成立。
|
||||||
|
/// 这两条都不是「选中即重扫」带来的——`Select` 只改本地字段、不发命令(落点见
|
||||||
|
/// `app/update/adopt.rs`),所以过期判定只能在这里做。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn fresh_plan<'a>(&self, adopt_state: &'a AdoptState) -> Option<&'a AdoptPlanView> {
|
pub fn fresh_plan<'a>(&self, adopt_state: &'a AdoptState) -> Option<&'a AdoptPlanView> {
|
||||||
|
let selected = self.selected.as_deref()?;
|
||||||
adopt_state
|
adopt_state
|
||||||
.plan
|
.plan
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|plan| plan.target == self.target)
|
.filter(|plan| plan.target == self.target && plan.version_id == selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前选中的候选(选中项已不在列表里时返回 `None`,界面自动退回「请选择」)。
|
/// 当前选中的候选(选中项已不在列表里时返回 `None`,界面自动退回「请选择」)。
|
||||||
|
|||||||
@@ -6,8 +6,19 @@
|
|||||||
//! | [`title_text`] / [`subtitle`] | 四段的标题与一句说明(文案全部来自 i18n 目录) |
|
//! | [`title_text`] / [`subtitle`] | 四段的标题与一句说明(文案全部来自 i18n 目录) |
|
||||||
//! | [`footer`] | 按段给按钮组合;在等回程时主按钮禁用 |
|
//! | [`footer`] | 按段给按钮组合;在等回程时主按钮禁用 |
|
||||||
//!
|
//!
|
||||||
//! 纯搬移:函数体一行未改,只是换了文件(`app/adopt.rs` 落笔 412 行越过 `components::gate`
|
//! ## 这个文件不是「纯搬移」
|
||||||
//! 的 400 行硬线)。调用方仍是 `app/adopt.rs` 的 `layer` 与 `middle`。
|
//!
|
||||||
|
//! 它**起于**一次搬移:`app/adopt.rs` 落笔 412 行越过 `components::gate` 的 400 行硬线,
|
||||||
|
//! 于是把卡壳 / 标题 / 页脚搬到这里。但**同一个提交里**这三块还改过行为与几何,逐条如下
|
||||||
|
//! (复审对 diff 时按这几条看,别按「一行未改」看):
|
||||||
|
//!
|
||||||
|
//! | 改了什么 | 为什么 |
|
||||||
|
//! |---|---|
|
||||||
|
//! | `card` 拆成「定高 + 中间区可滚」(Pick / Failed)与「按内容收高」(Preview / Done)两种几何 | 原来只给 `max_height`,内容一高就被裁:页脚按钮与实例名整排不可见(出图时抓到) |
|
||||||
|
//! | `footer` 的「关闭」只在 Pick / Preview 出现 | Failed / Done 的主按钮就是关闭,再给一枚文字关闭就是两枚一模一样的按钮 |
|
||||||
|
//! | `subtitle` 的 Preview 文案改成「版本名 · 目标档」 | 原来打「隔离档 · 目标档」,同屏两个「共用目录」 |
|
||||||
|
//!
|
||||||
|
//! 调用方仍是 `app/adopt.rs` 的 `layer` 与 `middle`。
|
||||||
|
|
||||||
use iced::widget::{column, container, row, scrollable, Space};
|
use iced::widget::{column, container, row, scrollable, Space};
|
||||||
use iced::{Background, Element, Length};
|
use iced::{Background, Element, Length};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
//! ## 怎么用
|
//! ## 怎么用
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! ULL_ADOPT_STATE=pick|empty|preview|shared|failed|done \
|
//! ULL_ADOPT_STATE=pick|pick-selected|empty|preview|shared|failed|done \
|
||||||
//! unlucky-launcher-iced.exe --software-renderer --screenshot <path> [--screenshot-theme dark]
|
//! unlucky-launcher-iced.exe --software-renderer --screenshot <path> [--screenshot-theme dark]
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
@@ -65,6 +65,12 @@ pub(crate) fn seed(name: &str) -> Option<(Form, AdoptState)> {
|
|||||||
state.apply(AdoptEvent::Scan(Box::new(scan_view())));
|
state.apply(AdoptEvent::Scan(Box::new(scan_view())));
|
||||||
form.busy = Busy::Idle;
|
form.busy = Busy::Idle;
|
||||||
}
|
}
|
||||||
|
// 扫描完成、**已选中一个可导入的版本**(选中行底色 + 预检按钮启用态)
|
||||||
|
"pick-selected" => {
|
||||||
|
state.apply(AdoptEvent::Scan(Box::new(scan_view())));
|
||||||
|
form.selected = Some("1.20.1".to_string());
|
||||||
|
form.busy = Busy::Idle;
|
||||||
|
}
|
||||||
// 游戏根下真没有版本(空态)
|
// 游戏根下真没有版本(空态)
|
||||||
"empty" => {
|
"empty" => {
|
||||||
state.apply(AdoptEvent::Scan(Box::new(AdoptScanView {
|
state.apply(AdoptEvent::Scan(Box::new(AdoptScanView {
|
||||||
|
|||||||
@@ -243,3 +243,45 @@ fn a_foreign_target_shows_its_notes_without_jumping_to_confirm() {
|
|||||||
form.target = AdoptTargetView::VersionDir;
|
form.target = AdoptTargetView::VersionDir;
|
||||||
assert!(form.fresh_plan(&state).is_none(), "过期的预检不许画");
|
assert!(form.fresh_plan(&state).is_none(), "过期的预检不许画");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **P2-2**:`fresh_plan` 必须按**版本 id** 过期,不只是目标档。
|
||||||
|
///
|
||||||
|
/// 场景:先选 1.20.1 + 版本目录档(notes 说 `versions/1.20.1`),再改选 1.19.4——目标档没变,
|
||||||
|
/// 旧的预检结果若不按版本过滤就会继续当「这个版本的后果原文」画出来,那是一句假话。
|
||||||
|
///
|
||||||
|
/// 抓得住:`fresh_plan` 只看 `plan.target == self.target`(第一次修法就是这样漏的)。
|
||||||
|
/// 抓不住:core 真的会不会为 1.19.4 算出同一段 notes(那是 `target_notes()` 的活)。
|
||||||
|
#[test]
|
||||||
|
fn fresh_plan_expires_when_the_selected_version_changes() {
|
||||||
|
let mut state = state_with_scan();
|
||||||
|
state.apply(AdoptEvent::Planned(Box::new(AdoptPlanView {
|
||||||
|
version_id: "1.20.1".to_string(),
|
||||||
|
name: "1.20.1".to_string(),
|
||||||
|
target: AdoptTargetView::VersionDir,
|
||||||
|
isolation: AdoptIsolationView::Isolated,
|
||||||
|
run_dir: std::path::PathBuf::from("/game/versions/1.20.1"),
|
||||||
|
notes: vec!["运行目录 = 版本目录 /game/versions/1.20.1".to_string()],
|
||||||
|
})));
|
||||||
|
|
||||||
|
let mut form = Form {
|
||||||
|
open: true,
|
||||||
|
selected: Some("1.20.1".to_string()),
|
||||||
|
target: AdoptTargetView::VersionDir,
|
||||||
|
checked: true,
|
||||||
|
..Form::default()
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
form.fresh_plan(&state).is_some(),
|
||||||
|
"对得上版本与目标档时应当可读"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 改选另一个版本:目标档没变,但旧预检说的是别的版本的目录
|
||||||
|
form.selected = Some("1.19.4".to_string());
|
||||||
|
assert!(
|
||||||
|
form.fresh_plan(&state).is_none(),
|
||||||
|
"换了版本之后旧预检必须过期(否则画的是 1.20.1 的后果原文)"
|
||||||
|
);
|
||||||
|
// 没选版本时同样不该画(`None` = 没有任何版本的预检可言)
|
||||||
|
form.selected = None;
|
||||||
|
assert!(form.fresh_plan(&state).is_none());
|
||||||
|
}
|
||||||
@@ -48,6 +48,12 @@ impl App {
|
|||||||
}
|
}
|
||||||
AdoptMessage::Name(name) => self.adopt_form.name = name,
|
AdoptMessage::Name(name) => self.adopt_form.name = name,
|
||||||
AdoptMessage::Target(target) => {
|
AdoptMessage::Target(target) => {
|
||||||
|
// 登记在飞时不吃「换档」:它会派一条零写盘的预览把 `adopt_epoch` +1,
|
||||||
|
// 于是在飞的 Confirm 交卷时 Done 被代次丢掉——实例写进去了,界面什么都没说。
|
||||||
|
// 连本地档位也不改:界面上显示 A 档、盘上写的是 B 档,是最糟的一种不一致。
|
||||||
|
if self.adopt_form.busy == Busy::Importing {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
self.adopt_form.target = target;
|
self.adopt_form.target = target;
|
||||||
// 换了目标档 ⇒ 上一档的预检作废(确认按钮不该继续亮着)
|
// 换了目标档 ⇒ 上一档的预检作废(确认按钮不该继续亮着)
|
||||||
self.adopt_form.checked = false;
|
self.adopt_form.checked = false;
|
||||||
@@ -74,6 +80,11 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
AdoptMessage::Back => {
|
AdoptMessage::Back => {
|
||||||
|
// 同 Close / Target:登记在飞时「返回」会把 `adopt_epoch` +1(它派 Scan),
|
||||||
|
// 在飞 Confirm 的 Done 就再也到不了界面。
|
||||||
|
if self.adopt_form.busy == Busy::Importing {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
// 退回候选列表 = 重新扫一次:`scan` 会把上一轮的预检/失败一起作废,
|
// 退回候选列表 = 重新扫一次:`scan` 会把上一轮的预检/失败一起作废,
|
||||||
// 于是界面回到 Pick 段,不需要再给 core 加一个「清失败」的命令。
|
// 于是界面回到 Pick 段,不需要再给 core 加一个「清失败」的命令。
|
||||||
self.adopt_form.checked = false;
|
self.adopt_form.checked = false;
|
||||||
@@ -124,8 +135,13 @@ impl App {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use unlucky_design::color::SchemeFlavor;
|
use unlucky_design::color::SchemeFlavor;
|
||||||
use unlucky_presenter::dto::{AdoptEvent, AdoptTargetView};
|
use unlucky_presenter::dto::{
|
||||||
|
AdoptCommand, AdoptEvent, AdoptTargetView, AppCommand, AppEvent, Backend,
|
||||||
|
};
|
||||||
use unlucky_presenter::mock::MockBackend;
|
use unlucky_presenter::mock::MockBackend;
|
||||||
use unlucky_presenter::settings_bridge::SettingsBridge;
|
use unlucky_presenter::settings_bridge::SettingsBridge;
|
||||||
|
|
||||||
@@ -150,6 +166,52 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 记账后端:把 `dispatch` 收到的命令留在台账里(`MockBackend` 只认自己那几条,
|
||||||
|
/// 「有没有派出去」根本看不见——而 P2-1 的判据正是「不许派」)。
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct RecordingBackend {
|
||||||
|
dispatched: Rc<RefCell<Vec<AppCommand>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend for RecordingBackend {
|
||||||
|
fn drain_events(&mut self) -> Vec<AppEvent> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch(&mut self, command: AppCommand) {
|
||||||
|
self.dispatched.borrow_mut().push(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 带台账的 App(返回台账句柄,测试直接查「派了什么」)。
|
||||||
|
fn recording() -> (App, Rc<RefCell<Vec<AppCommand>>>) {
|
||||||
|
let dispatched: Rc<RefCell<Vec<AppCommand>>> = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let app = App::with_presenter(
|
||||||
|
Options::default(),
|
||||||
|
FAMILIES,
|
||||||
|
SchemeFlavor::Light,
|
||||||
|
Box::new(RecordingBackend {
|
||||||
|
dispatched: Rc::clone(&dispatched),
|
||||||
|
}),
|
||||||
|
SettingsBridge::memory_only(),
|
||||||
|
);
|
||||||
|
(app, dispatched)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 台账里有没有派过某条 adopt 命令(只认命令种类,不看载荷内容)。
|
||||||
|
fn dispatched_adopt(ledger: &Rc<RefCell<Vec<AppCommand>>>, want: &str) -> bool {
|
||||||
|
ledger.borrow().iter().any(|command| match command {
|
||||||
|
AppCommand::Adopt(command) => match (&**command, want) {
|
||||||
|
(AdoptCommand::Scan, "scan") => true,
|
||||||
|
(AdoptCommand::Preview(_), "preview") => true,
|
||||||
|
(AdoptCommand::Confirm(_), "confirm") => true,
|
||||||
|
(AdoptCommand::Dismiss, "dismiss") => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 打开向导 = 表单进入「在等扫描」+ 派一条 `Scan`。
|
/// 打开向导 = 表单进入「在等扫描」+ 派一条 `Scan`。
|
||||||
///
|
///
|
||||||
/// 抓得住:`Open` 没派命令(面板永远停在「正在扫描」)、或忘了置 `Busy::Scanning`(没有等待态)。
|
/// 抓得住:`Open` 没派命令(面板永远停在「正在扫描」)、或忘了置 `Busy::Scanning`(没有等待态)。
|
||||||
@@ -241,4 +303,51 @@ mod tests {
|
|||||||
let _ = app.on_adopt(AdoptMessage::Close);
|
let _ = app.on_adopt(AdoptMessage::Close);
|
||||||
assert!(app.adopt_form.open, "登记在飞时不许把面板关掉");
|
assert!(app.adopt_form.open, "登记在飞时不许把面板关掉");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// **P2-1**:登记在飞时,除了 Close,「返回」与「换目标档」也必须早退。
|
||||||
|
///
|
||||||
|
/// 三条都是同一条病:它们都会派一条新命令把 `adopt_epoch` +1,于是在飞 Confirm 的
|
||||||
|
/// `Done` 回来时被代次丢掉 —— 实例**写进去了**,界面却什么都没说(Close 那条早就守住了,
|
||||||
|
/// 唯独这两支漏了)。
|
||||||
|
///
|
||||||
|
/// 抓得住:`Back` 派了 `Scan`(旧 `Done` 被丢)、`Target` 派了 `Preview`(同上),
|
||||||
|
/// 以及「守卫写了但写在改本地状态之后」(表单被改成新档位,而盘上写的是旧档位)。
|
||||||
|
/// 抓不住:host 侧代次比较的真实时序(那在 presenter 的 `host/adopt` 那层)。
|
||||||
|
#[test]
|
||||||
|
fn back_and_target_are_ignored_while_the_registration_is_in_flight() {
|
||||||
|
let (mut app, ledger) = recording();
|
||||||
|
let _ = app.on_adopt(AdoptMessage::Open);
|
||||||
|
let _ = app.on_adopt(AdoptMessage::Select("1.20.1".to_string()));
|
||||||
|
let _ = app.on_adopt(AdoptMessage::Target(AdoptTargetView::InstanceDir));
|
||||||
|
// 走到「确认」那一步:真的派一条 Confirm 出去
|
||||||
|
let _ = app.on_adopt(AdoptMessage::Check);
|
||||||
|
let _ = app.on_adopt(AdoptMessage::Confirm);
|
||||||
|
assert!(
|
||||||
|
dispatched_adopt(&ledger, "confirm"),
|
||||||
|
"前提:确认要真的派出去"
|
||||||
|
);
|
||||||
|
assert_eq!(app.adopt_form.busy, Busy::Importing);
|
||||||
|
ledger.borrow_mut().clear();
|
||||||
|
|
||||||
|
// 在飞期间点「返回」:不许派 Scan
|
||||||
|
let _ = app.on_adopt(AdoptMessage::Back);
|
||||||
|
assert!(
|
||||||
|
!dispatched_adopt(&ledger, "scan"),
|
||||||
|
"登记在飞时 Back 不许派 Scan(会把 Done 的代次顶掉)"
|
||||||
|
);
|
||||||
|
assert_eq!(app.adopt_form.busy, Busy::Importing, "等待态不许被改掉");
|
||||||
|
|
||||||
|
// 在飞期间换目标档:不许派 Preview,也不许改本地档位
|
||||||
|
let _ = app.on_adopt(AdoptMessage::Target(AdoptTargetView::Shared));
|
||||||
|
assert!(
|
||||||
|
!dispatched_adopt(&ledger, "preview"),
|
||||||
|
"登记在飞时换档不许派 Preview(会把 Done 的代次顶掉)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.adopt_form.target,
|
||||||
|
AdoptTargetView::InstanceDir,
|
||||||
|
"挡下来的点击不该改出界面与盘上不一致的档位"
|
||||||
|
);
|
||||||
|
assert_eq!(app.adopt_form.busy, Busy::Importing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user