用户原话:「我们内置了那么多镜像源 甚至用户不能手动选择指定版本下载。」
「自动下载 jdk17 作为基准版本,后续在设置中提供多版本可供用户下载。」
- core:新增 `game::java::catalog`(8/11/17/21/25 五档 × 能承接的发行版 × 「该装哪档」),
数据全部来自 `JdkVendor::supports` 与 `vendor::order`,不复制第二份真相。
- progress:`SourceFailed` 补一个结构化的 `kind`(在这之前只有中文 `reason`,双语界面
要么放弃语言、要么放弃「哪个源为什么退避」这条用户可见信息)。
- app-iced:`downloads` 模块真下(后台线程 + `ProgressEmitter` + 快照),三态(选择列表 /
下载中 / 失败)都能说出「命中的源 / 退避原因 / 试过哪些源」;成功后重扫并(在
`java.auto_install` 开着且没有默认 JDK 时)把新装的那份设为默认。
- 三个设置键做成可改 UI:`java.installRoot` / `java.vendorPriority` / `java.auto_install`,
各自带「现在生效什么、从哪一层来」(数据目录模式 / 地区默认序),改完回读落盘值。
- `State::satisfied` 换成带方向的 `Fit`:1.12.2 + Java 21 不再被说成「低于要求」。
- i18n:删掉 `SettingsJavaEmptyBody` / `SettingsJavaAddHint` 的 markdown 反引号;
`SettingsJavaNeverScannedBody` 改成不与首屏自动扫矛盾的表述。
- 出图三张(选择列表 / 下载中 / 失败)在 DevDocs/artifacts/m197/。
顺带拆文件守 400 行:`golden/tail.rs` 的 M174 段搬到 `golden/tail_ai.rs`,
`pages/settings/java/{context,prefs,prefs_store,downloads,download_view}` 各自成模块。
203 lines
5.7 KiB
Rust
203 lines
5.7 KiB
Rust
//! 进度事件与发射器。
|
|
//!
|
|
//! 事件是「字节级 + 文件级」双层的载体(设计文档 §5):文件级由 `Started/Completed/Failed`
|
|
//! 表达,字节级由节流后的 `Progress` 表达。`total_bytes` 未知时给 `None`——不做假进度。
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
|
|
|
use super::error::DownloadErrorKind;
|
|
use super::types::{DownloadId, HashAlgo, ResourceKind};
|
|
|
|
/// 一条下载进度事件。
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "event", rename_all = "snake_case")]
|
|
pub enum ProgressEvent {
|
|
/// 开始一次下载(含候选数量与总字节预算)。
|
|
Started {
|
|
/// 关联 id。
|
|
id: DownloadId,
|
|
/// 资源类型。
|
|
kind: ResourceKind,
|
|
/// 逻辑 URL。
|
|
url: String,
|
|
/// 落盘路径。
|
|
dest: String,
|
|
/// 候选源数量。
|
|
candidates: usize,
|
|
/// 总字节数(未知为 `None`)。
|
|
total_bytes: Option<u64>,
|
|
},
|
|
/// 字节级进度(节流后)。
|
|
Progress {
|
|
/// 关联 id。
|
|
id: DownloadId,
|
|
/// 已下载字节。
|
|
downloaded: u64,
|
|
/// 总字节数(未知为 `None`)。
|
|
total_bytes: Option<u64>,
|
|
},
|
|
/// 某个源失败(单次尝试)。
|
|
SourceFailed {
|
|
/// 关联 id。
|
|
id: DownloadId,
|
|
/// 源标识。
|
|
source: String,
|
|
/// 失败分类(**结构化**:文案层按这个类型选 i18n key,`reason` 只进日志)。
|
|
///
|
|
/// M197 补的:在这之前只有 `reason`(core 的中文句子),GUI 要么放弃双语、
|
|
/// 要么放弃「命中哪个源 / 为什么退避」这一条用户可见信息。
|
|
kind: DownloadErrorKind,
|
|
/// 失败原因(短描述)。
|
|
reason: String,
|
|
/// 第几次尝试(从 0 起)。
|
|
attempt: u32,
|
|
/// 是否还会在同源重试。
|
|
will_retry: bool,
|
|
/// 下次重试的等待毫秒数。
|
|
retry_in_ms: u64,
|
|
},
|
|
/// 切换到下一个候选源。
|
|
SourceSwitched {
|
|
/// 关联 id。
|
|
id: DownloadId,
|
|
/// 原源。
|
|
from: String,
|
|
/// 新源。
|
|
to: String,
|
|
},
|
|
/// 进入校验阶段。
|
|
Verifying {
|
|
/// 关联 id。
|
|
id: DownloadId,
|
|
/// 算法。
|
|
algo: HashAlgo,
|
|
/// 期望摘要前缀(8 位,够定位又不太啰嗦)。
|
|
expected_prefix: String,
|
|
},
|
|
/// 下载成功。
|
|
Completed {
|
|
/// 关联 id。
|
|
id: DownloadId,
|
|
/// 字节数。
|
|
bytes: u64,
|
|
/// 是否命中内容寻址缓存。
|
|
from_cache: bool,
|
|
/// 成功的源。
|
|
source: String,
|
|
/// 耗时毫秒。
|
|
elapsed_ms: u64,
|
|
},
|
|
/// 下载失败(所有候选源都失败或不可重试)。
|
|
Failed {
|
|
/// 关联 id。
|
|
id: DownloadId,
|
|
/// 错误分类。
|
|
kind: DownloadErrorKind,
|
|
/// 错误描述。
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
impl ProgressEvent {
|
|
/// 事件归属的下载 id。
|
|
#[must_use]
|
|
pub const fn id(&self) -> DownloadId {
|
|
match self {
|
|
Self::Started { id, .. }
|
|
| Self::Progress { id, .. }
|
|
| Self::SourceFailed { id, .. }
|
|
| Self::SourceSwitched { id, .. }
|
|
| Self::Verifying { id, .. }
|
|
| Self::Completed { id, .. }
|
|
| Self::Failed { id, .. } => *id,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 进度发射器:没有接收端时退化为空操作(core 不关心谁在听)。
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ProgressEmitter {
|
|
tx: Option<UnboundedSender<ProgressEvent>>,
|
|
}
|
|
|
|
impl ProgressEmitter {
|
|
/// 静默发射器(测试、后台静默下载用)。
|
|
#[must_use]
|
|
pub const fn silent() -> Self {
|
|
Self { tx: None }
|
|
}
|
|
|
|
/// 建一对(发射器, 接收端);接收端由 app 层转成 IPC DTO。
|
|
#[must_use]
|
|
pub fn channel() -> (Self, UnboundedReceiver<ProgressEvent>) {
|
|
let (tx, rx) = mpsc::unbounded_channel();
|
|
(Self { tx: Some(tx) }, rx)
|
|
}
|
|
|
|
/// 发一条事件;接收端已关闭则静默丢弃。
|
|
pub fn emit(&self, event: ProgressEvent) {
|
|
if let Some(tx) = &self.tx {
|
|
let _ = tx.send(event);
|
|
}
|
|
}
|
|
|
|
/// 是否没有接收端。
|
|
#[must_use]
|
|
pub const fn is_silent(&self) -> bool {
|
|
self.tx.is_none()
|
|
}
|
|
}
|
|
|
|
/// 进度节流器:距上次发射不足 `interval` 且未完成时压掉本次事件。
|
|
#[derive(Debug, Clone)]
|
|
pub struct ProgressThrottle {
|
|
interval: Duration,
|
|
last_emit: Option<Instant>,
|
|
}
|
|
|
|
impl ProgressThrottle {
|
|
/// 构造节流器。
|
|
#[must_use]
|
|
pub const fn new(interval: Duration) -> Self {
|
|
Self {
|
|
interval,
|
|
last_emit: None,
|
|
}
|
|
}
|
|
|
|
/// 节流间隔。
|
|
#[must_use]
|
|
pub const fn interval(&self) -> Duration {
|
|
self.interval
|
|
}
|
|
|
|
/// 是否应该发射本次进度(`finished = true` 时无条件放行)。
|
|
pub fn should_emit(&mut self, now: Instant, finished: bool) -> bool {
|
|
if finished {
|
|
self.last_emit = Some(now);
|
|
return true;
|
|
}
|
|
match self.last_emit {
|
|
Some(last) if now.duration_since(last) < self.interval => false,
|
|
_ => {
|
|
self.last_emit = Some(now);
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 复位(换源/重试时重新计时)。
|
|
pub fn reset(&mut self) {
|
|
self.last_emit = None;
|
|
}
|
|
}
|
|
|
|
impl Default for ProgressThrottle {
|
|
fn default() -> Self {
|
|
Self::new(Duration::from_millis(250))
|
|
}
|
|
}
|