fix(app-iced): 点选共享档立刻显示后果原文,登记在飞时不吃关闭(M223 自检)

两处自己复审抓到的问题:

1. 契约 §5.2 要求 Shared/VersionDir 被**点选**就展示 target_notes() 原文,而上一版要等
   用户再点一次「预检」才出现。现在点选这两档会顺手发一次**零写盘**的预检,结果落在
   Pick 段当提示读(不把用户甩到确认页:确认仍由显式的「预检」按钮触发,Form::checked)。
   改档之后旧 plan 因目标档不一致而不再显示(Form::fresh_plan),避免画上一档的后果原文。
2. 「关闭」在登记在飞时被忽略:core 那条线程已经开跑,关面板会让 Done 事件被代次丢掉,
   用户看到的就是「实例写进去了,界面什么都没说」。

用例:a_foreign_target_shows_its_notes_without_jumping_to_confirm /
choosing_a_foreign_target_previews_but_waits_for_the_user /
closing_is_ignored_while_the_registration_is_in_flight。
This commit is contained in:
AskaEth committed 2026-11-10 19:37:43 +08:00
1 parent 9ca5ee8ae7
commit a36e17a5d3
4 files changed
+163 -19

No files matched your search

+54 -13
View File
@@ -48,7 +48,9 @@ use unlucky_design::shape::ShapeScale;
use unlucky_design::spacing::Space as Sp;
use unlucky_design::typography::TypeRole;
use unlucky_i18n::{Catalog, Key, Lang};
use unlucky_presenter::dto::{AdoptCandidateView, AdoptStage, AdoptState, AdoptTargetView};
use unlucky_presenter::dto::{
AdoptCandidateView, AdoptPlanView, AdoptStage, AdoptState, AdoptTargetView,
};
use crate::app::components::buttons::{self, Button};
use crate::app::components::feedback::dialog::scrim;
@@ -102,6 +104,13 @@ pub struct Form {
pub target: AdoptTargetView,
/// 在等在飞的那一条命令
pub busy: Busy,
/// 用户已经点过「预检」(没点过就留在 Pick 段,哪怕后台已经顺手算过一次落点)。
///
/// 为什么要有这一位:共享档 / 版本目录档一旦被点选,界面就要立刻显示 `target_notes()` 的
/// **后果原文**(契约 §5.2)——那是 core 唯一会产出这段文字的地方,所以点选时顺手发一次
/// **零写盘**的预检。但那一次结果不该把用户甩到「可以导入」的确认页:确认必须由显式的
/// `Check` 触发。于是 `checked` 记住「这次预检是不是用户要的」。
pub checked: bool,
}
impl Form {
@@ -124,6 +133,30 @@ impl Form {
self.busy = Busy::Idle;
}
/// 当前该画哪一段:`Preview` 只在用户**点过预检**之后才算数,其余按 `AdoptState::stage()`。
///
/// 这条收窄是为了「点选共享档 ⇒ 立刻看到后果原文」而不把用户直接甩到确认页:
/// 自动预检的结果落在 Pick 段里当**提示**读,用户点「预检」之后才进确认段。
#[must_use]
pub fn stage(&self, adopt_state: &AdoptState) -> AdoptStage {
match adopt_state.stage() {
AdoptStage::Preview if !self.checked => AdoptStage::Pick,
other => other,
}
}
/// 后台算出来、且**对得上当前选择**的预检(对不上 = 上一档的过期结果,一律不画)。
///
/// 判据是「目标档一致」:用户改了目标档之后,旧 plan 的 `notes` 说的是别的目录,
/// 拿它当后果原文就是撒谎。版本一致性由 [`Self::candidate`] 那一路保证(选中即重扫)。
#[must_use]
pub fn fresh_plan<'a>(&self, adopt_state: &'a AdoptState) -> Option<&'a AdoptPlanView> {
adopt_state
.plan
.as_ref()
.filter(|plan| plan.target == self.target)
}
/// 当前选中的候选(选中项已不在列表里时返回 `None`,界面自动退回「请选择」)。
#[must_use]
pub fn candidate<'a>(&self, adopt_state: &'a AdoptState) -> Option<&'a AdoptCandidateView> {
@@ -206,24 +239,25 @@ fn card<'a>(
adopt_state: &'a AdoptState,
catalog: Catalog,
) -> Element<'a, Message> {
let stage = form.stage(adopt_state);
let body = column![
frame::text_role(
ui,
TypeRole::HeadlineSmall,
true,
title_text(adopt_state, catalog),
title_text(stage, catalog),
ui.color(Role::OnSurface),
)
.map(Message::Page),
wrapped(
ui,
subtitle(adopt_state, catalog),
subtitle(stage, adopt_state, catalog),
ui.color(Role::OnSurfaceVariant)
),
frame::divider(ui).map(Message::Page),
middle(ui, form, adopt_state, catalog),
middle(ui, form, adopt_state, stage, catalog),
frame::divider(ui).map(Message::Page),
footer(ui, form, adopt_state, catalog),
footer(ui, form, adopt_state, stage, catalog),
]
.spacing(ui.space(Sp::X3))
.width(Length::Fill);
@@ -241,8 +275,8 @@ fn card<'a>(
}
/// 段标题(四段各一句)。
fn title_text(adopt_state: &AdoptState, catalog: Catalog) -> &'static str {
match adopt_state.stage() {
fn title_text(stage: AdoptStage, catalog: Catalog) -> &'static str {
match stage {
AdoptStage::Pick => catalog.text(Key::AdoptTitle),
AdoptStage::Preview => catalog.text(Key::AdoptPlanTitle),
AdoptStage::Failed => catalog.text(Key::AdoptFailureTitle),
@@ -251,8 +285,8 @@ fn title_text(adopt_state: &AdoptState, catalog: Catalog) -> &'static str {
}
/// 标题下那句说明(Pick 段说清「什么都不复制」;Done 段说清「去实例列表启动」)。
fn subtitle(adopt_state: &AdoptState, catalog: Catalog) -> String {
match adopt_state.stage() {
fn subtitle(stage: AdoptStage, adopt_state: &AdoptState, catalog: Catalog) -> String {
match stage {
AdoptStage::Pick => catalog.text(Key::AdoptSubtitle).to_string(),
AdoptStage::Preview => adopt_state
.plan
@@ -279,11 +313,18 @@ fn middle<'a>(
ui: &'a Ui,
form: &'a Form,
adopt_state: &'a AdoptState,
stage: AdoptStage,
catalog: Catalog,
) -> Element<'a, Message> {
match adopt_state.stage() {
AdoptStage::Pick => pick::pick(ui, form, adopt_state.scan.as_ref(), catalog),
AdoptStage::Preview => panes::preview(ui, adopt_state.plan.as_ref(), catalog),
match stage {
AdoptStage::Pick => pick::pick(
ui,
form,
adopt_state.scan.as_ref(),
form.fresh_plan(adopt_state),
catalog,
),
AdoptStage::Preview => panes::preview(ui, form.fresh_plan(adopt_state), catalog),
AdoptStage::Failed => panes::failed(ui, adopt_state.failure.as_ref(), catalog),
AdoptStage::Done => panes::done(ui, adopt_state.done.as_ref(), catalog),
}
@@ -294,9 +335,9 @@ fn footer<'a>(
ui: &'a Ui,
form: &'a Form,
adopt_state: &'a AdoptState,
stage: AdoptStage,
catalog: Catalog,
) -> Element<'a, Message> {
let stage = adopt_state.stage();
let busy = form.busy.waiting();
let primary: Element<'a, Message> = match stage {