用户原话:「我们内置了那么多镜像源 甚至用户不能手动选择指定版本下载。」
「自动下载 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}` 各自成模块。
385 lines
12 KiB
Rust
385 lines
12 KiB
Rust
//! 「下载一个 JDK」的视图(M197):**版本选择列表 / 下载中 / 失败**三态。
|
|
//!
|
|
//! 三态各自能说清什么,是这一屏的全部要求:
|
|
//!
|
|
//! | 态 | 必须出现的 |
|
|
//! |---|---|
|
|
//! | 选择列表 | 8 / 11 / 17 / 21 / 25 每一档 + 它由哪几家发行版承接 + 与当前实例要求的关系 |
|
|
//! | 下载中 | 大版本 + 阶段 + 百分比(分母未知就不给)+ 当前源 + 最近一次「哪个源失败、多久后重试」 |
|
|
//! | 失败 | 大版本 + 最后试到的源 + 「完整原因在日志里」+ 下一步(查网络 / 改顺序) |
|
|
//!
|
|
//! 「与当前实例要求的关系」是任务书第三条:**不许静默失败**。老版本 MC 要 8、新版本要 21/25,
|
|
//! 用户在列表里能直接看到「装这一档也起不来」([`Fit`] 的两向措辞),不必等启动那一刻才发现。
|
|
//!
|
|
//! 列表里的「已安装」判定来自**真扫盘的结论**(`state.runtimes`),不是另存一份安装记录。
|
|
|
|
mod result;
|
|
|
|
use iced::widget::{column, container, row};
|
|
use iced::{Alignment, Element, Length};
|
|
|
|
use unlucky_core::download::DownloadErrorKind;
|
|
use unlucky_core::game::java::catalog::{self, JdkChoice};
|
|
use unlucky_design::color::Role;
|
|
use unlucky_design::spacing::Space;
|
|
use unlucky_design::typography::TypeRole;
|
|
use unlucky_i18n::{Catalog, Key};
|
|
|
|
use crate::app::components::selection::{chip, Chip};
|
|
use crate::pages::frame::{divider, text_role_owned};
|
|
use crate::pages::Message;
|
|
use crate::theme::Ui;
|
|
|
|
use super::downloads::{Event, Job, Phase};
|
|
use super::state::{Fit, State};
|
|
use super::view::msg;
|
|
use super::Message as JavaMessage;
|
|
|
|
/// 一整块(标题 + 口径 + 目录 + 结果)。
|
|
pub(super) fn section<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a, Message> {
|
|
let report = state.downloads.report();
|
|
let mut block = column![].spacing(ui.space(Space::X2)).width(Length::Fill);
|
|
|
|
block = block.push(text_role_owned(
|
|
ui,
|
|
TypeRole::BodyMedium,
|
|
true,
|
|
catalog.text(Key::SettingsJavaDownloadTitle).to_string(),
|
|
ui.color(Role::OnSurface),
|
|
));
|
|
block = block.push(text_role_owned(
|
|
ui,
|
|
TypeRole::BodySmall,
|
|
false,
|
|
catalog.text(Key::SettingsJavaDownloadHint).to_string(),
|
|
ui.color(Role::OnSurfaceVariant),
|
|
));
|
|
if let Some(refusal) = state.download_notice {
|
|
block = block.push(text_role_owned(
|
|
ui,
|
|
TypeRole::BodySmall,
|
|
false,
|
|
catalog.render(
|
|
Key::SettingsJavaDownloadRefused,
|
|
&[("reason", catalog.text(refusal.label()))],
|
|
),
|
|
ui.color(Role::Error),
|
|
));
|
|
}
|
|
if let Some(required) = &state.context.required {
|
|
block = block.push(text_role_owned(
|
|
ui,
|
|
TypeRole::BodySmall,
|
|
false,
|
|
catalog.render(
|
|
Key::SettingsJavaDownloadNeeded,
|
|
&[("requirement", required.describe().as_str())],
|
|
),
|
|
ui.color(Role::OnSurface),
|
|
));
|
|
}
|
|
|
|
let recommended = super::downloads::recommend(
|
|
state.context.required.as_ref(),
|
|
&state.runtimes,
|
|
state.prefs.saved_auto_install,
|
|
);
|
|
let busy = report.job.is_some();
|
|
for choice in catalog::choices() {
|
|
block = block.push(choice_row(
|
|
ui,
|
|
catalog,
|
|
state,
|
|
*choice,
|
|
recommended == Some(choice.major),
|
|
busy,
|
|
));
|
|
}
|
|
|
|
if let Some(job) = &report.job {
|
|
block = block.push(divider(ui));
|
|
block = block.push(running(ui, catalog, job));
|
|
}
|
|
if let Some(success) = &report.success {
|
|
block = block.push(divider(ui));
|
|
block = block.push(result::done(ui, catalog, success));
|
|
}
|
|
if let Some(failure) = &report.failure {
|
|
block = block.push(divider(ui));
|
|
block = block.push(result::failed(ui, catalog, failure, &report.tries));
|
|
}
|
|
block.into()
|
|
}
|
|
|
|
/// 目录里的一行:大版本 + 徽标 + 承接的发行版 + 与当前实例的关系 + 安装按钮。
|
|
fn choice_row<'a>(
|
|
ui: &'a Ui,
|
|
catalog: Catalog,
|
|
state: &'a State,
|
|
choice: JdkChoice,
|
|
recommended: bool,
|
|
busy: bool,
|
|
) -> Element<'a, Message> {
|
|
let title = format!("Java {}", choice.major);
|
|
let mut badges = row![].spacing(ui.space(Space::X1));
|
|
if choice.lts {
|
|
badges = badges.push(chip(
|
|
ui,
|
|
Chip::badge(catalog.text(Key::SettingsJavaDownloadLts)),
|
|
));
|
|
}
|
|
if choice.baseline {
|
|
badges = badges.push(chip(
|
|
ui,
|
|
Chip::badge(catalog.text(Key::SettingsJavaDownloadBaseline)),
|
|
));
|
|
}
|
|
if recommended {
|
|
badges = badges.push(chip(
|
|
ui,
|
|
Chip::badge(catalog.text(Key::SettingsJavaDownloadRecommended)),
|
|
));
|
|
}
|
|
|
|
let vendors = catalog::vendors_for(
|
|
choice.major,
|
|
&state.priority_vendors(),
|
|
state.effective.domestic,
|
|
);
|
|
let vendors_line = if vendors.is_empty() {
|
|
catalog.text(Key::SettingsJavaDownloadNoVendor).to_string()
|
|
} else {
|
|
catalog.render(
|
|
Key::SettingsJavaDownloadVendors,
|
|
&[(
|
|
"vendors",
|
|
vendors
|
|
.iter()
|
|
.map(|vendor| vendor.display())
|
|
.collect::<Vec<_>>()
|
|
.join(" → ")
|
|
.as_str(),
|
|
)],
|
|
)
|
|
};
|
|
|
|
let installed = state
|
|
.runtimes
|
|
.iter()
|
|
.find(|runtime| runtime.major == Some(choice.major));
|
|
|
|
let mut block = column![row![
|
|
container(
|
|
column![
|
|
text_role_owned(
|
|
ui,
|
|
TypeRole::BodyMedium,
|
|
recommended,
|
|
title,
|
|
ui.color(Role::OnSurface),
|
|
),
|
|
badges,
|
|
]
|
|
.spacing(ui.space(Space::X1))
|
|
.width(Length::Fill),
|
|
)
|
|
.width(Length::Fill),
|
|
install_button(ui, catalog, choice.major, installed.is_some(), busy),
|
|
]
|
|
.spacing(ui.space(Space::X1))
|
|
.width(Length::Fill)
|
|
.align_y(Alignment::Center)]
|
|
.spacing(ui.space(Space::X1))
|
|
.width(Length::Fill);
|
|
|
|
block = block.push(text_role_owned(
|
|
ui,
|
|
TypeRole::BodySmall,
|
|
false,
|
|
vendors_line,
|
|
ui.color(Role::OnSurfaceVariant),
|
|
));
|
|
if let Some(fit) = state.fit(Some(choice.major)) {
|
|
let (text, color) = fit_line(catalog, ui, fit);
|
|
block = block.push(text_role_owned(ui, TypeRole::BodySmall, false, text, color));
|
|
}
|
|
if let Some(runtime) = installed {
|
|
block = block.push(text_role_owned(
|
|
ui,
|
|
TypeRole::BodySmall,
|
|
false,
|
|
catalog.render(
|
|
Key::SettingsJavaDownloadInstalled,
|
|
&[("path", runtime.display_home().as_str())],
|
|
),
|
|
ui.color(Role::OnSurfaceVariant),
|
|
));
|
|
}
|
|
block.into()
|
|
}
|
|
|
|
/// 「装哪一档与当前实例合不合」那一行(两向措辞,来自 [`Fit`])。
|
|
fn fit_line(catalog: Catalog, ui: &Ui, fit: Fit) -> (String, iced::Color) {
|
|
match fit {
|
|
Fit::Ok => (
|
|
catalog.text(Key::SettingsJavaDownloadFits).to_string(),
|
|
ui.color(Role::Primary),
|
|
),
|
|
Fit::TooOld { required, .. } => (
|
|
catalog.render(
|
|
Key::SettingsJavaDownloadTooLow,
|
|
&[("minimum", required.to_string().as_str())],
|
|
),
|
|
ui.color(Role::Error),
|
|
),
|
|
Fit::TooNew { maximum, .. } => (
|
|
catalog.render(
|
|
Key::SettingsJavaDownloadTooHigh,
|
|
&[("maximum", maximum.to_string().as_str())],
|
|
),
|
|
ui.color(Role::Error),
|
|
),
|
|
}
|
|
}
|
|
|
|
/// 安装按钮:已在跑的作业期间一律灰掉(同时只允许一个),已装过的写成「重装」。
|
|
fn install_button<'a>(
|
|
ui: &'a Ui,
|
|
catalog: Catalog,
|
|
major: u32,
|
|
installed: bool,
|
|
busy: bool,
|
|
) -> Element<'a, Message> {
|
|
let label = catalog.text(if installed {
|
|
Key::SettingsJavaDownloadReinstall
|
|
} else {
|
|
Key::SettingsJavaDownloadAction
|
|
});
|
|
if busy {
|
|
return chip(ui, Chip::badge(label));
|
|
}
|
|
chip(
|
|
ui,
|
|
Chip::filter(label, false, msg(JavaMessage::Download(major))),
|
|
)
|
|
}
|
|
|
|
/// 下载中:阶段 + 百分比 + 当前源 + 最近一次退避 / 换源。
|
|
fn running<'a>(ui: &'a Ui, catalog: Catalog, job: &Job) -> Element<'a, Message> {
|
|
let header = catalog.render(
|
|
Key::SettingsJavaDownloadBusy,
|
|
&[("major", job.major.to_string().as_str())],
|
|
);
|
|
let phase = match job.phase {
|
|
Phase::Preparing => catalog
|
|
.text(Key::SettingsJavaDownloadPhasePreparing)
|
|
.to_string(),
|
|
Phase::Downloading => match job.percent() {
|
|
Some(percent) => catalog.render(
|
|
Key::SettingsJavaDownloadPhaseDownloading,
|
|
&[
|
|
("percent", percent.to_string().as_str()),
|
|
("done", result::mega(job.downloaded).as_str()),
|
|
("total", result::mega(job.total.unwrap_or(0)).as_str()),
|
|
],
|
|
),
|
|
// 分母未知:只说已经下了多少,不给假的百分比。
|
|
None => result::mega(job.downloaded),
|
|
},
|
|
Phase::Verifying => catalog
|
|
.text(Key::SettingsJavaDownloadPhaseVerifying)
|
|
.to_string(),
|
|
Phase::Extracting => catalog
|
|
.text(Key::SettingsJavaDownloadPhaseExtracting)
|
|
.to_string(),
|
|
};
|
|
let mut block = column![
|
|
text_role_owned(
|
|
ui,
|
|
TypeRole::BodyMedium,
|
|
true,
|
|
header,
|
|
ui.color(Role::Primary),
|
|
),
|
|
text_role_owned(
|
|
ui,
|
|
TypeRole::BodySmall,
|
|
false,
|
|
phase,
|
|
ui.color(Role::OnSurface),
|
|
),
|
|
]
|
|
.spacing(ui.space(Space::X1))
|
|
.width(Length::Fill);
|
|
if let Some(source) = &job.source {
|
|
block = block.push(text_role_owned(
|
|
ui,
|
|
TypeRole::BodySmall,
|
|
false,
|
|
catalog.render(
|
|
Key::SettingsJavaDownloadSource,
|
|
&[("source", source.as_str())],
|
|
),
|
|
ui.color(Role::OnSurfaceVariant),
|
|
));
|
|
}
|
|
if let Some(event) = &job.last {
|
|
let text = event_line(catalog, event);
|
|
block = block.push(text_role_owned(
|
|
ui,
|
|
TypeRole::BodySmall,
|
|
false,
|
|
text,
|
|
ui.color(Role::OnSurfaceVariant),
|
|
));
|
|
}
|
|
block.into()
|
|
}
|
|
|
|
/// 「哪个源失败 / 多久后重试」或「换源」那一条。
|
|
fn event_line(catalog: Catalog, event: &Event) -> String {
|
|
match event {
|
|
Event::Retry {
|
|
source,
|
|
kind,
|
|
attempt,
|
|
retry_in_ms,
|
|
} => catalog.render(
|
|
Key::SettingsJavaDownloadRetry,
|
|
&[
|
|
("source", source.as_str()),
|
|
("reason", source_reason(catalog, *kind).as_str()),
|
|
("attempt", attempt.to_string().as_str()),
|
|
(
|
|
"seconds",
|
|
format!(
|
|
"{:.1}",
|
|
f64::from(u32::try_from(*retry_in_ms).unwrap_or(u32::MAX)) / 1000.0
|
|
)
|
|
.as_str(),
|
|
),
|
|
],
|
|
),
|
|
Event::Switched { from, to } => catalog.render(
|
|
Key::SettingsJavaDownloadSwitch,
|
|
&[("from", from.as_str()), ("to", to.as_str())],
|
|
),
|
|
}
|
|
}
|
|
|
|
/// `DownloadErrorKind` → 页面文案(core 的原句只进日志,见 `downloads.rs`)。
|
|
pub(super) fn source_reason(catalog: Catalog, kind: DownloadErrorKind) -> String {
|
|
let key = match kind {
|
|
DownloadErrorKind::NotFound => Key::SettingsJavaSourceReasonNotFound,
|
|
DownloadErrorKind::ServerError => Key::SettingsJavaSourceReasonServer,
|
|
DownloadErrorKind::Timeout => Key::SettingsJavaSourceReasonTimeout,
|
|
DownloadErrorKind::Network => Key::SettingsJavaSourceReasonNetwork,
|
|
DownloadErrorKind::Integrity => Key::SettingsJavaSourceReasonIntegrity,
|
|
DownloadErrorKind::Io => Key::SettingsJavaSourceReasonIo,
|
|
DownloadErrorKind::InvalidRequest
|
|
| DownloadErrorKind::RangeUnsupported
|
|
| DownloadErrorKind::AllSourcesFailed => Key::SettingsJavaSourceReasonOther,
|
|
};
|
|
catalog.text(key).to_string()
|
|
}
|