Files
Unlucky-Launcher/crates/app/src/conv/pages.rs
T
AskaEth 5f8382565e
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Unlucky Launcher v0.1.0
单提交快照:main 只保留已发布版本的代码树,开发史在 dev。
2026-11-01 20:02:49 +08:00

301 lines
11 KiB
Rust

//! 状态 → `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;