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
@@ -24,6 +24,15 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::dto::{InstanceId, LoaderKind};
|
||||
|
||||
pub use query::ContentSort;
|
||||
|
||||
mod detail;
|
||||
mod query;
|
||||
|
||||
pub use detail::{
|
||||
ContentDetail, ContentDetailRequest, ContentFailure, ContentFailureKind, ContentVersionView,
|
||||
};
|
||||
|
||||
/// 内容类型(内容库页左侧那四条一级侧栏)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ContentKind {
|
||||
@@ -117,50 +126,6 @@ impl ContentSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// 排序方式(两个站点的官方排序项,口径对齐后再映射到各自参数)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ContentSort {
|
||||
/// 相关度(Modrinth `relevance`;CF 没有对应项,退回热度)
|
||||
Relevance,
|
||||
/// 下载量
|
||||
Downloads,
|
||||
/// 最新发布
|
||||
Newest,
|
||||
/// 最近更新
|
||||
Updated,
|
||||
}
|
||||
|
||||
impl ContentSort {
|
||||
/// 四种排序的声明顺序。
|
||||
pub const ALL: [ContentSort; 4] = [
|
||||
ContentSort::Relevance,
|
||||
ContentSort::Downloads,
|
||||
ContentSort::Newest,
|
||||
ContentSort::Updated,
|
||||
];
|
||||
|
||||
/// Modrinth `/v2/search` 的 `index` 参数值。
|
||||
#[must_use]
|
||||
pub const fn modrinth_index(self) -> &'static str {
|
||||
match self {
|
||||
Self::Relevance => "relevance",
|
||||
Self::Downloads => "downloads",
|
||||
Self::Newest => "newest",
|
||||
Self::Updated => "updated",
|
||||
}
|
||||
}
|
||||
|
||||
/// CurseForge 的 `sortField`(1 = 热度,2 = 更新,3 = 发布,4 = 名称)。
|
||||
#[must_use]
|
||||
pub const fn curseforge_field(self) -> u32 {
|
||||
match self {
|
||||
Self::Relevance | Self::Downloads => 1,
|
||||
Self::Newest => 3,
|
||||
Self::Updated => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载器 → Modrinth 的 loader facet 值;`None` = 该加载器没有对应 facet(原版等)。
|
||||
#[must_use]
|
||||
pub const fn loader_facet(loader: LoaderKind) -> Option<&'static str> {
|
||||
@@ -333,14 +298,20 @@ pub struct ContentState {
|
||||
pub results: Option<ContentResults>,
|
||||
/// 有请求在飞(页面画加载态的依据;命中项只由事件写进来,不做乐观回显)
|
||||
pub pending: bool,
|
||||
/// 最近一次失败的原文(`None` = 没失败)
|
||||
pub failed: Option<String>,
|
||||
/// 最近一次失败(`None` = 没失败)。M162 起带**大类**,「没联网」不再是普通失败。
|
||||
pub failed: Option<ContentFailure>,
|
||||
/// **最近一页**是否装满(`hits.len() >= limit`)。
|
||||
///
|
||||
/// 为什么不是「累计条数够一页」:分页是**累加**到 `results.hits` 里的,累计值在第 2 页之后
|
||||
/// 必然 ≥ limit ⇒ 用累计判会得到「has_more 恒真」:加载更多永不消失、「没有更多了」在真实
|
||||
/// 翻页路径上不可达,而且每点一次白发一次网络请求(复审 P1-2)。所以这一位单独记。
|
||||
pub last_page_filled: bool,
|
||||
/// 选中项目的详情(M162:`LoadContentDetail` 的结果;`None` = 还没拉过)
|
||||
pub detail: Option<Box<ContentDetail>>,
|
||||
/// 详情请求在飞
|
||||
pub detail_pending: bool,
|
||||
/// 最近一次详情请求的失败
|
||||
pub detail_failed: Option<ContentFailure>,
|
||||
}
|
||||
|
||||
impl Default for ContentState {
|
||||
@@ -351,11 +322,40 @@ impl Default for ContentState {
|
||||
pending: false,
|
||||
failed: None,
|
||||
last_page_filled: false,
|
||||
detail: None,
|
||||
detail_pending: false,
|
||||
detail_failed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentState {
|
||||
/// 进「详情请求在飞」态并清掉上一条(换选中项时调)。
|
||||
pub fn request_detail(&mut self, project: &str) {
|
||||
if self
|
||||
.detail
|
||||
.as_ref()
|
||||
.is_some_and(|detail| detail.project != project)
|
||||
{
|
||||
self.detail = None;
|
||||
}
|
||||
self.detail_pending = true;
|
||||
self.detail_failed = None;
|
||||
}
|
||||
|
||||
/// 详情落地(迟到的结果(换了选中项)由调用方按 `project` 判)。
|
||||
pub fn apply_detail(&mut self, detail: ContentDetail) {
|
||||
self.detail_pending = false;
|
||||
self.detail_failed = None;
|
||||
self.detail = Some(Box::new(detail));
|
||||
}
|
||||
|
||||
/// 详情失败。
|
||||
pub fn fail_detail(&mut self, failure: ContentFailure) {
|
||||
self.detail_pending = false;
|
||||
self.detail_failed = Some(failure);
|
||||
}
|
||||
|
||||
/// 这批结果还算当前条件吗(`false` = 迟到的事件,丢掉,不许覆盖新的筛选结果)。
|
||||
#[must_use]
|
||||
pub fn accepts(&self, results: &ContentResults) -> bool {
|
||||
|
||||
Reference in new issue
Block a user