feat(app-iced): 实例页「导入已有版本」向导(M223)
页头「导入」按钮(以前是占位动作,点了只打日志)现在打开四段向导: | 段 | 画什么 | |---|---| | Pick | 候选版本(可导入的才可点)+ 目标三档 chip + 实例名(缺省 InstanceDir) | | Preview | 生效运行目录(回程给的)+ 隔离档 + target_notes() 原文逐条 | | Failed | 稳定码 + problems 逐条(不折叠成一句「导入失败」) | | Done | 实例名 + 运行目录 + 提示原文 | - 向导是叠在页面之上的一层(遮罩 + 居中卡),不搅乱 list-detail 的版式判据; - 表单态在 App(app/adopt.rs 的 Form),事件真相在 presenter 的 AdoptState; - 在等回程时主按钮禁用(连点「确认」不会写两条实例); - i18n 中英目录 + 金表补 27 条向导文案(实例域末尾追加)。
This commit is contained in:
1 parent
5a59cb33b2
commit
41caae3571
16 files changed
+1265
-2
No files matched your search
@@ -0,0 +1,131 @@
|
||||
//! Preview / Failed / Done 三段。
|
||||
//!
|
||||
//! | 段 | 画什么 | 判据 |
|
||||
//! |---|---|---|
|
||||
//! | Preview | 生效运行目录(**回程给的**)+ 隔离档 + `notes` 原文逐条 | `AdoptEvent::Planned` |
|
||||
//! | Failed | 稳定码 + `problems[]` 逐条 | `AdoptEvent::Failed` |
|
||||
//! | Done | 实例名 + 运行目录 + `notes` 原文逐条 | `AdoptEvent::Done` |
|
||||
//!
|
||||
//! 三段都不自己算路径、不自己写后果句:`run_dir` 与 `notes` 是 core 的返回值原样。
|
||||
//! 缺省档会带 `-2`/`-3` 后缀(两个不同展示名可能落到同一个物理目录),**只有 core 知道**
|
||||
//! 最后用了哪个——页面对它再做一次派生就会与真正启动时用的目录分叉。
|
||||
|
||||
use iced::widget::{column, scrollable};
|
||||
use iced::{Element, Length};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::spacing::Space as Sp;
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
use unlucky_presenter::dto::{AdoptDoneView, AdoptFailureView, AdoptPlanView};
|
||||
|
||||
use crate::pages::frame;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::message::Message;
|
||||
use super::text::{field_line, isolation_label, mono, problem_line, wrapped};
|
||||
use super::CARD_LIST_HEIGHT;
|
||||
|
||||
/// 预检结果:落点 + 隔离档 + 后果原文。
|
||||
pub(super) fn preview<'a>(
|
||||
ui: &'a Ui,
|
||||
plan: Option<&'a AdoptPlanView>,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, Message> {
|
||||
let Some(plan) = plan else {
|
||||
return wrapped(
|
||||
ui,
|
||||
catalog.text(Key::AdoptScanning).to_string(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
);
|
||||
};
|
||||
let mut body = column![
|
||||
field_line(
|
||||
ui,
|
||||
catalog.text(Key::AdoptPlanRunDir),
|
||||
&plan.run_dir.display().to_string()
|
||||
),
|
||||
field_line(
|
||||
ui,
|
||||
catalog.text(Key::AdoptPlanIsolation),
|
||||
&isolation_label(plan.isolation, catalog),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Sp::X2))
|
||||
.width(Length::Fill);
|
||||
|
||||
// 后果话术:core `target_notes()` 的原文,一条一行、一个字不改
|
||||
for note in &plan.notes {
|
||||
body = body.push(wrapped(ui, note.clone(), ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
body.into()
|
||||
}
|
||||
|
||||
/// 失败:稳定码 + **problems 逐条**。
|
||||
pub(super) fn failed<'a>(
|
||||
ui: &'a Ui,
|
||||
failure: Option<&'a AdoptFailureView>,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, Message> {
|
||||
let Some(failure) = failure else {
|
||||
return wrapped(
|
||||
ui,
|
||||
catalog.text(Key::AdoptScanning).to_string(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
);
|
||||
};
|
||||
let mut body = column![mono(ui, failure.code.clone())]
|
||||
.spacing(ui.space(Sp::X2))
|
||||
.width(Length::Fill);
|
||||
|
||||
if failure.problems.is_empty() {
|
||||
body = body.push(wrapped(
|
||||
ui,
|
||||
failure.message.clone(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
));
|
||||
return body.into();
|
||||
}
|
||||
body = body.push(wrapped(
|
||||
ui,
|
||||
catalog.text(Key::AdoptProblemCount).to_string(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
));
|
||||
for problem in &failure.problems {
|
||||
body = body.push(problem_line(ui, problem));
|
||||
}
|
||||
scrollable(body)
|
||||
.height(Length::Fixed(CARD_LIST_HEIGHT))
|
||||
.style(frame::scroll_style(ui))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 完成:实例名 + 运行目录 + 提示原文。
|
||||
pub(super) fn done<'a>(
|
||||
ui: &'a Ui,
|
||||
done: Option<&'a AdoptDoneView>,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, Message> {
|
||||
let Some(done) = done else {
|
||||
return wrapped(ui, String::new(), ui.color(Role::OnSurfaceVariant));
|
||||
};
|
||||
let mut body = column![
|
||||
field_line(ui, catalog.text(Key::AdoptTitle), &done.name),
|
||||
field_line(
|
||||
ui,
|
||||
catalog.text(Key::AdoptPlanRunDir),
|
||||
&done.run_dir.display().to_string()
|
||||
),
|
||||
field_line(
|
||||
ui,
|
||||
catalog.text(Key::AdoptPlanIsolation),
|
||||
&isolation_label(done.isolation, catalog),
|
||||
),
|
||||
]
|
||||
.spacing(ui.space(Sp::X2))
|
||||
.width(Length::Fill);
|
||||
|
||||
for note in &done.notes {
|
||||
body = body.push(wrapped(ui, note.clone(), ui.color(Role::OnSurfaceVariant)));
|
||||
}
|
||||
body.into()
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
//! Pick 段:候选列表 + 目标档 + 实例名。
|
||||
//!
|
||||
//! **可导入的候选才可点**:不可导入的行把 `problems` 逐条列在下面但不接点击——点它只会换来
|
||||
//! 一条注定被 core 拒绝的命令(`adopt_not_adoptable`),而用户从行上已经看到了原因。
|
||||
//!
|
||||
//! 目标档三枚 chip 的**短提示**在选中时立刻换(共享/版本目录档用 `Error` 色);
|
||||
//! **后果原文**不在这里——它在 Preview 段逐条画 `plan.notes`(core `target_notes()` 的原文)。
|
||||
|
||||
use iced::widget::{button, column, container, row, scrollable};
|
||||
use iced::{Alignment, Background, Border, Color, Element, Length};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::shape::ShapeScale;
|
||||
use unlucky_design::spacing::Space as Sp;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
use unlucky_presenter::dto::{AdoptCandidateView, AdoptScanView, AdoptTargetView};
|
||||
|
||||
use crate::app::components::inputs::{text_field, Field};
|
||||
use crate::app::components::selection::{chip, Chip};
|
||||
use crate::pages::frame;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::message::Message;
|
||||
use super::text::{label, problem_line, target_label, wrapped};
|
||||
use super::{adopt, AdoptMessage, Form, CARD_LIST_HEIGHT};
|
||||
|
||||
/// Pick 段整体。
|
||||
pub(super) fn pick<'a>(
|
||||
ui: &'a Ui,
|
||||
form: &'a Form,
|
||||
scan: Option<&'a AdoptScanView>,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, Message> {
|
||||
let Some(scan) = scan else {
|
||||
// 扫描还没回来:一行进度文案(不做假进度条)
|
||||
return wrapped(
|
||||
ui,
|
||||
catalog.text(Key::AdoptScanning).to_string(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
);
|
||||
};
|
||||
|
||||
let mut body = column![].spacing(ui.space(Sp::X3)).width(Length::Fill);
|
||||
if scan.is_empty() {
|
||||
let dir = scan.versions_dir.display().to_string();
|
||||
body = body.push(wrapped(
|
||||
ui,
|
||||
catalog.render(Key::AdoptEmpty, &[("dir", &dir)]),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
));
|
||||
} else {
|
||||
body = body.push(label(ui, catalog.text(Key::AdoptSelectLabel)));
|
||||
body = body.push(candidate_list(ui, form, scan, catalog));
|
||||
}
|
||||
|
||||
body = body.push(frame::divider(ui).map(Message::Page));
|
||||
body = body.push(label(ui, catalog.text(Key::AdoptTargetLabel)));
|
||||
body = body.push(target_row(ui, form, catalog));
|
||||
body = body.push(text_field(
|
||||
ui,
|
||||
Field::new(form.name.as_str()).label(catalog.text(Key::AdoptNameLabel)),
|
||||
|value| adopt(AdoptMessage::Name(value)),
|
||||
));
|
||||
body.into()
|
||||
}
|
||||
|
||||
/// 候选列表(可滚,定高:列表再长页脚按钮也留在卡里)。
|
||||
fn candidate_list<'a>(
|
||||
ui: &'a Ui,
|
||||
form: &'a Form,
|
||||
scan: &'a AdoptScanView,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, Message> {
|
||||
let rows = column(
|
||||
scan.candidates
|
||||
.iter()
|
||||
.map(|candidate| candidate_row(ui, form, candidate, catalog))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.spacing(ui.space(Sp::X1))
|
||||
.width(Length::Fill);
|
||||
|
||||
scrollable(rows)
|
||||
.height(Length::Fixed(CARD_LIST_HEIGHT))
|
||||
.style(frame::scroll_style(ui))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一条候选。
|
||||
fn candidate_row<'a>(
|
||||
ui: &'a Ui,
|
||||
form: &'a Form,
|
||||
candidate: &'a AdoptCandidateView,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, Message> {
|
||||
let selected = form.selected.as_deref() == Some(candidate.version_id.as_str());
|
||||
|
||||
let mut items = column![row![
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::BodyLarge,
|
||||
selected,
|
||||
candidate.version_id.as_str(),
|
||||
ui.color(Role::OnSurface),
|
||||
)
|
||||
.map(Message::Page),
|
||||
container(status(ui, candidate, selected, catalog))
|
||||
.width(Length::Fill)
|
||||
.align_x(Alignment::End),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_y(Alignment::Center)]
|
||||
.spacing(ui.space(Sp::X1))
|
||||
.width(Length::Fill);
|
||||
|
||||
// 问题逐条(不折叠):候选阶段就把「为什么不能导入」说清楚
|
||||
for problem in &candidate.problems {
|
||||
items = items.push(problem_line(ui, problem));
|
||||
}
|
||||
|
||||
let body = container(items)
|
||||
.width(Length::Fill)
|
||||
.padding(ui.space(Sp::X2))
|
||||
.style(move |_theme| container::Style {
|
||||
background: selected.then(|| Background::Color(ui.color(Role::SecondaryContainer))),
|
||||
border: Border {
|
||||
radius: ui.radius_dp(ShapeScale::Medium),
|
||||
..Border::default()
|
||||
},
|
||||
..container::Style::default()
|
||||
});
|
||||
|
||||
if !candidate.adoptable() {
|
||||
// 不可导入的行**不可点**(也就不存在「点了才被告知不行」这条路)
|
||||
return body.into();
|
||||
}
|
||||
button(body)
|
||||
.width(Length::Fill)
|
||||
.padding(0)
|
||||
.style(|_theme, _status| button::Style {
|
||||
background: None,
|
||||
text_color: Color::TRANSPARENT,
|
||||
border: Border::default(),
|
||||
shadow: iced::Shadow::default(),
|
||||
snap: true,
|
||||
})
|
||||
.on_press(adopt(AdoptMessage::Select(candidate.version_id.clone())))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 候选右侧那枚状态(可导入 / N 个问题 / 已被某实例使用)。
|
||||
fn status<'a>(
|
||||
ui: &'a Ui,
|
||||
candidate: &AdoptCandidateView,
|
||||
selected: bool,
|
||||
catalog: Catalog,
|
||||
) -> Element<'a, Message> {
|
||||
if !candidate.adoptable() {
|
||||
let count = candidate.problems.len().to_string();
|
||||
return wrapped(
|
||||
ui,
|
||||
catalog.render(Key::AdoptBlocked, &[("count", &count)]),
|
||||
ui.color(Role::Error),
|
||||
);
|
||||
}
|
||||
if let Some(name) = candidate.registered.first() {
|
||||
return wrapped(
|
||||
ui,
|
||||
catalog.render(Key::AdoptRegistered, &[("name", name)]),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
);
|
||||
}
|
||||
if selected {
|
||||
return chip(ui, Chip::current(catalog.text(Key::AdoptReady)));
|
||||
}
|
||||
wrapped(
|
||||
ui,
|
||||
catalog.text(Key::AdoptReady).to_string(),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
)
|
||||
}
|
||||
|
||||
/// 三枚目标 chip(缺省档在最前;共享 / 版本目录档必须**明确点选**)。
|
||||
fn target_row<'a>(ui: &'a Ui, form: &'a Form, catalog: Catalog) -> Element<'a, Message> {
|
||||
let chips = AdoptTargetView::ALL
|
||||
.into_iter()
|
||||
.map(|target| {
|
||||
chip(
|
||||
ui,
|
||||
Chip::filter(
|
||||
target_label(target, catalog),
|
||||
form.target == target,
|
||||
adopt(AdoptMessage::Target(target)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
column![
|
||||
row(chips).spacing(ui.space(Sp::X2)).width(Length::Fill),
|
||||
target_hint(ui, form.target, catalog),
|
||||
]
|
||||
.spacing(ui.space(Sp::X1))
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 目标档的一句话说明(点选后立刻换)。**不是后果原文**:原文在 Preview 段逐条画
|
||||
/// `plan.notes`(core `target_notes()`)。
|
||||
fn target_hint<'a>(ui: &'a Ui, target: AdoptTargetView, catalog: Catalog) -> Element<'a, Message> {
|
||||
let key = match target {
|
||||
AdoptTargetView::InstanceDir => Key::AdoptTargetInstanceDirHint,
|
||||
AdoptTargetView::Shared => Key::AdoptTargetSharedHint,
|
||||
AdoptTargetView::VersionDir => Key::AdoptTargetVersionDirHint,
|
||||
};
|
||||
let color = if target.touches_foreign_dir() {
|
||||
ui.color(Role::Error)
|
||||
} else {
|
||||
ui.color(Role::OnSurfaceVariant)
|
||||
};
|
||||
wrapped(ui, catalog.text(key).to_string(), color)
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//! `app::adopt` 的用态用例(M223):表单态 + 四段投影。
|
||||
//!
|
||||
//! 抓得住:
|
||||
//! - `Form` 的生命周期(open 清上一轮 / close 全清 / settled 解除等待);
|
||||
//! - `candidate()` 在选中项不在列表里时返回 `None`(界面自动退回「请选择」);
|
||||
//! - 四段的文案真源(标题/后果原文/问题逐条)能从 `AdoptState` 投影出来。
|
||||
//!
|
||||
//! 抓不住:**像素**(那是 D30 的截图通道);也不抓 core 的判据(那是 `host/adopt/tests.rs`)。
|
||||
|
||||
use unlucky_i18n::{Key, Lang};
|
||||
use unlucky_presenter::dto::{
|
||||
AdoptCandidateView, AdoptDoneView, AdoptEvent, AdoptFailureView, AdoptIsolationView,
|
||||
AdoptPlanView, AdoptProblemView, AdoptScanView, AdoptStage, AdoptState, AdoptTargetView,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn candidate(version_id: &str, problems: Vec<AdoptProblemView>) -> AdoptCandidateView {
|
||||
AdoptCandidateView {
|
||||
version_id: version_id.to_string(),
|
||||
jar_present: true,
|
||||
inherits: Vec::new(),
|
||||
registered: Vec::new(),
|
||||
problems,
|
||||
}
|
||||
}
|
||||
|
||||
fn state_with_scan() -> AdoptState {
|
||||
let mut state = AdoptState::default();
|
||||
state.apply(AdoptEvent::Scan(Box::new(AdoptScanView {
|
||||
versions_dir: std::path::PathBuf::from("/game/versions"),
|
||||
candidates: vec![candidate("1.20.1", Vec::new())],
|
||||
})));
|
||||
state
|
||||
}
|
||||
|
||||
/// 打开 ⇒ 关闭:上一轮的选中与名字都要清掉(否则下次打开画的是上一次的草稿)。
|
||||
#[test]
|
||||
fn open_and_close_reset_the_form() {
|
||||
let mut form = Form::default();
|
||||
form.open();
|
||||
assert!(form.open);
|
||||
assert_eq!(form.busy, Busy::Scanning);
|
||||
assert_eq!(form.target, AdoptTargetView::InstanceDir);
|
||||
|
||||
form.selected = Some("1.20.1".to_string());
|
||||
form.name = "生存".to_string();
|
||||
form.close();
|
||||
assert!(!form.open);
|
||||
assert!(form.selected.is_none());
|
||||
assert!(form.name.is_empty());
|
||||
|
||||
// 再开一次也不该带着上一轮的选中项
|
||||
form.open();
|
||||
assert!(form.selected.is_none());
|
||||
}
|
||||
|
||||
/// `settled` 只解除等待,不动用户填的东西(回程不该清表单)。
|
||||
#[test]
|
||||
fn settling_keeps_what_the_user_typed() {
|
||||
let mut form = Form::default();
|
||||
form.open();
|
||||
form.selected = Some("1.20.1".to_string());
|
||||
form.name = "生存".to_string();
|
||||
form.settled();
|
||||
assert_eq!(form.busy, Busy::Idle);
|
||||
assert_eq!(form.selected.as_deref(), Some("1.20.1"));
|
||||
assert_eq!(form.name, "生存");
|
||||
}
|
||||
|
||||
/// 选中项不在候选列表里 ⇒ `candidate()` 是 `None`(界面不能对着一个不存在的版本画「预检」)。
|
||||
#[test]
|
||||
fn candidate_is_none_when_the_selection_vanished() {
|
||||
let state = state_with_scan();
|
||||
let mut form = Form {
|
||||
selected: Some("1.20.1".to_string()),
|
||||
..Form::default()
|
||||
};
|
||||
assert!(form.candidate(&state).is_some());
|
||||
form.selected = Some("9.9.9".to_string());
|
||||
assert!(form.candidate(&state).is_none());
|
||||
}
|
||||
|
||||
/// 四段的标题与后果原文都从目录来(不写死英文字面量、不重写 core 的后果句)。
|
||||
#[test]
|
||||
fn the_four_stages_project_from_the_event_state() {
|
||||
let catalog = Lang::ZhCn.catalog();
|
||||
let mut state = state_with_scan();
|
||||
|
||||
// Pick
|
||||
assert_eq!(state.stage(), AdoptStage::Pick);
|
||||
assert_eq!(
|
||||
title_text(&state, catalog),
|
||||
catalog.text(Key::AdoptTitle),
|
||||
"标题走目录"
|
||||
);
|
||||
assert!(subtitle(&state, catalog).contains("不复制"));
|
||||
|
||||
// Preview:`notes` 是 core `target_notes()` 的原文,界面一个字都不许改
|
||||
let note =
|
||||
"运行目录 = /game(共享档):mods/saves/config 会落在别人的 .minecraft 里".to_string();
|
||||
state.apply(AdoptEvent::Planned(Box::new(AdoptPlanView {
|
||||
version_id: "1.20.1".to_string(),
|
||||
name: "1.20.1".to_string(),
|
||||
target: AdoptTargetView::Shared,
|
||||
isolation: AdoptIsolationView::Shared,
|
||||
run_dir: std::path::PathBuf::from("/game"),
|
||||
notes: vec![note.clone()],
|
||||
})));
|
||||
assert_eq!(state.stage(), AdoptStage::Preview);
|
||||
assert_eq!(
|
||||
state.plan.as_ref().map(|plan| plan.notes.first().cloned()),
|
||||
Some(Some(note.clone())),
|
||||
"后果原文必须原样留着"
|
||||
);
|
||||
|
||||
// Failed:problems 逐条(不折叠)
|
||||
state.apply(AdoptEvent::Failed(Box::new(AdoptFailureView {
|
||||
code: "adopt_not_adoptable".to_string(),
|
||||
message: "版本 1.19.4 不能登记".to_string(),
|
||||
problems: vec![
|
||||
AdoptProblemView {
|
||||
code: "manifest_missing".to_string(),
|
||||
message: "没有清单".to_string(),
|
||||
},
|
||||
AdoptProblemView {
|
||||
code: "missing_parent".to_string(),
|
||||
message: "缺父版本 1.20.1".to_string(),
|
||||
},
|
||||
],
|
||||
})));
|
||||
assert_eq!(state.stage(), AdoptStage::Failed);
|
||||
assert_eq!(
|
||||
state.failure.as_ref().map(|f| f.problems.len()),
|
||||
Some(2),
|
||||
"两条问题一条都不许吞"
|
||||
);
|
||||
assert_eq!(
|
||||
title_text(&state, catalog),
|
||||
catalog.text(Key::AdoptFailureTitle)
|
||||
);
|
||||
|
||||
// Done
|
||||
state.apply(AdoptEvent::Done(Box::new(AdoptDoneView {
|
||||
instance_id: "id-1".to_string(),
|
||||
name: "1.20.1".to_string(),
|
||||
version_id: "1.20.1".to_string(),
|
||||
isolation: AdoptIsolationView::Shared,
|
||||
run_dir: std::path::PathBuf::from("/game"),
|
||||
dir_created: false,
|
||||
notes: vec![note],
|
||||
})));
|
||||
assert_eq!(state.stage(), AdoptStage::Done);
|
||||
assert_eq!(
|
||||
title_text(&state, catalog),
|
||||
catalog.text(Key::AdoptDoneTitle)
|
||||
);
|
||||
}
|
||||
|
||||
/// 中英两套目录的向导文案都不是占位(`text()` 回落到 key 名就是这个用例要抓的假绿)。
|
||||
#[test]
|
||||
fn both_catalogs_have_the_wizard_wording() {
|
||||
for lang in [Lang::En, Lang::ZhCn] {
|
||||
let catalog = lang.catalog();
|
||||
for key in [
|
||||
Key::AdoptTitle,
|
||||
Key::AdoptSubtitle,
|
||||
Key::AdoptScanning,
|
||||
Key::AdoptEmpty,
|
||||
Key::AdoptSelectLabel,
|
||||
Key::AdoptReady,
|
||||
Key::AdoptTargetLabel,
|
||||
Key::AdoptTargetInstanceDir,
|
||||
Key::AdoptTargetShared,
|
||||
Key::AdoptTargetVersionDir,
|
||||
Key::AdoptNameLabel,
|
||||
Key::AdoptPreview,
|
||||
Key::AdoptConfirm,
|
||||
Key::AdoptBack,
|
||||
Key::AdoptClose,
|
||||
Key::AdoptPlanTitle,
|
||||
Key::AdoptPlanRunDir,
|
||||
Key::AdoptPlanIsolation,
|
||||
Key::AdoptFailureTitle,
|
||||
Key::AdoptProblemCount,
|
||||
Key::AdoptDoneTitle,
|
||||
Key::AdoptDoneHint,
|
||||
] {
|
||||
let value = catalog.text(key);
|
||||
assert!(!value.is_empty(), "{key:?} 在 {lang:?} 是空的");
|
||||
assert_ne!(
|
||||
value,
|
||||
format!("{key:?}"),
|
||||
"{key:?} 在 {lang:?} 没进目录(回落成了 key 名)"
|
||||
);
|
||||
}
|
||||
}
|
||||
// 共享档的短提示必须把「会写进别人的 .minecraft」说清楚(选之前就得知道)
|
||||
let hint = Lang::En.catalog().text(Key::AdoptTargetSharedHint);
|
||||
assert!(hint.contains(".minecraft"), "{hint}");
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
//! 向导里的小工具:等宽行、可换行正文、标签、问题行、档位人话。
|
||||
//!
|
||||
//! 单独一个文件只为守 400 行硬线(AGENTS §3.1):三段视图(`pick` / `panes`)与卡壳
|
||||
//! (`adopt.rs`)都要用它们,放哪儿都会让那一处越线。
|
||||
|
||||
use iced::widget::text::LineHeight;
|
||||
use iced::widget::{column, text};
|
||||
use iced::{Color, Element, Font, Length, Pixels};
|
||||
|
||||
use unlucky_design::color::Role;
|
||||
use unlucky_design::typography::TypeRole;
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
use unlucky_presenter::dto::{AdoptIsolationView, AdoptProblemView, AdoptTargetView};
|
||||
|
||||
use crate::pages::frame;
|
||||
use crate::theme::Ui;
|
||||
|
||||
use super::super::message::Message;
|
||||
|
||||
/// 段内的次级标签(`LabelMedium` + `OnSurfaceVariant`)。
|
||||
pub(super) fn label<'a>(ui: &'a Ui, value: &'a str) -> Element<'a, Message> {
|
||||
frame::text_role(
|
||||
ui,
|
||||
TypeRole::LabelMedium,
|
||||
false,
|
||||
value,
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
)
|
||||
.map(Message::Page)
|
||||
}
|
||||
|
||||
/// 等宽一行(路径 / 机读码要能逐字读)。
|
||||
pub(super) fn mono<'a>(ui: &'a Ui, value: String) -> Element<'a, Message> {
|
||||
let style = ui.style(TypeRole::BodySmall);
|
||||
text(value)
|
||||
.size(style.size_px)
|
||||
.line_height(LineHeight::Absolute(Pixels(style.line_height_px)))
|
||||
.font(Font {
|
||||
family: iced::font::Family::Name(ui.families().mono),
|
||||
..Font::DEFAULT
|
||||
})
|
||||
.color(ui.color(Role::OnSurface))
|
||||
.width(Length::Fill)
|
||||
.wrapping(iced::widget::text::Wrapping::WordOrGlyph)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一段可换行的正文(长路径只按词折行会把卡顶破,所以按词或字形折)。
|
||||
pub(super) fn wrapped<'a>(ui: &'a Ui, value: String, color: Color) -> Element<'a, Message> {
|
||||
frame::text_role_owned(ui, TypeRole::BodySmall, false, value, color).map(Message::Page)
|
||||
}
|
||||
|
||||
/// 「标签 + 值」一行(值用等宽)。
|
||||
pub(super) fn field_line<'a>(ui: &'a Ui, label_text: &'a str, value: &str) -> Element<'a, Message> {
|
||||
column![label(ui, label_text), mono(ui, value.to_string())]
|
||||
.spacing(0.0)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 一条问题:稳定码(等宽) + 人读原因(`Error` 色)。**一条都不许折叠**。
|
||||
pub(super) fn problem_line<'a>(ui: &'a Ui, problem: &'a AdoptProblemView) -> Element<'a, Message> {
|
||||
column![
|
||||
mono(ui, problem.code.clone()),
|
||||
wrapped(ui, problem.message.clone(), ui.color(Role::Error)),
|
||||
]
|
||||
.spacing(0.0)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// 隔离档的人话(沿用实例页已有的两条;`custom` 就是缺省档,直接说「在 ULL 数据区新建」)。
|
||||
pub(super) fn isolation_label(isolation: AdoptIsolationView, catalog: Catalog) -> String {
|
||||
match isolation {
|
||||
AdoptIsolationView::Isolated => catalog.text(Key::InstancesIsolationIsolated).to_string(),
|
||||
AdoptIsolationView::Shared => catalog.text(Key::InstancesIsolationShared).to_string(),
|
||||
AdoptIsolationView::Custom => catalog.text(Key::AdoptTargetInstanceDir).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 目标档的短标签。
|
||||
pub(super) fn target_label(target: AdoptTargetView, catalog: Catalog) -> &'static str {
|
||||
match target {
|
||||
AdoptTargetView::InstanceDir => catalog.text(Key::AdoptTargetInstanceDir),
|
||||
AdoptTargetView::Shared => catalog.text(Key::AdoptTargetShared),
|
||||
AdoptTargetView::VersionDir => catalog.text(Key::AdoptTargetVersionDir),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user