feat(packui): M113 第一批 —— 内容库页的导入/导出区(界面 + DTO + i18n + 拖拽落点)
导入入口(拖拽 + 路径输入 + 目标实例)与导出(格式选择 + 预检)的**界面层**,
数据面镜像 M111/M112 的管线报告(presenter DTO),进度整块复用下载页的安装面板。
## 落了什么
- pages/content/packui*:入口 / 进度块 / 结果块 / 导出预检块(每文件 ≤400)
- 拖拽区(hover 变主色描边)+ 路径输入框 + 目标实例 chips
- 进度:一行「步骤 n/5 · 解包覆盖内容」+ **下载页那块安装面板**(不另画进度条)
- 结果:三态(Success/Partial/Failed)+ 缺文件逐条(Warn/Tertiary)+ 冲突逐条
(成因 + 本次处置 + 可展开三方 SHA-1)+ 致命块(Error/ErrorContainer)
- 导出:mrpack / CF 二选一 + 包名/版本/路径 + 预检「将内联 N 个文件 · 无法导出 M 项」
+ 逐条无法导出项 + 已知损失 + 确认写盘
- presenter/src/dto/pack{,/format,/import,/conflict,/export}.rs:镜像 core 报告的 DTO
(三态、五阶段、三分类、十一条原因,全部 from_slug 与 core 稳定码逐字对齐)
- i18n:81 条 Pack* 键(content_entries! 尾部追加;golden.rs 同序同文)
- app/update.rs:FileDropped / FileHovered / FilesHoveredLeft → 内容库页(本版唯一的选文件通道)
- M109 的四个文件各一处插入:mod 声明 + Pack(..) 变体 + pack 字段 + 正列插入点
## 边界(如实)
- 没有系统文件选择框:依赖树里无对话框依赖,加它要动根 Cargo.toml + 许可门禁 +
Linux 后端选型(影响单文件产物)。入口是拖拽 + 路径输入,原因写在界面上。
- 冲突没有逐文件决策:core 的 ConflictPolicy 是整次导入一个全局档,ConflictFile 是事后账。
本批给「逐文件看 + 整体翻档(以包为准重新导入)」,文案明写「两者都留(改名)暂不支持」。
默认一律 KeepUser——绝不默认覆盖用户改过的文件。
- 本批按钮半通:拖拽/路径/格式/实例/展开都真生效;「点导入真的开跑」「预检/确认写盘」
要等第二批的 AppCommand 与事件通道(M109 rebase 到新 main 后按新形状写一次)。
This commit is contained in:
1 parent
0367073d32
commit
fbeff94e55
31 files changed
+3646
-9
No files matched your search
@@ -0,0 +1,219 @@
|
||||
//! 导入/导出区的视图模型:**presenter 快照 + 页面选择 → 要画的东西**。
|
||||
//!
|
||||
//! ## 分层(与内容库页、下载页同一条)
|
||||
//!
|
||||
//! | 数据源 | 谁转 | 用途 |
|
||||
//! |---|---|---|
|
||||
//! | `AppState::import` / `AppState::export` | [`Pack::of`] | 产品运行时的真页面 |
|
||||
//! | [`super::demo`] 的固定夹具 | 同上 | `pages_demo` 出图与单测 |
|
||||
//!
|
||||
//! 页面自己**不留**业务状态:进度、报告、预检都在 presenter(`ImportState` / `ExportState`)。
|
||||
//! [`PackPageState`] 里只有「用户在这一屏上点了什么」——路径草稿、目标实例、格式、包名、
|
||||
//! 展开的那条冲突。
|
||||
//!
|
||||
//! ## 文件分工(守 400 行)
|
||||
//!
|
||||
//! | 文件 | 职责 |
|
||||
//! |---|---|
|
||||
//! | 本文件 | 整块 [`Pack`] + 页内选择 [`PackPageState`] + 探测诊断 [`Probe`] |
|
||||
//! | [`progress`] | 阶段 / 进度 → 下载页面板的入参(**唯一**的进度形状来源) |
|
||||
//! | [`report`] | 导入结果:三态 + 容忍域逐条 + 致命块 |
|
||||
//! | [`export`] | 导出:格式选择 + 预检(不写盘) |
|
||||
//!
|
||||
//! ## 容忍 / 致命在模型层就分开
|
||||
//!
|
||||
//! [`report::Report::missing`] 是「搞不到某个文件」(实例可用),[`report::Report::fatal`]
|
||||
//! 是「已整包回滚」。两者字段类型不同、画的时候颜色与图标也不同,不可能混成一个列表。
|
||||
|
||||
mod export;
|
||||
mod progress;
|
||||
mod report;
|
||||
|
||||
pub use export::{format_label, Export, Preview};
|
||||
pub use progress::Running;
|
||||
pub use report::{ConflictRow, Report, Row};
|
||||
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
use unlucky_presenter::dto::{ExportState, ImportState, InstanceId, PackFormatView, ProbeView};
|
||||
|
||||
use crate::pages::content::Target;
|
||||
|
||||
/// 页面自留的选择(所有者在 `App::pack`,重建快照不该把它们冲掉)。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PackPageState {
|
||||
/// 路径草稿(拖入或手输;提交后才进请求)
|
||||
pub draft: String,
|
||||
/// 有文件悬在窗口上方(拖拽高亮)
|
||||
pub hover: bool,
|
||||
/// 目标实例(`None` = 按包名新建)
|
||||
pub instance: Option<InstanceId>,
|
||||
/// 新实例名
|
||||
pub instance_name: String,
|
||||
/// 导出格式
|
||||
pub format: PackFormatView,
|
||||
/// 导出包名
|
||||
pub export_name: String,
|
||||
/// 导出包版本
|
||||
pub export_version: String,
|
||||
/// 落盘路径草稿(空 = 让 host 给缺省)
|
||||
pub output: String,
|
||||
/// 导出面板开着吗
|
||||
pub export_open: bool,
|
||||
/// 展开的那条冲突路径
|
||||
pub expanded: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for PackPageState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
draft: String::new(),
|
||||
hover: false,
|
||||
instance: None,
|
||||
instance_name: String::new(),
|
||||
// 导出的首选是 Modrinth:CF 的 `files[]` 只能写能证明 `projectID`/`fileID` 的项,
|
||||
// 而离线目录证明不了任何一条 ⇒ 全内联 + 一堆「表达不了」的注记。mrpack 才是主路。
|
||||
format: PackFormatView::Modrinth,
|
||||
export_name: String::new(),
|
||||
export_version: "1.0.0".to_string(),
|
||||
output: String::new(),
|
||||
export_open: false,
|
||||
expanded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个实例候选(导入目标与导出源共用)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PackTarget {
|
||||
/// 实例 id
|
||||
pub id: InstanceId,
|
||||
/// 一行显示名(`Survival · 1.20.1 · Fabric`)
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
/// 探测失败时的具名诊断块。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Probe {
|
||||
/// 最像哪个格式(`None` = 三个都不像)
|
||||
pub looks_like: Option<PackFormatView>,
|
||||
/// 「看起来像 X,但缺 Y」(或「没有任何已知格式认领」)
|
||||
pub lead: String,
|
||||
/// 包里实际看到的标记(host 已截断,只给代表性几条)
|
||||
pub saw: Vec<String>,
|
||||
/// 兜底说明(core 原文:试过哪些格式)
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
/// 整块导入/导出区要画的东西。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Pack {
|
||||
/// 拖拽高亮
|
||||
pub hover: bool,
|
||||
/// 路径草稿
|
||||
pub draft: String,
|
||||
/// 入口说明(拖什么)
|
||||
pub drop_hint: &'static str,
|
||||
/// 没有系统文件框的原因(如实说,不许装作有)
|
||||
pub no_dialog_note: &'static str,
|
||||
/// 目标实例候选
|
||||
pub targets: Vec<PackTarget>,
|
||||
/// 选中的目标实例
|
||||
pub instance: Option<InstanceId>,
|
||||
/// 新实例名
|
||||
pub instance_name: String,
|
||||
/// 正在导入(有它就画那块面板)
|
||||
pub running: Option<Running>,
|
||||
/// 探测失败诊断
|
||||
pub probe: Option<Probe>,
|
||||
/// 导入结果
|
||||
pub report: Option<Report>,
|
||||
/// 导出块
|
||||
pub export: Export,
|
||||
}
|
||||
|
||||
impl Pack {
|
||||
/// presenter 快照 + 页面选择 → 视图模型(**唯一的真数据入口**)。
|
||||
pub fn of(
|
||||
import: &ImportState,
|
||||
export: &ExportState,
|
||||
page: &PackPageState,
|
||||
targets: &[Target],
|
||||
catalog: Catalog,
|
||||
) -> Self {
|
||||
let candidates: Vec<PackTarget> = targets
|
||||
.iter()
|
||||
.map(|target| PackTarget {
|
||||
id: target.id.clone(),
|
||||
label: format!(
|
||||
"{} · {} · {}",
|
||||
target.name,
|
||||
target.mc_version,
|
||||
crate::pages::content::loader_name(target.loader)
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
let instance = page
|
||||
.instance
|
||||
.clone()
|
||||
.or_else(|| candidates.first().map(|target| target.id.clone()));
|
||||
|
||||
Self {
|
||||
hover: page.hover,
|
||||
draft: page.draft.clone(),
|
||||
drop_hint: catalog.text(Key::PackDropHint),
|
||||
no_dialog_note: catalog.text(Key::PackNoDialog),
|
||||
targets: candidates.clone(),
|
||||
instance: instance.clone(),
|
||||
instance_name: page.instance_name.clone(),
|
||||
running: import
|
||||
.progress
|
||||
.as_ref()
|
||||
.map(|progress| progress::running_of(progress, catalog)),
|
||||
probe: import.probe.as_ref().map(|probe| probe_of(probe, catalog)),
|
||||
report: import
|
||||
.report
|
||||
.as_ref()
|
||||
.map(|report| report::report_of(report, page, catalog)),
|
||||
export: export::export_of(export, page, candidates, catalog),
|
||||
}
|
||||
}
|
||||
|
||||
/// 有没有东西可画(没有就整块收起来,不占版面)。
|
||||
pub fn is_idle(&self) -> bool {
|
||||
self.running.is_none()
|
||||
&& self.probe.is_none()
|
||||
&& self.report.is_none()
|
||||
&& !self.export.open
|
||||
&& self.draft.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// 探测诊断:把「缺哪个标记」摊开。
|
||||
///
|
||||
/// `saw` 为空时补一句「没有可识别的东西」——空列表直接画出来是一行空白,用户会以为是渲染坏了。
|
||||
fn probe_of(probe: &ProbeView, catalog: Catalog) -> Probe {
|
||||
let lead = match probe.looks_like {
|
||||
Some(format) => catalog.render(
|
||||
Key::PackProbeLooksLike,
|
||||
&[
|
||||
("format", export::format_label(format, catalog)),
|
||||
("missing", &probe.missing.join(" · ")),
|
||||
],
|
||||
),
|
||||
None => catalog.text(Key::PackProbeUnknown).to_owned(),
|
||||
};
|
||||
let saw = if probe.saw.is_empty() {
|
||||
vec![catalog.text(Key::PackProbeNothing).to_owned()]
|
||||
} else {
|
||||
probe.saw.clone()
|
||||
};
|
||||
Probe {
|
||||
looks_like: probe.looks_like,
|
||||
lead,
|
||||
saw,
|
||||
detail: probe.detail.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user