feat(packui): M113 phase 2 —— 导入导出真正接线(三个共享枚举各只多一个变体)
搬到 M109 rebase 后的新 main(26e764e)之后按**新 main 的形状**写一次,不写两遍。
## 接线
- `dto/pack/event.rs`(新):`PackCommand` / `PackEvent` / `PackState::apply`。
整条流程在三个最挤的共享枚举里各只占**一个**变体(`AppCommand::Pack` / `CoreEvent::Pack` /
`HostTask::Pack{event, epoch}`),子枚举承载细节 —— rebase 时不用替别人定序。
- `events.rs` / `job.rs` / `handle.rs` / `dispatch.rs` / `state.rs`:变体与分支,全部带
`// M113: import/export` 锚点。`state.rs` 只多 1 个字段 + 1 条 apply 分支(399 行)。
- `host/addon/**`(新目录,父文件只加 `mod pack;` + 再导出):
- `pack.rs`:`CoreHost::pack` 落点 + 取消标记 + 实例目录/快照/格式/缺省路径解析
- `pack/import.rs`:探测 → 目标目录 → `import_pack` → 报告(默认 KeepUser,显式才 Overwrite)
- `pack/export.rs`:预检(`scan_instance`,**不写盘**)与写盘(`export_instance`)
- `pack/convert.rs`:core 报告 → DTO,逐字段照搬(三态/五阶段/四处置/三成因/十一原因)
- `pack/diagnose.rs`:探测诊断「像 X 缺 Y」(按标记文件的有无,不看文件名)
- `app/update/content.rs`:导入 / 以包为准 / 预检 / 确认写盘 / 关面板真的派指令;
预检时先记下那份请求,确认时**原样发回**(防「预检 A、写 B」)。
- `pages/content/model.rs`:读 `state.pack`(不再是默认空态)。
- 面板接口按新形状改:`install_panel_model(&InstallProgressView, catalog) -> Option<Download>`
(`InstallView` 不再从 `pages/download` 导出)。
## 取消(如实)
`CancelInstall` 现在同时置导入的取消标记 + 代次 +1:面板立刻收回、旧事件按代次丢弃。
但 core 的 `import_pack` 没有取消令牌、进度回调也拿不到控制流 ⇒ **后台那次导入会跑完**。
所以「取消」的真实含义是「不再显示 + 结果作废」,已在模块文档与报告里写清,并单开 finding。
取消标记用 `Arc<AtomicBool>` 而不是裸静态量:裸静态量会在并行测试间泄漏取消状态(实测到过)。
## 图看出来的三处(塔台要求)
1. `no download source (CurseForge file API not wired)` → `no download source available yet`,
API 名只留在 DTO 的 detail 与日志里;顺带把 `sha512` / `projectID/fileID` 也改成用户话。
2. 右栏 `Installing modpacks is not available yet` → `Installing modpacks from the library into
an instance is not available yet`(那句在 i18n 词表里,不在 M109 页面代码里,所以我自己改)。
3. 结果面板接线后可以关:`Dismiss` / `AbortExport` 发 `PackCommand::Dismiss` → host 回
`PackEvent::Cleared` → 回到入口/浏览态,不再常驻。
## 证据(三条都是真跑,不是假数据)
- `bad_packs_get_a_named_diagnosis`:把文本改名成 .mrpack → 有具名诊断(不是静默、不 panic);
目录形态有 overrides/ 无 manifest.json → 说得出「缺哪个标记」。
- `a_real_pack_runs_through_the_pipeline_end_to_end`:造一个真 MultiMC 实例目录当包,
走 `import::run` 真路径,断言事件流里有进度 + 结果、格式=MutliMC、报告有写盘文件、文件真落地、
代次原样带回。
- `preview_counts_match_the_written_report`:同一实例上跑预检与 `export_instance`,
inline / skipped / remote **三项计数逐项相等**,且逐条路径的归类一致。
门禁:cargo check ✓ / clippy -D warnings(三 crate)EXIT=0 /
cargo test(app-iced 504 + 6 守卫,i18n 24,presenter 181)全绿 / rustfmt --check ✓ /
所有改动文件 ≤400 行(`state.rs` 399、`addon.rs` 106,pack 子目录最大 338)。
This commit is contained in:
1 parent
2129d8cee6
commit
5f09b8113a
23 files changed
+1336
-73
No files matched your search
@@ -11,8 +11,8 @@ use crate::dto::CreateRequest;
|
||||
use crate::dto::{
|
||||
AccountView, AgentEvent, ContentInstallRequest, ContentQuery, ContentResults, DeviceLoginView,
|
||||
DiagnosisReport, InstallProgressView, InstanceId, InstanceRuntime, InstanceSummary,
|
||||
JavaPageView, LoaderKind, LogLine, LogOutcome, LogSession, NoticeView, SessionId,
|
||||
SettingGroupView, VersionCategory, VersionSummary,
|
||||
JavaPageView, LoaderKind, LogLine, LogOutcome, LogSession, NoticeView, PackCommand, PackEvent,
|
||||
SessionId, SettingGroupView, VersionCategory, VersionSummary,
|
||||
};
|
||||
use crate::toast::Toast;
|
||||
|
||||
@@ -98,6 +98,9 @@ pub enum CoreEvent {
|
||||
ContentResults(Box<ContentResults>),
|
||||
/// 内容库检索失败(M109):一句人读原因(没网 / 站点限速 / 缺密钥)
|
||||
ContentFailed(String),
|
||||
// M113: import/export
|
||||
/// 导入/导出(M113):进度、结果、探测诊断、导出预检(子枚举见 [`PackEvent`])
|
||||
Pack(Box<PackEvent>),
|
||||
}
|
||||
|
||||
/// 统一事件队列的元素:core / agent / UI 自产(对话框、Toast)都走这一条路,
|
||||
@@ -186,6 +189,9 @@ pub enum AppCommand {
|
||||
SearchContent(ContentQuery),
|
||||
/// 把某个内容项目装进指定实例(M109;整合包不支持,host 会拒绝并给原因)
|
||||
InstallContent(Box<ContentInstallRequest>),
|
||||
// M113: import/export
|
||||
/// 整合包导入/导出(M113):导入 / 预检 / 确认写盘 / 关面板(子枚举见 [`PackCommand`])
|
||||
Pack(Box<PackCommand>),
|
||||
}
|
||||
|
||||
impl AppCommand {
|
||||
|
||||
@@ -49,7 +49,7 @@ pub use pack::{
|
||||
ExportFileView, ExportPreviewView, ExportReasonView, ExportRequestView, ExportState,
|
||||
FileActionView, ImportErrorView, ImportProgressView, ImportReportView, ImportRequestView,
|
||||
ImportStageView, ImportState, ImportStatusView, ImportedFileView, MissingFileView,
|
||||
MissingReasonView, PackFormatView, ProbeView,
|
||||
MissingReasonView, PackCommand, PackEvent, PackFormatView, PackState, ProbeView,
|
||||
};
|
||||
pub use settings::{SettingControl, SettingGroupView, SettingRowView};
|
||||
pub use versions::{AncientKind, VersionCategory, VersionKind, VersionSummary};
|
||||
@@ -32,11 +32,13 @@
|
||||
//! **绝不默认覆盖用户改过的文件**。
|
||||
|
||||
mod conflict;
|
||||
mod event;
|
||||
mod export;
|
||||
mod format;
|
||||
mod import;
|
||||
|
||||
pub use conflict::{ConflictChoiceView, ConflictFileView, ConflictReasonView};
|
||||
pub use event::{PackCommand, PackEvent, PackState};
|
||||
pub use export::{
|
||||
ExportClassView, ExportDoneView, ExportFileView, ExportPreviewView, ExportReasonView,
|
||||
ExportRequestView, ExportState,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! 导入/导出的**指令、事件与面板状态**(M113):`AppCommand::Pack` / `CoreEvent::Pack` 的载荷。
|
||||
//!
|
||||
//! ## 为什么整条流程只占一个枚举变体
|
||||
//!
|
||||
//! `AppCommand` / `CoreEvent` / `HostTask` 是这条装配链上最拥挤的三个枚举(M106/M107/M108/M109/M114
|
||||
//! 都在往里加变体)。所以导入导出**不各占一个变体**,而是各自一个 [`PackCommand`] / [`PackEvent`]
|
||||
//! 子枚举、外面只多一层包装:共享枚举里只多一行,rebase 时不用替别人定序。
|
||||
//!
|
||||
//! ## 取消是**界面级**的,不是管线级的(如实写)
|
||||
//!
|
||||
//! core 的 `import_pack` 没有取消令牌(`ImportRequest` 里没有 cancel 字段,`progress` 回调是
|
||||
//! 纯通知、拿不到控制流)。所以「取消导入」在本轮的含义是:
|
||||
//!
|
||||
//! 1. 立刻把面板收回([`PackEvent::Progress`] 传 `None`);
|
||||
//! 2. **丢弃**这一次运行之后回来的事件(代次比较,与 M105 的安装取消同一套做法);
|
||||
//! 3. 后台那次导入会继续跑到结束——**它不会因为点了取消而停手**。
|
||||
//!
|
||||
//! 这条必须在报告里如实登记:把它写成「真的停下来了」就是撒谎。
|
||||
//!
|
||||
//! ## 事件 → 状态只有一条路
|
||||
//!
|
||||
//! [`PackState::apply`] 是**唯一**的写入点(与 `AppState::apply` 同一条纪律):页面读到的进度、
|
||||
//! 报告、预检都只能从事件来,页面自己不做乐观回显。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::dto::pack::{
|
||||
ExportDoneView, ExportPreviewView, ExportRequestView, ExportState, ImportProgressView,
|
||||
ImportReportView, ImportRequestView, ImportState, ProbeView,
|
||||
};
|
||||
|
||||
/// app → host 的导入/导出意图。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackCommand {
|
||||
/// 开始导入(目标实例已选好;`instance` 为 `None` = 按 `instance_name` 新建目录)
|
||||
Import(Box<ImportRequestView>),
|
||||
/// 跑**预检**(只扫描分类,**不写盘**)
|
||||
Export(Box<ExportRequestView>),
|
||||
/// 确认写盘。载荷是**预检时那一份请求**,不是页面当前字段——否则「预检 A、写 B」。
|
||||
Confirm(Box<ExportRequestView>),
|
||||
/// 关掉面板(导入结果 / 探测诊断 / 导出预检都收起来,回到入口态)
|
||||
Dismiss,
|
||||
}
|
||||
|
||||
/// host → app 的导入/导出事件。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PackEvent {
|
||||
/// 导入进度(`None` = 收回面板:取消,或流程已经给出结果)
|
||||
Progress(Option<Box<ImportProgressView>>),
|
||||
/// 导入结果(三态 + 逐项原因;致命失败也走它,`report.error` 里有原因)
|
||||
Report(Box<ImportReportView>),
|
||||
/// 探测失败:这个包看起来像什么、缺哪个标记文件
|
||||
Probe(Box<ProbeView>),
|
||||
/// 导出预检结果
|
||||
Preview(Box<ExportPreviewView>),
|
||||
/// 导出写盘完成
|
||||
Exported(Box<ExportDoneView>),
|
||||
/// 导出/预检在 host 侧失败(实例找不到、目录派生失败……一句人读原因)
|
||||
ExportFailed(String),
|
||||
/// 关掉所有面板
|
||||
Cleared,
|
||||
}
|
||||
|
||||
/// 导入/导出两个面板的状态(`AppState::pack` 的全部内容)。
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct PackState {
|
||||
/// 导入侧(进度 / 结果 / 探测诊断)
|
||||
pub import: ImportState,
|
||||
/// 导出侧(预检 / 完成 / 失败)
|
||||
pub export: ExportState,
|
||||
}
|
||||
|
||||
impl PackState {
|
||||
/// 事件 → 状态(**唯一**写入点)。
|
||||
pub fn apply(&mut self, event: PackEvent) {
|
||||
match event {
|
||||
PackEvent::Progress(progress) => {
|
||||
self.import.progress = progress.map(|progress| *progress);
|
||||
}
|
||||
PackEvent::Report(report) => {
|
||||
// 结果到了:进度面板让位,探测诊断作废(这一轮已经有结论了)
|
||||
self.import.progress = None;
|
||||
self.import.probe = None;
|
||||
self.import.report = Some(*report);
|
||||
}
|
||||
PackEvent::Probe(probe) => {
|
||||
self.import.progress = None;
|
||||
self.import.report = None;
|
||||
self.import.probe = Some(*probe);
|
||||
}
|
||||
PackEvent::Preview(preview) => {
|
||||
self.export.pending = false;
|
||||
self.export.failed = None;
|
||||
self.export.done = None;
|
||||
self.export.preview = Some(*preview);
|
||||
}
|
||||
PackEvent::Exported(done) => {
|
||||
self.export.pending = false;
|
||||
self.export.failed = None;
|
||||
// 写完之后预检就过期了:留着会让用户以为「还能再点一次确认」
|
||||
self.export.preview = None;
|
||||
self.export.done = Some(*done);
|
||||
}
|
||||
PackEvent::ExportFailed(reason) => {
|
||||
self.export.pending = false;
|
||||
self.export.failed = Some(reason);
|
||||
}
|
||||
PackEvent::Cleared => {
|
||||
self.import = ImportState::default();
|
||||
self.export = ExportState::default();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 记下「这次预检是对着哪份请求做的」(确认写盘时原样发回)。
|
||||
pub fn set_export_request(&mut self, request: ExportRequestView) {
|
||||
self.export.request = Some(request);
|
||||
self.export.pending = true;
|
||||
self.export.failed = None;
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,8 @@ pub struct ExportDoneView {
|
||||
pub struct ExportState {
|
||||
/// 预检结果(`None` = 还没预检)。
|
||||
pub preview: Option<ExportPreviewView>,
|
||||
/// **这次预检是对着哪份请求做的**(确认写盘时原样发回,防「预检 A、写 B」)。
|
||||
pub request: Option<ExportRequestView>,
|
||||
/// 预检在飞。
|
||||
pub pending: bool,
|
||||
/// 最近一次写盘的结果。
|
||||
|
||||
Reference in new issue
Block a user