用户原话:「我们内置了那么多镜像源 甚至用户不能手动选择指定版本下载。」
「自动下载 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}` 各自成模块。
293 lines
10 KiB
Rust
293 lines
10 KiB
Rust
//! 「Java」分组的一张卡片(M175)。
|
||
//!
|
||
//! ## 版面(自上而下)
|
||
//!
|
||
//! | 行 | 内容 | 数值 / 证据 |
|
||
//! |---|---|---|
|
||
//! | 状态 | 未检测到 / N 个 JDK / 正在扫描 3/7 | 扫描按钮与取消按钮同步禁用 |
|
||
//! | 运行时列表 | 每个 JDK 一行:路径 + 版本 + 发行方 + 来源 | 「默认」标记 / 「设为默认」chip |
|
||
//! | 版本门槛 | 当前实例需要 Java ≥ N,哪一行不满足 | 来自 app 层算好的 [`Context`] |
|
||
//! | 手动添加 | 输入框 + 「添加」 | 校验不过时下面直接说原因 |
|
||
//! | 默认内存 | 数字框 + 范围提示 + 「现在生效多少来自哪一层」 | 落盘值(不是草稿) |
|
||
//! | JVM 参数 | 文本框 + 拼命令行提示 | 落盘的 `java.extra_args` |
|
||
//! | 动作 | 保存 | 灰掉时下面是原因 |
|
||
//!
|
||
//! 文案一律走 [`unlucky_i18n::Catalog`]:本组要 `Lang`,所以走 `settings/mod.rs` 的按下标分发
|
||
//! (与 General / Update / Assistant 同一条路),`Group.body` 保持 `None`。
|
||
|
||
use iced::widget::{column, container, row};
|
||
use iced::{Alignment, Element, Length};
|
||
|
||
use unlucky_design::color::Role;
|
||
use unlucky_design::spacing::Space;
|
||
use unlucky_design::typography::TypeRole;
|
||
use unlucky_i18n::{Catalog, Key, Lang};
|
||
|
||
use crate::app::components::buttons::{self, Button};
|
||
use crate::app::components::selection::{chip, Chip};
|
||
use crate::pages::frame::{card, divider, text_role_owned};
|
||
use crate::pages::Message;
|
||
use crate::theme::Ui;
|
||
|
||
use super::fields::{actions_row, add_row, hint_line, jvm_field, memory_field, notice_line};
|
||
use super::state::{Progress, RuntimeRow, State};
|
||
use super::Message as JavaMessage;
|
||
use crate::pages::settings::{GroupMessage, States};
|
||
|
||
/// 一条本组消息 → 页面消息。
|
||
pub(super) fn msg(message: JavaMessage) -> Message {
|
||
Message::Settings(GroupMessage::Java(message))
|
||
}
|
||
|
||
/// 一张卡片(`pages::settings::form` 按下标把 `Lang` 递进来)。
|
||
pub(crate) fn body<'a>(ui: &'a Ui, states: States<'a>, lang: Lang) -> Element<'a, Message> {
|
||
body_of(ui, &states.groups.java, lang)
|
||
}
|
||
|
||
fn body_of<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message> {
|
||
let catalog = lang.catalog();
|
||
let mut stacked = column![].spacing(ui.space(Space::X2)).width(Length::Fill);
|
||
|
||
stacked = stacked.push(status_row(ui, catalog, state));
|
||
stacked = stacked.push(divider(ui));
|
||
stacked = stacked.push(runtime_list(ui, catalog, state));
|
||
stacked = stacked.push(divider(ui));
|
||
stacked = stacked.push(add_row(ui, catalog, state));
|
||
// M197:真下载一个 JDK(版本选择列表 / 下载中 / 失败三态都在这一块里)。
|
||
stacked = stacked.push(divider(ui));
|
||
stacked = stacked.push(super::download_view::section(ui, catalog, state));
|
||
// M197:三个设置键(安装目录 / 发行版顺序 / 缺 JDK 时自动准备)。
|
||
stacked = stacked.push(divider(ui));
|
||
stacked = stacked.push(super::prefs_view::section(ui, catalog, state));
|
||
stacked = stacked.push(divider(ui));
|
||
stacked = stacked.push(memory_field(ui, catalog, state));
|
||
stacked = stacked.push(jvm_field(ui, catalog, state));
|
||
stacked = stacked.push(divider(ui));
|
||
stacked = stacked.push(actions_row(ui, catalog, state));
|
||
stacked = stacked.push(hint_line(ui, catalog, state));
|
||
|
||
if let Some(notice) = &state.notice {
|
||
stacked = stacked.push(notice_line(ui, catalog, notice));
|
||
}
|
||
|
||
card(ui, stacked)
|
||
}
|
||
|
||
/// 状态行:未检测到 / N 个 JDK / 正在扫描 / 正在取消 + 两个按钮。
|
||
///
|
||
/// 「取消」的语义(r1 复审 P2-5):按下之后到线程真退出之间,界面显示的是**正在取消…**,
|
||
/// 「立即扫描」保持禁用——否则用户能在旧线程还活着的时候起第二个。
|
||
fn status_row<'a>(ui: &'a Ui, catalog: Catalog, state: &State) -> Element<'a, Message> {
|
||
let (text, scanning) = match (state.scanning, state.scanned) {
|
||
(Some(_), _) if state.cancelled => {
|
||
(catalog.text(Key::SettingsJavaCancelling).to_string(), true)
|
||
}
|
||
// 候选表还在收集(`total == 0`):不显示 `0/0`,给一句「正在扫描…」。
|
||
(Some(Progress { total: 0, .. }), _) => (
|
||
catalog.text(Key::SettingsJavaScanningStart).to_string(),
|
||
true,
|
||
),
|
||
(Some(Progress { done, total }), _) => (
|
||
catalog.render(
|
||
Key::SettingsJavaScanning,
|
||
&[
|
||
("done", done.to_string().as_str()),
|
||
("total", total.to_string().as_str()),
|
||
],
|
||
),
|
||
true,
|
||
),
|
||
(None, true) if state.runtimes.is_empty() => {
|
||
(catalog.text(Key::SettingsJavaEmptyTitle).to_string(), false)
|
||
}
|
||
(None, true) => (
|
||
catalog.render(
|
||
Key::SettingsJavaScanDone,
|
||
&[("count", state.runtimes.len().to_string().as_str())],
|
||
),
|
||
false,
|
||
),
|
||
(None, false) => (
|
||
catalog.text(Key::SettingsJavaNeverScanned).to_string(),
|
||
false,
|
||
),
|
||
};
|
||
|
||
let scan_label = catalog.text(Key::SettingsJavaScan);
|
||
let scan = if scanning || !state.storage {
|
||
buttons::filled(ui, Button::disabled(scan_label))
|
||
} else {
|
||
buttons::filled(ui, Button::new(scan_label, msg(JavaMessage::Scan)))
|
||
};
|
||
// 已经按过取消(线程还没退出)时,「取消」也灰掉:再按一次没有任何意义。
|
||
let cancel = buttons::tonal(
|
||
ui,
|
||
if scanning && !state.cancelled {
|
||
Button::new(
|
||
catalog.text(Key::SettingsJavaCancel),
|
||
msg(JavaMessage::CancelScan),
|
||
)
|
||
} else {
|
||
Button::disabled(catalog.text(Key::SettingsJavaCancel))
|
||
},
|
||
);
|
||
|
||
// 「扫描已取消」那句只在线程**真退出之后**出现;退出之前状态行本身写着「正在取消…」。
|
||
let cancelled = (state.cancelled && state.scanning.is_none()).then(|| {
|
||
text_role_owned(
|
||
ui,
|
||
TypeRole::BodySmall,
|
||
false,
|
||
catalog.text(Key::SettingsJavaCancelled).to_string(),
|
||
ui.color(Role::OnSurfaceVariant),
|
||
)
|
||
});
|
||
|
||
let mut block = column![row![
|
||
container(text_role_owned(
|
||
ui,
|
||
TypeRole::BodyMedium,
|
||
true,
|
||
text,
|
||
ui.color(if state.scanning.is_some() {
|
||
Role::Primary
|
||
} else {
|
||
Role::OnSurface
|
||
}),
|
||
))
|
||
.width(Length::Fill),
|
||
scan,
|
||
cancel,
|
||
]
|
||
.spacing(ui.space(Space::X2))
|
||
.width(Length::Fill)
|
||
.align_y(Alignment::Center),]
|
||
.spacing(ui.space(Space::X1))
|
||
.width(Length::Fill);
|
||
|
||
if let Some(line) = cancelled {
|
||
block = block.push(line);
|
||
}
|
||
block.into()
|
||
}
|
||
|
||
/// 运行时列表:每行一个 JDK,带「默认」标记与「设为默认」chip。
|
||
fn runtime_list<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a, Message> {
|
||
let mut list = column![].spacing(ui.space(Space::X2)).width(Length::Fill);
|
||
|
||
if state.runtimes.is_empty() {
|
||
let body = if state.scanned {
|
||
catalog.text(Key::SettingsJavaEmptyBody)
|
||
} else {
|
||
catalog.text(Key::SettingsJavaNeverScannedBody)
|
||
};
|
||
return text_role_owned(
|
||
ui,
|
||
TypeRole::BodySmall,
|
||
false,
|
||
body.to_string(),
|
||
ui.color(Role::OnSurfaceVariant),
|
||
);
|
||
}
|
||
|
||
for runtime in &state.runtimes {
|
||
list = list.push(runtime_row(ui, catalog, state, runtime));
|
||
}
|
||
list.into()
|
||
}
|
||
|
||
fn runtime_row<'a>(
|
||
ui: &'a Ui,
|
||
catalog: Catalog,
|
||
state: &'a State,
|
||
runtime: &'a RuntimeRow,
|
||
) -> Element<'a, Message> {
|
||
let is_default = super::state::same_path(&runtime.binary, &state.selected);
|
||
// M197:**带方向**的三态。M175 的布尔判定对「1.12.2 + Java 21」会说「低于要求:需要 Java 8
|
||
// 及以上」——21 当然 ≥ 8,那句话自相矛盾;现在两个方向各说各的。
|
||
let (verdict, verdict_color) = match state.fit(runtime.major) {
|
||
Some(super::state::Fit::Ok) => (
|
||
catalog.text(Key::SettingsJavaFits).to_string(),
|
||
ui.color(Role::Primary),
|
||
),
|
||
Some(super::state::Fit::TooOld { required, .. }) => (
|
||
catalog.render(
|
||
Key::SettingsJavaTooOld,
|
||
&[("minimum", required.to_string().as_str())],
|
||
),
|
||
ui.color(Role::Error),
|
||
),
|
||
Some(super::state::Fit::TooNew { maximum, .. }) => (
|
||
catalog.render(
|
||
Key::SettingsJavaTooNew,
|
||
&[("maximum", maximum.to_string().as_str())],
|
||
),
|
||
ui.color(Role::Error),
|
||
),
|
||
None => (String::new(), ui.color(Role::OnSurfaceVariant)),
|
||
};
|
||
|
||
let action = if is_default {
|
||
chip(
|
||
ui,
|
||
// 已选中那颗是**纯展示**的 badge(没有消息,不可交互):r2 复审要求清掉
|
||
// 「禁用态 chip 上还挂着一条真消息」的残留。
|
||
Chip::badge(catalog.text(Key::SettingsJavaDefault)),
|
||
)
|
||
} else {
|
||
chip(
|
||
ui,
|
||
Chip::filter(
|
||
catalog.text(Key::SettingsJavaSetDefault),
|
||
false,
|
||
msg(JavaMessage::SelectRuntime(runtime.binary.clone())),
|
||
),
|
||
)
|
||
};
|
||
|
||
let mut block = column![row![
|
||
container(
|
||
column![
|
||
text_role_owned(
|
||
ui,
|
||
TypeRole::BodyMedium,
|
||
is_default,
|
||
runtime.display_home(),
|
||
ui.color(Role::OnSurface),
|
||
),
|
||
text_role_owned(
|
||
ui,
|
||
TypeRole::BodySmall,
|
||
false,
|
||
format!(
|
||
"{} · {}",
|
||
runtime.summary(),
|
||
catalog.text(runtime.source_key())
|
||
),
|
||
ui.color(Role::OnSurfaceVariant),
|
||
),
|
||
]
|
||
.spacing(ui.space(Space::X1))
|
||
.width(Length::Fill),
|
||
)
|
||
.width(Length::Fill),
|
||
action,
|
||
]
|
||
.spacing(ui.space(Space::X2))
|
||
.width(Length::Fill)
|
||
.align_y(Alignment::Center)]
|
||
.spacing(ui.space(Space::X1))
|
||
.width(Length::Fill);
|
||
|
||
if !verdict.is_empty() {
|
||
block = block.push(text_role_owned(
|
||
ui,
|
||
TypeRole::BodySmall,
|
||
false,
|
||
verdict,
|
||
verdict_color,
|
||
));
|
||
}
|
||
block.into()
|
||
}
|