feat(app): GUI 全量接线,五页改吃 core 真实数据(M43)
app 从「mock 骨架」切到「真实 core host」:新增 src/host 装配层(env/job/ game/launch/identity/versions/install/meter/java/accounts/logs/settings/engine/ dispatch),是 app 里除 settings_bridge 外唯一 use unlucky_core 的地方,出口只有 crate::dto 的 DTO/事件(三通道纪律,AGENTS §6)。 页面接线: - 实例页:profiles+版本 JSON 继承链+会话索引 → 磁贴(真实版本/加载器徽标/最后启动/ 隔离档位/载荷缺失);启动按钮走 LaunchRequest 装配 → Launcher::spawn → JSONL 尾随 → 运行/失败回写;M30 ModeChangeWarning 进横幅通知条。 - 新建实例向导:版本 → 加载器 → 名字 → 确认 → 真实安装进度;Fabric/Quilt 可装, Forge/NeoForge 因需要 installer processors(仅 CLI 有)显式拒装并给出 CLI 命令。 - 下载页:远端 manifest(12h 缓存)+ 内嵌 unlisted → 分类(正式/预览/远古四子类); 安装进度(文件数/字节/速度/ETA,速度由相邻采样差分)+ 阶段边界可取消; Java 分组:四家发行版 + 回退链可视化(计划链/真实 ChainLog)+ 本地运行时。 - 账户页:三轨账户列表(只读公开字段)+ 离线新增 + 删除/切换 + 微软设备码登录 (用户码/链接/轮询状态;取消后丢弃结果)。凭据只在 host/identity 与 host/accounts 出现,DTO 无 token 字段(单测钉住)。 - 设置页:新增 download/java/data/ai/mcp 五组,行由 core::setting schema 键生成, 写回走 SettingStore::set(与 ull config set / AI set_config 同源 launcher.json)。 - 日志页:会话索引 → 列表;事件 JSONL → 日志行(等级高亮)+ D8 规则引擎诊断卡片; 启动中实时尾随(600ms/拍,只读新增字节);崩溃日志导入分析(hs_err/crash/latest)。 架构与守线: - 新页面状态走 ui/page_state.slint 的 PageState global(main.slint 再导出), Rust 侧 page_sync.rs 灌入、callbacks_pages.rs 接线;避免属性逐层转发顶破 400 行。 - DTO 全部 Serialize+Deserialize(IPC 契约),新增 accounts/java/settings/install/ notices 五个子模块;事件扩展 LogAppended/LaunchFinished/AccountsChanged/ DeviceLoginChanged/JavaChanged/InstallProgress/SettingsChanged/NoticesChanged。 - 截图通道扩展:--data-dir(钉数据根且 settings 同源)/--mock/--screenshot-page/ --screenshot-wizard/--screenshot-session/--screenshot-install/--screenshot-launch (真实中间态,取图后自动 stop 防孤儿进程)。 已知边界:GUI 账户「当前启动账户」仅内存态(core schema 无 auth.* 键,app 不自建键); Forge/NeoForge GUI 安装待 processors 执行器上移。
This commit is contained in:
1 parent
bc41dfcf7d
commit
ddc0577f83
64 files changed
+8047
-408
No files matched your search
@@ -33,6 +33,7 @@ pub fn phase(runtime: &InstanceRuntime) -> (RuntimePhase, f32, String) {
|
||||
dto::LaunchStage::Download => RuntimePhase::Downloading,
|
||||
dto::LaunchStage::Verify => RuntimePhase::Verifying,
|
||||
dto::LaunchStage::Launch => RuntimePhase::Launching,
|
||||
dto::LaunchStage::Window => RuntimePhase::Running,
|
||||
};
|
||||
(phase, progress.ratio(), stage.label().to_string())
|
||||
}
|
||||
|
||||
@@ -8,12 +8,17 @@
|
||||
mod agent;
|
||||
mod instances;
|
||||
mod logs;
|
||||
mod pages;
|
||||
mod ui;
|
||||
mod versions;
|
||||
|
||||
pub use agent::{chat, dialog, toasts};
|
||||
pub use instances::{running_tiles, tiles};
|
||||
pub use logs::{diagnosis, log_lines, log_sessions};
|
||||
pub use pages::{
|
||||
accounts, active_account_name, device, install, java_chain, java_runtimes, java_vendors,
|
||||
notices, setting_groups, wizard, wizard_versions,
|
||||
};
|
||||
pub use ui::{
|
||||
accent_color, bg_fill, bg_fill_from, category_from, instance_view, instance_view_from, route,
|
||||
route_from, theme_mode, theme_mode_from, tile_size, tile_size_from, version_category,
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
//! 状态 → `page_state.slint` 的视图模型(账户 / Java / 安装 / 设置分组 / 通知 / 向导)。
|
||||
//!
|
||||
//! 与 `conv/{instances,versions,logs,agent}.rs` 同一套路:只做「领域值 → 展示值」的搬运,
|
||||
//! 不含业务判断。`ui_bind::sync` 把这些灌进 `PageState` global。
|
||||
|
||||
use slint::{ModelRc, SharedString, VecModel};
|
||||
|
||||
use crate::dto::{
|
||||
AccountKind as DtoAccountKind, DeviceLoginPhase, InstallStage as DtoInstallStage,
|
||||
JavaChainOutcome, NoticeLevel, SettingControl, VersionKind as DtoVersionKind,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::wizard::WizardStep;
|
||||
use crate::{
|
||||
AccountKind, InstallStage, JavaStepOutcome, NoticeKind, SettingControl as UiControl, UiAccount,
|
||||
UiDeviceCode, UiInstallProgress, UiJavaRuntime, UiJavaStep, UiJavaVendor, UiNotice,
|
||||
UiSettingGroup, UiSettingRow, UiVersion, UiWizard,
|
||||
};
|
||||
|
||||
fn strings(items: &[String]) -> ModelRc<SharedString> {
|
||||
ModelRc::new(VecModel::from(
|
||||
items
|
||||
.iter()
|
||||
.map(|item| SharedString::from(item.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
/// 账户行。
|
||||
pub fn accounts(state: &AppState) -> Vec<UiAccount> {
|
||||
state
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|account| UiAccount {
|
||||
id: account.id.as_str().into(),
|
||||
name: account.name.as_str().into(),
|
||||
kind: match account.kind {
|
||||
DtoAccountKind::Offline => AccountKind::Offline,
|
||||
DtoAccountKind::Microsoft => AccountKind::Microsoft,
|
||||
DtoAccountKind::Authlib => AccountKind::Authlib,
|
||||
},
|
||||
kind_label: account.kind.label().into(),
|
||||
profile_id: account.profile_id.as_str().into(),
|
||||
login_name: account.login_name.as_str().into(),
|
||||
created: account.created.as_str().into(),
|
||||
active: account.active,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 当前启动账户的展示名(导航底部头像)。
|
||||
pub fn active_account_name(state: &AppState) -> String {
|
||||
state
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|account| account.active)
|
||||
.map(|account| account.name.clone())
|
||||
.unwrap_or_else(|| "未登录".to_string())
|
||||
}
|
||||
|
||||
/// 设备码登录面板。
|
||||
pub fn device(state: &AppState) -> UiDeviceCode {
|
||||
let view = &state.device_login;
|
||||
UiDeviceCode {
|
||||
visible: view.phase.visible(),
|
||||
busy: view.phase.busy(),
|
||||
user_code: view.user_code.as_str().into(),
|
||||
verification_uri: view.verification_uri.as_str().into(),
|
||||
qr_link: view.qr_link.as_str().into(),
|
||||
expires: i32::try_from(view.expires_in).unwrap_or(i32::MAX),
|
||||
message: view.message.as_str().into(),
|
||||
phase_label: match view.phase {
|
||||
DeviceLoginPhase::Idle => "空闲",
|
||||
DeviceLoginPhase::Requesting => "申请中",
|
||||
DeviceLoginPhase::Waiting => "等待确认",
|
||||
DeviceLoginPhase::Succeeded => "成功",
|
||||
DeviceLoginPhase::Failed => "失败",
|
||||
}
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Java 发行版行。
|
||||
pub fn java_vendors(state: &AppState) -> Vec<UiJavaVendor> {
|
||||
state
|
||||
.java
|
||||
.vendors
|
||||
.iter()
|
||||
.map(|vendor| UiJavaVendor {
|
||||
id: vendor.id.as_str().into(),
|
||||
display: vendor.display.as_str().into(),
|
||||
rank: i32::try_from(vendor.rank).unwrap_or(0),
|
||||
majors: vendor.majors.as_str().into(),
|
||||
sources: vendor.sources.join(" · ").into(),
|
||||
installed: vendor.installed,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Java 回退链。
|
||||
pub fn java_chain(state: &AppState) -> Vec<UiJavaStep> {
|
||||
state
|
||||
.java
|
||||
.chain
|
||||
.iter()
|
||||
.map(|step| UiJavaStep {
|
||||
vendor: step.vendor.as_str().into(),
|
||||
outcome: match step.outcome {
|
||||
JavaChainOutcome::Pending => JavaStepOutcome::Pending,
|
||||
JavaChainOutcome::Skip => JavaStepOutcome::Skip,
|
||||
JavaChainOutcome::Failed => JavaStepOutcome::Failed,
|
||||
JavaChainOutcome::Hit => JavaStepOutcome::Hit,
|
||||
},
|
||||
outcome_label: step.outcome.label().into(),
|
||||
detail: step.detail.as_str().into(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Java 本地运行时。
|
||||
pub fn java_runtimes(state: &AppState) -> Vec<UiJavaRuntime> {
|
||||
state
|
||||
.java
|
||||
.runtimes
|
||||
.iter()
|
||||
.map(|runtime| UiJavaRuntime {
|
||||
binary: runtime.binary.as_str().into(),
|
||||
version: runtime.version.as_str().into(),
|
||||
vendor: runtime.vendor.as_str().into(),
|
||||
origin: runtime.origin.as_str().into(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 安装进度卡片。
|
||||
pub fn install(state: &AppState) -> UiInstallProgress {
|
||||
let Some(progress) = &state.install else {
|
||||
return UiInstallProgress {
|
||||
visible: false,
|
||||
stage: InstallStage::Planning,
|
||||
..UiInstallProgress::default()
|
||||
};
|
||||
};
|
||||
UiInstallProgress {
|
||||
visible: true,
|
||||
version: progress.version.as_str().into(),
|
||||
instance: progress.instance.as_str().into(),
|
||||
loader: progress.loader.as_str().into(),
|
||||
stage: match progress.stage {
|
||||
DtoInstallStage::Planning => InstallStage::Planning,
|
||||
DtoInstallStage::Downloading => InstallStage::Downloading,
|
||||
DtoInstallStage::Installing => InstallStage::Installing,
|
||||
DtoInstallStage::Registering => InstallStage::Registering,
|
||||
DtoInstallStage::Done => InstallStage::Done,
|
||||
DtoInstallStage::Failed => InstallStage::Failed,
|
||||
DtoInstallStage::Cancelled => InstallStage::Cancelled,
|
||||
},
|
||||
stage_label: progress.stage.label().into(),
|
||||
ratio: progress.ratio(),
|
||||
known: progress.is_known(),
|
||||
files_label: progress.files_label().into(),
|
||||
bytes_label: if progress.bytes_total > 0 {
|
||||
format!(
|
||||
"{}/{}",
|
||||
crate::dto::human_bytes(progress.bytes_done),
|
||||
crate::dto::human_bytes(progress.bytes_total)
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
.into(),
|
||||
speed: progress.speed_label().into(),
|
||||
eta: progress.eta_label().into(),
|
||||
message: progress.message.as_str().into(),
|
||||
terminal: progress.stage.is_terminal(),
|
||||
failed: progress.stage == DtoInstallStage::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置分组(行 + 控件)。
|
||||
pub fn setting_groups(state: &AppState) -> Vec<UiSettingGroup> {
|
||||
state
|
||||
.settings_groups
|
||||
.iter()
|
||||
.map(|group| UiSettingGroup {
|
||||
id: group.id.as_str().into(),
|
||||
title: group.title.as_str().into(),
|
||||
rows: ModelRc::new(VecModel::from(
|
||||
group
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| UiSettingRow {
|
||||
key: row.key.as_str().into(),
|
||||
label: row.label.as_str().into(),
|
||||
description: row.description.as_str().into(),
|
||||
control: match row.control {
|
||||
SettingControl::Toggle => UiControl::Toggle,
|
||||
SettingControl::Choice => UiControl::Choice,
|
||||
SettingControl::Text => UiControl::Text,
|
||||
SettingControl::ReadOnly => UiControl::Readonly,
|
||||
SettingControl::Action => UiControl::Action,
|
||||
},
|
||||
flag: row.flag,
|
||||
selected: row.selected,
|
||||
options: strings(&row.options),
|
||||
value_text: row.value_text.as_str().into(),
|
||||
action: row.action.as_str().into(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 横幅通知。
|
||||
pub fn notices(state: &AppState) -> Vec<UiNotice> {
|
||||
state
|
||||
.notices
|
||||
.iter()
|
||||
.map(|notice| UiNotice {
|
||||
id: notice.id.as_str().into(),
|
||||
kind: match notice.level {
|
||||
NoticeLevel::Info => NoticeKind::Info,
|
||||
NoticeLevel::Warn => NoticeKind::Warn,
|
||||
NoticeLevel::Error => NoticeKind::Error,
|
||||
},
|
||||
title: notice.title.as_str().into(),
|
||||
body: notice.body.as_str().into(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 向导状态。
|
||||
pub fn wizard(state: &AppState) -> UiWizard {
|
||||
let wizard = &state.wizard;
|
||||
let step = wizard.step;
|
||||
let can_next = match step {
|
||||
WizardStep::Version => !wizard.version.trim().is_empty(),
|
||||
WizardStep::Name => crate::wizard::validate_name(&wizard.name).is_ok(),
|
||||
WizardStep::Loader | WizardStep::Confirm => true,
|
||||
WizardStep::Progress => false,
|
||||
};
|
||||
UiWizard {
|
||||
open: wizard.open,
|
||||
step: i32::try_from(step.index()).unwrap_or(0),
|
||||
step_label: step.title().into(),
|
||||
step_index: i32::try_from(step.index()).unwrap_or(0),
|
||||
step_total: 5,
|
||||
version: wizard.version.as_str().into(),
|
||||
loader: loader_index(wizard.loader),
|
||||
loader_label: if wizard.loader == crate::dto::LoaderKind::Vanilla {
|
||||
"原版".to_string()
|
||||
} else {
|
||||
format!("{} {}", wizard.loader.label(), wizard.loader_version.trim())
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
.into(),
|
||||
loader_version: wizard.loader_version.as_str().into(),
|
||||
name: wizard.name.as_str().into(),
|
||||
error: wizard.error.as_str().into(),
|
||||
busy: wizard.submitting,
|
||||
can_next,
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载器在向导里的单选序号(与 `wizard.slint` 的 LoaderChoice 顺序一致)。
|
||||
pub fn loader_index(loader: crate::dto::LoaderKind) -> i32 {
|
||||
match loader {
|
||||
crate::dto::LoaderKind::Vanilla => 0,
|
||||
crate::dto::LoaderKind::Fabric => 1,
|
||||
crate::dto::LoaderKind::Quilt => 2,
|
||||
crate::dto::LoaderKind::Forge => 3,
|
||||
crate::dto::LoaderKind::NeoForge => 4,
|
||||
crate::dto::LoaderKind::OptiFine | crate::dto::LoaderKind::LiteLoader => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 向导里的版本列表(正式版 + 快照;远古版走下载页的显式入口)。
|
||||
pub fn wizard_versions(state: &AppState) -> Vec<UiVersion> {
|
||||
state
|
||||
.versions
|
||||
.iter()
|
||||
.filter(|version| !version.kind.is_ancient())
|
||||
.map(|version| UiVersion {
|
||||
id: version.id.as_str().into(),
|
||||
kind: match version.kind {
|
||||
DtoVersionKind::Release => crate::VersionKind::Release,
|
||||
_ => crate::VersionKind::Snapshot,
|
||||
},
|
||||
released: version.released.as_str().into(),
|
||||
installed: version.installed,
|
||||
ancient: false,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "pages_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,156 @@
|
||||
//! conv/pages.rs 的单测(拆文件守 400 行上限)。
|
||||
|
||||
use super::*;
|
||||
use crate::dto::{
|
||||
AccountView, DeviceLoginView, InstallProgressView, JavaPageView, JavaVendorView, NoticeView,
|
||||
SettingGroupView, SettingRowView, VersionSummary,
|
||||
};
|
||||
use crate::wizard::WizardState;
|
||||
use slint::Model;
|
||||
|
||||
#[test]
|
||||
fn accounts_and_device_login_render() {
|
||||
let mut state = AppState::new();
|
||||
state.accounts = vec![AccountView {
|
||||
id: "account:1".to_string(),
|
||||
name: "Steve".to_string(),
|
||||
kind: DtoAccountKind::Offline,
|
||||
profile_id: "0f8f".to_string(),
|
||||
login_name: String::new(),
|
||||
created: "2026-10-31 20:12".to_string(),
|
||||
active: true,
|
||||
}];
|
||||
state.device_login =
|
||||
DeviceLoginView::waiting("ABCD-EFGH", "https://www.microsoft.com/link", 900, "");
|
||||
|
||||
let rows = accounts(&state);
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert!(rows[0].active);
|
||||
assert_eq!(rows[0].kind_label, "离线");
|
||||
assert_eq!(active_account_name(&state), "Steve");
|
||||
|
||||
let device = device(&state);
|
||||
assert!(device.visible && device.busy);
|
||||
assert_eq!(device.user_code, "ABCD-EFGH");
|
||||
assert_eq!(device.phase_label, "等待确认");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_progress_renders_labels_and_terminal_flags() {
|
||||
let mut state = AppState::new();
|
||||
state.install = Some(InstallProgressView {
|
||||
version: "1.20.4".to_string(),
|
||||
instance: "生存".to_string(),
|
||||
loader: "Fabric 0.16.5".to_string(),
|
||||
stage: DtoInstallStage::Downloading,
|
||||
files_done: 3,
|
||||
files_total: 6,
|
||||
bytes_done: 1024,
|
||||
bytes_total: 4096,
|
||||
speed_bps: 2048,
|
||||
eta_secs: 2,
|
||||
message: "下载中".to_string(),
|
||||
});
|
||||
let ui = install(&state);
|
||||
assert!(ui.visible && !ui.terminal && !ui.failed);
|
||||
assert!((ui.ratio - 0.5).abs() < f32::EPSILON);
|
||||
assert_eq!(ui.files_label, "3/6 文件");
|
||||
assert_eq!(ui.speed, "2.0 KB/s");
|
||||
assert_eq!(ui.eta, "剩 2s");
|
||||
|
||||
state.install = None;
|
||||
assert!(!install(&state).visible);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_chain_and_vendors_map_outcomes() {
|
||||
let mut state = AppState::new();
|
||||
state.java = JavaPageView {
|
||||
vendors: vec![JavaVendorView {
|
||||
id: "temurin".to_string(),
|
||||
display: "Eclipse Temurin".to_string(),
|
||||
rank: 1,
|
||||
majors: "17/21".to_string(),
|
||||
sources: vec!["BMCLAPI".to_string(), "GitHub".to_string()],
|
||||
installed: true,
|
||||
}],
|
||||
chain: vec![crate::dto::JavaChainStep {
|
||||
vendor: "temurin".to_string(),
|
||||
outcome: JavaChainOutcome::Hit,
|
||||
detail: "21".to_string(),
|
||||
}],
|
||||
..JavaPageView::new()
|
||||
};
|
||||
let vendors = java_vendors(&state);
|
||||
assert_eq!(vendors.len(), 1);
|
||||
assert_eq!(vendors[0].sources, "BMCLAPI · GitHub");
|
||||
let chain = java_chain(&state);
|
||||
assert!(matches!(chain[0].outcome, JavaStepOutcome::Hit));
|
||||
assert_eq!(chain[0].outcome_label, "命中");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_groups_map_controls() {
|
||||
let mut state = AppState::new();
|
||||
state.settings_groups = vec![SettingGroupView::new("download", "下载")
|
||||
.with(SettingRowView::toggle("agent.mcp.enabled", "MCP", "", true))
|
||||
.with(SettingRowView::choice(
|
||||
"download.region",
|
||||
"地区",
|
||||
"",
|
||||
&["auto", "cn"],
|
||||
1,
|
||||
))];
|
||||
let groups = setting_groups(&state);
|
||||
assert_eq!(groups.len(), 1);
|
||||
let rows = groups[0].rows.clone();
|
||||
assert_eq!(rows.row_count(), 2);
|
||||
let first = rows.row_data(0).expect("第一行");
|
||||
let second = rows.row_data(1).expect("第二行");
|
||||
assert!(first.flag);
|
||||
assert!(matches!(second.control, UiControl::Choice));
|
||||
assert_eq!(second.selected, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notices_and_wizard_render() {
|
||||
let mut state = AppState::new();
|
||||
state.notices = vec![NoticeView::new(
|
||||
"isolation:x",
|
||||
NoticeLevel::Warn,
|
||||
"隔离档位已切换",
|
||||
"两侧都有内容",
|
||||
)];
|
||||
let notices = notices(&state);
|
||||
assert!(matches!(notices[0].kind, NoticeKind::Warn));
|
||||
|
||||
state.wizard = WizardState::open();
|
||||
state.wizard.select_version("1.20.4");
|
||||
let wizard = wizard(&state);
|
||||
assert!(wizard.open);
|
||||
assert_eq!(wizard.step_label, "选择版本");
|
||||
assert!(wizard.can_next);
|
||||
assert_eq!(wizard.loader, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_versions_skip_ancient_ones() {
|
||||
let mut state = AppState::new();
|
||||
state.versions = vec![
|
||||
VersionSummary {
|
||||
id: "1.20.4".to_string(),
|
||||
kind: DtoVersionKind::Release,
|
||||
released: "2023-12-07".to_string(),
|
||||
installed: false,
|
||||
},
|
||||
VersionSummary {
|
||||
id: "rd-132211".to_string(),
|
||||
kind: DtoVersionKind::Ancient(crate::dto::AncientKind::Classic),
|
||||
released: "2009-05-13".to_string(),
|
||||
installed: false,
|
||||
},
|
||||
];
|
||||
let rows = wizard_versions(&state);
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(rows[0].id, "1.20.4");
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
//! 版本列表与下载分类映射(下载页)。
|
||||
|
||||
use crate::dto::{AncientKind, VersionKind as DtoVersionKind};
|
||||
use crate::dto::{AncientKind, VersionCategory, VersionKind as DtoVersionKind, VersionSummary};
|
||||
use crate::state::AppState;
|
||||
use crate::{UiVersion, VersionKind};
|
||||
|
||||
/// 下载页版本行(已按「仅已装」+ 版本号筛选词过滤)。
|
||||
pub fn versions(state: &AppState) -> Vec<UiVersion> {
|
||||
let query = state.download_query.trim().to_lowercase();
|
||||
state
|
||||
.versions
|
||||
/// 分类侧栏 → 该分类下的版本(远古版按子类细分)。
|
||||
pub fn in_category(category: VersionCategory, kind: DtoVersionKind) -> bool {
|
||||
match (category, kind) {
|
||||
(VersionCategory::Release, DtoVersionKind::Release) => true,
|
||||
(VersionCategory::Snapshot, DtoVersionKind::Snapshot) => true,
|
||||
(VersionCategory::Ancient(expected), DtoVersionKind::Ancient(actual)) => expected == actual,
|
||||
// 未接线的分类(整合包 / Mod)不显示任何版本,而不是把全部版本倒进去
|
||||
(VersionCategory::Modpack | VersionCategory::Mod, _) => false,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 版本行(下载页与新建向导共用)。
|
||||
pub fn version_rows(versions: &[VersionSummary]) -> Vec<UiVersion> {
|
||||
versions
|
||||
.iter()
|
||||
.filter(|version| !state.installed_only || version.installed)
|
||||
.filter(|version| query.is_empty() || version.id.to_lowercase().contains(&query))
|
||||
.map(|version| UiVersion {
|
||||
id: version.id.clone().into(),
|
||||
kind: version_kind(version.kind),
|
||||
@@ -22,6 +30,20 @@ pub fn versions(state: &AppState) -> Vec<UiVersion> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 下载页版本行(按当前分类 + 「仅已装」+ 版本号筛选词过滤)。
|
||||
pub fn versions(state: &AppState) -> Vec<UiVersion> {
|
||||
let query = state.download_query.trim().to_lowercase();
|
||||
let filtered: Vec<VersionSummary> = state
|
||||
.versions
|
||||
.iter()
|
||||
.filter(|version| in_category(state.prefs.download_category, version.kind))
|
||||
.filter(|version| !state.installed_only || version.installed)
|
||||
.filter(|version| query.is_empty() || version.id.to_lowercase().contains(&query))
|
||||
.cloned()
|
||||
.collect();
|
||||
version_rows(&filtered)
|
||||
}
|
||||
|
||||
fn version_kind(kind: DtoVersionKind) -> VersionKind {
|
||||
match kind {
|
||||
DtoVersionKind::Release => VersionKind::Release,
|
||||
|
||||
Reference in new issue
Block a user