feat(m175): Java 设置页接真运行时(扫描 / 手动路径 / 默认 JDK / 内存 / JVM 参数)
用户实测四条抱怨逐条对应: - 「没有自动检测本机 Java」:首屏自动扫一次盘(托管根 + JAVA_HOME + PATH + 标准安装位置 + 注册表),之后由「立即扫描」手动触发;扫描在后台线程里跑,带 done/total 进度与取消 - 「显示 Temurin 21.0.4 不知道是不是硬编码」:演示行与绊线测试一起删掉,列表来自真读盘 - 「没一个能改」:默认内存(MB,512~65536,范围与 schema 同源)与 JVM 参数可真改、真落盘、读回 - 「不能手动指定 Java 路径 / 不能改默认版本」:手动添加走严格校验(真跑 java -version), 设为默认写 java.selected_path 并给出「为什么选它」(当前实例的版本门槛 + 每行满足与否) core:java::manual(严格探测 + 厂商识别 + 名单持久化)、java::staged(候选收集 + 带进度与 取消的扫描)、java::proc(带超时且可打断的子进程执行:卡住的 java -version 会被 kill)。 i18n:catalog/settings/java.rs 55 条中英 + 金表按装配顺序插入。 复审 r1(p2-5items)全修: - P2-1 「设为默认」不再清脏:只写 java.selected_path,内存/JVM 草稿一个字节都不动 - P2-2 错误文案全部走 i18n(三档「写不进去」/「路径太长」/「名单写不进去」各自一个 key), 不再把 Rust 枚举名当人话 - P2-3 手动名单落盘有正面断言(加一条 → 从盘上新读一份 → 在) - P2-4 版本号显示到小版本(17.0.13 / 17.0.9 可区分),解析不出来时给原始版本串 - P2-5 每候选 8 秒超时并 kill、取消打断正在跑的探测、扫描槽挂在 State 上、线程活着期间 不许起第二个(界面显示「正在取消…」) 复审 r2(p2-2items)全修: - P2-1 空态不再指向不存在的「Java 页」,改成真实存在的 `ull java install 21` 命令行入口 - P2-2 `auto_scan_due` 增加 `!report.cancelled`:取消过一次之后本进程不再自动扫, 配置文件再变(存内存 / 改主题)也不会把取消无声撤销;手动「立即扫描」不受影响 - 顺带:proc 的 kill+wait 补「子进程真死了」的回归守卫(标记文件从未出现)、 core 的 Schema 错误改成页面自己的 key(不再把中文 Display 直出给英文界面)、 默认态 chip 用新的 `Chip::badge`(不再挂一条永远收不到的消息)
This commit is contained in:
1 parent
30b61e2684
commit
df331f6a60
26 files changed
+4030
-85
No files matched your search
@@ -0,0 +1,284 @@
|
||||
//! 「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));
|
||||
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);
|
||||
let satisfied = state.satisfied(runtime.major);
|
||||
|
||||
let (verdict, verdict_color) = match satisfied {
|
||||
Some(true) => (
|
||||
catalog.text(Key::SettingsJavaFits).to_string(),
|
||||
ui.color(Role::Primary),
|
||||
),
|
||||
Some(false) => {
|
||||
let required = state
|
||||
.context
|
||||
.required
|
||||
.as_ref()
|
||||
.map(|required| required.minimum.to_string())
|
||||
.unwrap_or_default();
|
||||
(
|
||||
catalog.render(Key::SettingsJavaTooOld, &[("minimum", required.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()
|
||||
}
|
||||
Reference in new issue
Block a user