导入入口(拖拽 + 路径输入 + 目标实例)与导出(格式选择 + 预检)的**界面层**,
数据面镜像 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 后按新形状写一次)。
222 lines
8.7 KiB
Rust
222 lines
8.7 KiB
Rust
//! 内容库的演示数据(`pages_demo` 出图与单测用;产品路径永远走 `Content::from_snapshot`)。
|
||
//!
|
||
//! ## 为什么要环境变量选「哪一屏」
|
||
//!
|
||
//! D30 的验收矩阵要求「四类内容 × 浅/深」加「搜索无结果」「加载器不兼容禁用」两种状态,
|
||
//! 共 10 张。`pages_demo` 的 `--screen=` 只够选到**哪一页**,选不到「同一页的哪一态」;
|
||
//! 而 `pages/harness.rs` 与 `cli.rs` 不在本 mission 的 scope。
|
||
//!
|
||
//! 所以沿用本仓既有的取证开关范式(`ULL_START_ROUTE` / `ULL_APPEARANCE_*` / `ULL_TICK_TRACE`):
|
||
//!
|
||
//! | 变量 | 取值 | 作用 |
|
||
//! |---|---|---|
|
||
//! | `ULL_CONTENT_KIND` | `modpack` / `mod` / `resourcepack` / `shader` | 选内容类型(默认 `mod`) |
|
||
//! | `ULL_CONTENT_STATE` | `ok` / `empty` / `incompatible` | 选状态(默认 `ok`) |
|
||
//!
|
||
//! 只影响本来就用演示数据的 harness 路径;产品二进制里 `demo()` 根本不会被调用。
|
||
|
||
use unlucky_i18n::{Catalog, Lang};
|
||
use unlucky_presenter::dto::{ContentHit, ContentKind, ContentSort, ContentSource, LoaderKind};
|
||
|
||
use super::model::{blockers_of, Content, ContentQueryView, Hit, HitFacts, Status, Target};
|
||
use crate::pages::download::Download;
|
||
|
||
/// 一个固定的演示环境:两个实例(一个 Fabric / 一个 Forge)。
|
||
fn targets() -> Vec<Target> {
|
||
vec![
|
||
Target {
|
||
id: "survival".to_string(),
|
||
name: "Survival".to_string(),
|
||
mc_version: "1.20.1".to_string(),
|
||
loader: LoaderKind::Fabric,
|
||
},
|
||
Target {
|
||
id: "modded".to_string(),
|
||
name: "Modded".to_string(),
|
||
mc_version: "1.21.1".to_string(),
|
||
loader: LoaderKind::Forge,
|
||
},
|
||
]
|
||
}
|
||
|
||
/// 四条命中项:**先造 `ContentHit` 再走 `Hit::of`**,保证演示数据与产品路径逐字段同源
|
||
/// (自己拼 `Hit` 的那一版会把「1,200,000 下载量」「Fabric 首字母大写」这类格式化漂开)。
|
||
fn hits(kind: ContentKind, catalog: Catalog, compatible_only: bool) -> Vec<Hit> {
|
||
let rows: &[(&str, &str, &str)] = match kind {
|
||
ContentKind::Modpack => &[
|
||
(
|
||
"Fabulously Optimized",
|
||
"robotkoer",
|
||
"Performance-focused vanilla+ pack",
|
||
),
|
||
("Adrenaline", "Someone", "Lightweight kitchen-sink pack"),
|
||
("Better MC", "Luna", "Adventure pack with quests"),
|
||
("All the Mods 9", "ATMTeam", "Kitchen sink, 400+ mods"),
|
||
],
|
||
ContentKind::Mod => &[
|
||
("Sodium", "jellysquid3", "Modern rendering engine"),
|
||
("Lithium", "jellysquid3", "Game logic optimizations"),
|
||
("Iris Shaders", "coderbot", "Shader support for Fabric"),
|
||
("JEI", "mezz", "Item and recipe viewer"),
|
||
],
|
||
ContentKind::Resourcepack => &[
|
||
(
|
||
"Faithful 32x",
|
||
"Faithful",
|
||
"Double resolution vanilla textures",
|
||
),
|
||
("Stay True", "Fred", "Warm, hand-drawn look"),
|
||
("Classic 3D", "Someone", "3D blocks and items"),
|
||
("Bare Bones", "Someone", "Flat, minimalist textures"),
|
||
],
|
||
ContentKind::Shader => &[
|
||
("Complementary Reimagined", "Emin", "Balanced, high quality"),
|
||
("BSL Shaders", "capttatsu", "Soft colors, strong weather"),
|
||
("Photon", "SixthSurge", "Path-traced look"),
|
||
("MakeUp Ultra Fast", "Someone", "Lightweight shader"),
|
||
],
|
||
};
|
||
|
||
rows.iter()
|
||
.enumerate()
|
||
.map(|(index, (title, author, summary))| {
|
||
// 前两条只支持 1.20.1 + Fabric(能装进 Survival);后两条是 1.21.1 + Forge
|
||
let compatible = compatible_only || index < 2;
|
||
Hit::of(
|
||
&ContentHit {
|
||
id: format!("{}-{index}", kind.slug()),
|
||
slug: format!("{}-{index}", kind.slug()),
|
||
title: (*title).to_string(),
|
||
summary: (*summary).to_string(),
|
||
author: (*author).to_string(),
|
||
downloads: 1_200_000u64.saturating_sub(index as u64 * 111_111),
|
||
updated: format!("2026-0{}-1{index}", index + 1),
|
||
source: if index % 2 == 0 {
|
||
ContentSource::Modrinth
|
||
} else {
|
||
ContentSource::Curseforge
|
||
},
|
||
game_versions: if compatible {
|
||
vec!["1.20.1".to_string()]
|
||
} else {
|
||
vec!["1.21.1".to_string()]
|
||
},
|
||
loaders: if compatible {
|
||
vec!["fabric".to_string()]
|
||
} else {
|
||
vec!["forge".to_string()]
|
||
},
|
||
icon: None,
|
||
},
|
||
catalog,
|
||
)
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// 装入中的面板(**复用下载页那块**:`pages::download::install_panel_model` + `install_panel`)。
|
||
fn install_panel(catalog: Catalog) -> Download {
|
||
use unlucky_presenter::dto::{InstallProgressView, InstallStage};
|
||
let progress = InstallProgressView {
|
||
version: "Sodium".to_string(),
|
||
instance: "survival".to_string(),
|
||
loader: "Fabric".to_string(),
|
||
stage: InstallStage::Downloading,
|
||
files_done: 0,
|
||
files_total: 1,
|
||
message: "sodium-fabric-0.6.0.jar".to_string(),
|
||
..InstallProgressView::new()
|
||
};
|
||
crate::pages::download::install_panel_model(&progress, catalog)
|
||
.expect("演示数据的进度是「下载中」,面板必然存在")
|
||
}
|
||
|
||
/// 内建模型(默认 `ok` + 从环境变量读类型/状态)。
|
||
pub(super) fn demo(fallback_kind: ContentKind, lang: Lang) -> Content {
|
||
let catalog = lang.catalog();
|
||
let kind = std::env::var("ULL_CONTENT_KIND")
|
||
.ok()
|
||
.and_then(|value| {
|
||
ContentKind::ALL
|
||
.into_iter()
|
||
.find(|item| item.slug() == value.trim())
|
||
})
|
||
.unwrap_or(fallback_kind);
|
||
let state = std::env::var("ULL_CONTENT_STATE").unwrap_or_else(|_| "ok".to_string());
|
||
|
||
let targets = targets();
|
||
let target = Some("survival".to_string());
|
||
let empty = state == "empty";
|
||
// 「装入中」那一屏:面板就是下载页那块(同一个函数),用来证明它在内容库语义下也可读
|
||
let installing = state == "install";
|
||
let list = if empty {
|
||
Vec::new()
|
||
} else {
|
||
hits(kind, catalog, state != "incompatible")
|
||
};
|
||
// 「不兼容」那一屏:选中的那条是 1.21.1 + Forge 的项目,而目标是 1.20.1 + Fabric
|
||
// 「CF 未接通」那一屏:选中的那条来自 CurseForge(文件端点还没接,按钮必须禁用)
|
||
let selected_index = match state.as_str() {
|
||
"incompatible" => 2,
|
||
"cf" => 1,
|
||
_ => 0,
|
||
};
|
||
let selected = list.get(selected_index).map(|hit| hit.id.clone());
|
||
let blockers = blockers_of(
|
||
list.get(selected_index).map(|hit| HitFacts {
|
||
versions: hit.versions.as_slice(),
|
||
loaders: hit.loaders.as_slice(),
|
||
source: hit.source,
|
||
}),
|
||
kind,
|
||
&targets,
|
||
target.as_deref(),
|
||
);
|
||
let target_label = targets
|
||
.iter()
|
||
.find(|item| Some(&item.id) == target.as_ref())
|
||
.map(|item| item.label());
|
||
|
||
// M113: import/export —— 导入导出区自己那一态由 `ULL_CONTENT_PACK` 选
|
||
// (`importing` / `partial` / `conflict` / `export`;不设 = 只有入口)
|
||
let pack = super::packui::demo_pack(lang).0;
|
||
|
||
Content {
|
||
kind,
|
||
query: ContentQueryView {
|
||
source: ContentSource::Modrinth,
|
||
sort: ContentSort::Relevance,
|
||
version: Some("1.20.1".to_string()),
|
||
loader: Some(LoaderKind::Fabric),
|
||
text: if empty {
|
||
"quarkonium".to_string()
|
||
} else {
|
||
String::new()
|
||
},
|
||
},
|
||
hits: list,
|
||
selected,
|
||
status: if empty { Status::Empty } else { Status::Ready },
|
||
total: if empty { 0 } else { 1_284 },
|
||
// 演示数据故意把 `has_more` 置真(真实判据是「本页装满」):出图要能看到「加载更多」
|
||
has_more: !empty,
|
||
curseforge: false,
|
||
targets,
|
||
target: target.clone(),
|
||
target_label,
|
||
blockers,
|
||
panel: if installing {
|
||
Some(install_panel(catalog))
|
||
} else {
|
||
None
|
||
},
|
||
versions: vec!["1.20.1".to_string(), "1.21.1".to_string()],
|
||
draft: if empty {
|
||
"quarkonium".to_string()
|
||
} else {
|
||
String::new()
|
||
},
|
||
pack,
|
||
}
|
||
}
|