Files
Unlucky-Launcher/crates/app-iced/src/pages/settings/java/prefs_store.rs
T
AskaEth ce463cc9b5 feat(app-iced): Java 页「下载一个 JDK」+ 三个设置键(M197)
用户原话:「我们内置了那么多镜像源 甚至用户不能手动选择指定版本下载。」
「自动下载 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}` 各自成模块。
2026-11-08 20:49:56 +08:00

247 lines
9.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 三个设置键的真读写(M197):`java.installRoot` / `java.vendorPriority` / `java.auto_install`。
//!
//! 与 `store.rs`(M175 的内存 / JVM / 默认 JDK)同一条通路:`core::setting::store::SettingStore`
//! —— 读一份 → 改几个键 → 存 → **再从磁盘读回来**。写盘只认 `Saved` 一档,其余一律当失败
//! (`WriteError` 的三档与 `store.rs` 同一套类型,界面文案也同一套)。
//!
//! ## 「生效值 + 从哪一层来」为什么在这里算
//!
//! 两个键都可能**留空**,而留空的语义分别指向另外两层:
//!
//! | 键 | 留空 = | 生效值的第二层 |
//! |---|---|---|
//! | `java.installRoot` | 按 `core.data_mode` 派生落点 | `JavaInstallRoot::resolve`(portable / standard) |
//! | `java.vendorPriority` | 按地区默认序 | `vendor::order(&[], domestic)` |
//!
//! 所以「现在到底装到哪 / 按什么顺序找」只有把**落盘值**代进 core 的纯函数才说得清。
//! 这两个纯函数本身就是真正下载时用的那一对(`install` 也走 `resolve` 与 `order`),
//! 于是界面上的「生效」与真跑起来的行为不会各说一套。
//!
//! ## 地区判定只做一次
//!
//! `domestic` 从 `cache/region.json` 读(引擎建过一次就有);读不到时按环境信号判一次并写回
//! ——与 `shared::build` 的做法一致。它**不联网**(`region_system` 只跑 `tzutil` / `reg`),
//! 且本函数只在首屏与配置文件变化时被调用,不进心跳。
use std::path::{Path, PathBuf};
use unlucky_core::download::cache::now_ms;
use unlucky_core::download::region::{decide_from, load_cached, store_cached};
use unlucky_core::download::region_system::detect_signals;
use unlucky_core::game::java::vendor::{self, JdkVendor};
use unlucky_core::game::java::{JavaInstallRoot, LocationMode};
use unlucky_core::platform::data_dir::DataMode;
use unlucky_core::platform::layout::LauncherLayout;
use unlucky_core::platform::paths::{Platform, ProcessEnv};
use unlucky_core::setting::keys::launcher_schema;
use unlucky_core::setting::store::{SaveOutcome, SettingStore};
use super::prefs::{Effective, OrderLayer, Prefs, RootLayer, Snapshot};
use super::store::{unwritable, WriteError};
/// 读三键真值。
#[must_use]
pub fn load(config: &Path) -> Snapshot {
let store = SettingStore::load(config, launcher_schema());
snapshot(store.doc())
}
/// 写三键,返回**落盘后读回来**的真值。
///
/// # Errors
/// 本地校验不过([`WriteError::Refused`])、文件只读 / 不可信([`WriteError::Unwritable`])
/// 或 schema 拒绝([`WriteError::Schema`])时返回原因。
pub fn save(config: &Path, prefs: &Prefs) -> Result<Snapshot, WriteError> {
prefs.validate().map_err(WriteError::Refused)?;
let mut store = SettingStore::load(config, launcher_schema());
let root = prefs.install_root.trim().to_string();
let ids = prefs.priority_ids();
// `java.vendorPriority` 是数组键,而 `SettingStore::set` 只收 `serde_json::Value`——
// app-iced 的依赖表里没有 serde_json(本 mission 也改不了 manifest),所以不现造一个
// `Value`:从文档里取**当前那个数组**(schema 默认就是空数组)清空再填,形状与 schema 一致。
let mut array = store
.doc()
.effective("java.vendorPriority")
.cloned()
.unwrap_or_default();
{
let Some(items) = array.as_array_mut() else {
return Err(WriteError::Schema(
"java.vendorPriority 当下不是数组(schema 的默认值是空数组)".to_string(),
));
};
items.clear();
for id in &ids {
items.push(id.as_str().into());
}
}
store
.set("java.installRoot", root.into())
.and_then(|()| store.set("java.auto_install", prefs.auto_install.into()))
.and_then(|()| store.set("java.vendorPriority", array))
.map_err(|error| WriteError::Schema(format!("{error}")))?;
match store.save() {
Ok(SaveOutcome::Saved(_)) => Ok(load(config)),
Ok(SaveOutcome::Skipped(reason)) => Err(WriteError::Unwritable(unwritable(reason))),
Err(error) => Err(WriteError::Schema(format!("{error}"))),
}
}
/// 生效值 + 来源层(渲染器只翻文案)。
#[must_use]
pub fn effective(config: &Path, prefs: &Prefs) -> Effective {
let store = SettingStore::load(config, launcher_schema());
let doc = store.doc();
let data_mode = doc
.effective_str("core.data_mode")
.and_then(DataMode::parse)
.unwrap_or_else(|| DataMode::default_for(Platform::current()));
let override_root = {
let text = prefs.saved_install_root.trim();
(!text.is_empty()).then(|| PathBuf::from(text))
};
let layout = home_of(config).map(|home| LauncherLayout::new(home));
let (root, root_mode, root_layer, root_unresolved) = match &layout {
Some(layout) => match JavaInstallRoot::resolve(
data_mode,
layout,
override_root.as_deref(),
Platform::current(),
&ProcessEnv,
) {
Ok(resolved) => (
resolved.root.display().to_string(),
resolved.mode.as_str(),
layer_of(resolved.mode),
false,
),
Err(error) => {
eprintln!("[java] 解析安装根失败:{error}");
(
String::new(),
data_mode.as_str(),
if override_root.is_some() {
RootLayer::Setting
} else {
RootLayer::DataMode
},
true,
)
}
},
None => (String::new(), data_mode.as_str(), RootLayer::DataMode, true),
};
let domestic = domestic_of(config);
let priority = prefs
.saved_priority
.iter()
.filter_map(|id| JdkVendor::parse(id))
.collect::<Vec<_>>();
let order = vendor::order(&priority, domestic);
Effective {
root,
root_mode,
root_layer,
root_unresolved,
order: order.iter().map(|vendor| vendor.display()).collect(),
order_layer: if priority.is_empty() {
OrderLayer::Region
} else {
OrderLayer::Setting
},
domestic,
}
}
/// 现在的地区判定(`cache/region.json` 优先,没有就判一次并写回)。
///
/// 与 `core::download::shared::build` 同源:同一个 `region.json`、同一套信号。
/// 读不到缓存目录(配置路径不像 `<home>/config/launcher.json`)时按**国内**算——
/// 那是 D23 出厂口径,也是唯一一个「不知道还更保守」的取值(镜像先试不会更差)。
#[must_use]
pub fn domestic_of(config: &Path) -> bool {
let Some(home) = home_of(config) else {
return true;
};
let cache = LauncherLayout::new(home).cache();
let now = now_ms();
let path = cache.join("region.json");
if let Some(profile) = load_cached(&path, now) {
return profile.is_domestic();
}
let profile = decide_from(&detect_signals(), now);
let _ = std::fs::create_dir_all(&cache);
let _ = store_cached(&path, &profile);
profile.is_domestic()
}
/// 托管 JDK 该装到哪(下载作业用;与界面上的「生效」同一个函数)。
#[must_use]
pub fn install_root(config: &Path, saved_install_root: &str) -> Option<JavaInstallRoot> {
let store = SettingStore::load(config, launcher_schema());
let data_mode = store
.doc()
.effective_str("core.data_mode")
.and_then(DataMode::parse)
.unwrap_or_else(|| DataMode::default_for(Platform::current()));
let home = home_of(config)?;
let layout = LauncherLayout::new(home);
let override_root = {
let text = saved_install_root.trim();
(!text.is_empty()).then(|| PathBuf::from(text))
};
JavaInstallRoot::resolve(
data_mode,
&layout,
override_root.as_deref(),
Platform::current(),
&ProcessEnv,
)
.ok()
}
/// 下载引擎的缓存根(`<home>/cache`):让 JDK 归档与其它下载共用同一份缓存 / 健康度。
#[must_use]
pub fn cache_root(config: &Path) -> Option<PathBuf> {
home_of(config).map(|home| LauncherLayout::new(home).cache())
}
/// launcher home(`<home>/config/launcher.json` 的上一级再上一级)。
fn home_of(config: &Path) -> Option<&Path> {
config.parent().and_then(Path::parent)
}
/// 落点模式 → 「从哪一层来」。
fn layer_of(mode: LocationMode) -> RootLayer {
match mode {
LocationMode::Custom => RootLayer::Setting,
LocationMode::Standard | LocationMode::Portable => RootLayer::DataMode,
}
}
/// 文档 → 快照。
fn snapshot(doc: &unlucky_core::setting::document::SettingDocument) -> Snapshot {
Snapshot {
install_root: doc
.effective_str("java.installRoot")
.unwrap_or_default()
.trim()
.to_string(),
auto_install: doc
.effective_bool("java.auto_install")
.unwrap_or(super::prefs::DEFAULT_AUTO_INSTALL),
priority: doc
.effective("java.vendorPriority")
.and_then(|value| value.as_array())
.map(|items| {
items
.iter()
.filter_map(|item| item.as_str())
.map(str::to_string)
.collect()
})
.unwrap_or_default(),
}
}