feat(m162): Modrinth 在线内容源(匿名)+ 远端条目按 sha512 补全 + 离线态
core:新增 addon::online —— 按 Modrinth v2 真实形状建模(search facets / project /
project versions / version_file by hash),网络出口只有 Transport 一个 trait,生产实现
EngineTransport 走既有 DownloadEngine::download_bytes(零新依赖);错误分大类,网络类
失败映射成 NotConnected(= 未联网),不再用空列表冒充「没有结果」。
core:online::download::download_verified —— sha1 交给引擎流式校验、sha512 落盘后复核
(download::HashAlgo 没有 Sha512,改它超出 scope,已登记 finding),不符/失败删文件重试,
最多 3 次尝试且不留半截文件。
core:远端条目补全 —— PackFetcher::modrinth_file_by_sha512(默认 Unsupported = 补不出),
import 管线与新增的 scan_pack_online 都会在「有 path 但缺 hashes.sha1」时按 sha512 反查;
补出的 sha1 走 M160 同一套 judge 与合并判定,补不出仍进 unknown_remote 计数(口径未改)。
DownloadEngineFetcher 改用 download_verified,FetchRequest 增 sha512。
presenter:Modrinth 检索/详情改由 core 的 online 客户端提供(不再自己拼 URL、不再读错
date_modified);ContentFailure{kind} 取代裸字符串,无网 = Offline;新增
ContentDetailRequest/ContentDetail + LoadContentDetail 命令与 CoreEvent::ContentDetail 通道;
预检改走 scan_pack_online;内容库安装也走 download_verified(sha1+sha512)。
app-iced:Status::Offline 独立一态(未联网 ≠ 空列表 ≠ 普通失败),详情面板消费新 DTO
显示可用版本;选中项目即拉详情。i18n 新增 6 个键并同步金表。
测试:online 模块 24 个离线用例(录制 JSON + 假字节:搜索/详情/版本/按摘要反查/校验失败
即删/重试/离线分类/补全),import 侧 6 个(离线计数不变、补全后判冲突、补错摘要 ⇒ Failed),
presenter/app-iced 各补失败分类与详情状态机用例。
This commit is contained in:
1 parent
0241f793b2
commit
81278f55af
50 files changed
+3794
-222
No files matched your search
@@ -27,7 +27,7 @@ use unlucky_presenter::dto::{
|
||||
};
|
||||
use unlucky_presenter::state::AppState;
|
||||
|
||||
use unlucky_i18n::Catalog;
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
|
||||
use crate::pages::download::Download;
|
||||
|
||||
@@ -35,7 +35,7 @@ mod items;
|
||||
|
||||
pub(crate) use items::HitFacts;
|
||||
pub(crate) use items::{capitalize, thousands};
|
||||
pub use items::{loader_name, Blocker, Hit, PageState, Status, Target};
|
||||
pub use items::{loader_name, Blocker, Hit, PageState, Status, Target, VersionRow};
|
||||
|
||||
/// 内容库页要画的东西。
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -75,6 +75,10 @@ pub struct Content {
|
||||
pub draft: String,
|
||||
/// 导入/导出区(M113):入口 + 进度 + 结果 + 导出预检
|
||||
pub pack: super::packui::Pack,
|
||||
/// 选中项目的可用版本(M162:详情通道;空 = 还没拉到 / 站点没给)
|
||||
pub available_versions: Vec<VersionRow>,
|
||||
/// 版本区的一句话说明(正在取 / 取不到:原因;`None` = 不必说)
|
||||
pub versions_note: Option<String>,
|
||||
}
|
||||
|
||||
/// 筛选条的展示态(数字/枚举都在这里,页面不再从 `ContentQuery` 里挖字段)。
|
||||
@@ -156,6 +160,9 @@ impl Content {
|
||||
catalog,
|
||||
);
|
||||
|
||||
let (available_versions, versions_note) =
|
||||
detail_view(content, page.selected.as_deref(), catalog);
|
||||
|
||||
Self {
|
||||
kind: page.kind,
|
||||
query: ContentQueryView {
|
||||
@@ -183,6 +190,8 @@ impl Content {
|
||||
versions,
|
||||
draft: page.draft.clone(),
|
||||
pack,
|
||||
available_versions,
|
||||
versions_note,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,8 +222,13 @@ impl Content {
|
||||
|
||||
/// 快照 → 四态。判据只有三个字段(请求在飞 / 失败原因 / 命中项),没有第四份真相。
|
||||
fn status_of(content: &ContentState, hits: &[Hit]) -> Status {
|
||||
if let Some(reason) = &content.failed {
|
||||
return Status::Failed(reason.clone());
|
||||
if let Some(failure) = &content.failed {
|
||||
// M162:没联网单独一态(空列表不能冒充「没有结果」)
|
||||
return if failure.is_offline() {
|
||||
Status::Offline(failure.detail.clone())
|
||||
} else {
|
||||
Status::Failed(failure.detail.clone())
|
||||
};
|
||||
}
|
||||
if content.pending || content.results.is_none() {
|
||||
return Status::Loading;
|
||||
@@ -237,6 +251,57 @@ fn content_install(state: &AppState, catalog: Catalog) -> Option<Download> {
|
||||
.and_then(|progress| crate::pages::download::install_panel_model(progress, catalog))
|
||||
}
|
||||
|
||||
/// 选中项目的版本列表 + 一句说明(详情通道;迟到结果按项目 id 丢)。
|
||||
///
|
||||
/// 三条优先级:**有结果就画结果** → 在飞就画「正在取」 → 失败才画原因。选中项变了而结果是
|
||||
/// 上一个项目的,一律当「没有」(`detail.project != selected`)——否则会画着 A 的版本列表
|
||||
/// 却高亮着 B。
|
||||
fn detail_view(
|
||||
content: &ContentState,
|
||||
selected: Option<&str>,
|
||||
catalog: Catalog,
|
||||
) -> (Vec<VersionRow>, Option<String>) {
|
||||
let Some(selected) = selected else {
|
||||
return (Vec::new(), None);
|
||||
};
|
||||
if let Some(detail) = content.detail.as_ref().filter(|d| d.project == selected) {
|
||||
let rows = detail
|
||||
.versions
|
||||
.iter()
|
||||
.map(|version| VersionRow {
|
||||
number: version.number.clone(),
|
||||
kind: version.kind.clone(),
|
||||
file_name: version.file_name.clone(),
|
||||
line: catalog.render(
|
||||
Key::ContentDetailVersionLine,
|
||||
&[
|
||||
("number", version.number.as_str()),
|
||||
("kind", capitalize(&version.kind).as_str()),
|
||||
("date", version.published.as_str()),
|
||||
],
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
return (rows, None);
|
||||
}
|
||||
if content.detail_pending {
|
||||
return (
|
||||
Vec::new(),
|
||||
Some(catalog.text(Key::ContentDetailFetching).to_string()),
|
||||
);
|
||||
}
|
||||
match &content.detail_failed {
|
||||
Some(failure) => (
|
||||
Vec::new(),
|
||||
Some(catalog.render(
|
||||
Key::ContentDetailFetchFailed,
|
||||
&[("reason", failure.detail.as_str())],
|
||||
)),
|
||||
),
|
||||
None => (Vec::new(), None),
|
||||
}
|
||||
}
|
||||
|
||||
/// 兼容性判定(**纯函数**,单测直接钉它;见模块文档第 2 节)。
|
||||
///
|
||||
/// `hit` 是「选中项的两个支持列表」:`None` = 没选中,`Some((版本, 加载器))` = 选中了。
|
||||
|
||||
Reference in new issue
Block a user