refactor 2

This commit is contained in:
Julian Freeman
2026-04-18 15:58:19 -04:00
parent 2aaa330c9a
commit db377852fc
7 changed files with 276 additions and 71 deletions

View File

@@ -1,6 +1,9 @@
use tauri::AppHandle;
use crate::domain::models::{AppSettings, EssentialsRepo, LogPayload, SyncEssentialsResult};
use crate::domain::models::{
AppSettings, DashboardSnapshot, EssentialsRepo, EssentialsStatusItem, LogPayload, SyncEssentialsResult,
UpdateCandidate,
};
use crate::services::{essentials_service, settings_service, software_state_service};
use crate::winget::Software;
@@ -39,6 +42,21 @@ pub async fn get_updates(app: AppHandle) -> Vec<Software> {
software_state_service::get_updates(app).await
}
#[tauri::command]
pub async fn get_dashboard_snapshot(app: AppHandle) -> DashboardSnapshot {
software_state_service::get_dashboard_snapshot(app).await
}
#[tauri::command]
pub async fn get_essentials_status(app: AppHandle) -> (String, Vec<EssentialsStatusItem>) {
software_state_service::get_essentials_status(app).await
}
#[tauri::command]
pub async fn get_update_candidates(app: AppHandle) -> Vec<UpdateCandidate> {
software_state_service::get_update_candidates(app).await
}
#[tauri::command]
pub async fn get_software_info(app: AppHandle, id: String) -> Option<Software> {
software_state_service::get_software_info(app, id).await

View File

@@ -63,3 +63,44 @@ pub struct ResolvedPostInstall {
pub software: Software,
pub steps: Vec<PostInstallStep>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct EssentialsStatusItem {
pub id: String,
pub name: String,
pub description: Option<String>,
pub version: Option<String>,
pub recommended_version: Option<String>,
pub available_version: Option<String>,
pub icon_url: Option<String>,
pub use_manifest: bool,
pub manifest_url: Option<String>,
pub post_install: Option<Vec<PostInstallStep>>,
pub post_install_url: Option<String>,
pub action_label: String,
pub target_version: Option<String>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct UpdateCandidate {
pub id: String,
pub name: String,
pub description: Option<String>,
pub version: Option<String>,
pub available_version: Option<String>,
pub icon_url: Option<String>,
pub use_manifest: bool,
pub manifest_url: Option<String>,
pub post_install: Option<Vec<PostInstallStep>>,
pub post_install_url: Option<String>,
pub action_label: String,
pub target_version: Option<String>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct DashboardSnapshot {
pub essentials_version: String,
pub essentials: Vec<EssentialsStatusItem>,
pub updates: Vec<UpdateCandidate>,
pub installed_software: Vec<Software>,
}

View File

@@ -24,6 +24,9 @@ pub fn run() {
commands::app_commands::get_essentials,
commands::app_commands::get_installed_software,
commands::app_commands::get_updates,
commands::app_commands::get_dashboard_snapshot,
commands::app_commands::get_essentials_status,
commands::app_commands::get_update_candidates,
commands::app_commands::get_software_icon,
commands::app_commands::get_software_info,
tasks::install_queue::install_software,

View File

@@ -1,4 +1,5 @@
pub mod essentials_service;
pub mod log_service;
pub mod reconcile_service;
pub mod settings_service;
pub mod software_state_service;

View File

@@ -0,0 +1,158 @@
use std::collections::HashMap;
use crate::domain::models::{DashboardSnapshot, EssentialsRepo, EssentialsStatusItem, UpdateCandidate};
use crate::winget::Software;
pub fn build_dashboard_snapshot(
repo: Option<EssentialsRepo>,
installed_software: Vec<Software>,
updates: Vec<Software>,
) -> DashboardSnapshot {
let essentials_version = repo
.as_ref()
.map(|item| item.version.clone())
.unwrap_or_default();
let definitions = repo.map(|item| item.essentials).unwrap_or_default();
let essentials = build_essentials_status(&definitions, &installed_software, &updates);
let update_candidates = build_update_candidates(&definitions, updates);
DashboardSnapshot {
essentials_version,
essentials,
updates: update_candidates,
installed_software,
}
}
pub fn build_essentials_status(
definitions: &[Software],
installed_software: &[Software],
updates: &[Software],
) -> Vec<EssentialsStatusItem> {
definitions
.iter()
.map(|definition| {
let installed = installed_software
.iter()
.find(|item| item.id.eq_ignore_ascii_case(&definition.id));
let update = updates
.iter()
.find(|item| item.id.eq_ignore_ascii_case(&definition.id));
let current_version = installed.and_then(|item| item.version.clone());
let recommended_version = definition.version.clone();
let available_version = update.and_then(|item| item.available_version.clone());
let (action_label, target_version) = if installed.is_some() {
match compare_versions(current_version.as_deref(), recommended_version.as_deref()) {
Some(std::cmp::Ordering::Less) => (
"更新".to_string(),
recommended_version.clone().or(available_version.clone()),
),
Some(_) => ("已安装".to_string(), None),
None => ("已安装".to_string(), None),
}
} else {
(
"安装".to_string(),
recommended_version.clone().or(available_version.clone()),
)
};
EssentialsStatusItem {
id: definition.id.clone(),
name: definition.name.clone(),
description: definition.description.clone(),
version: current_version,
recommended_version,
available_version,
icon_url: definition.icon_url.clone(),
use_manifest: definition.use_manifest,
manifest_url: definition.manifest_url.clone(),
post_install: definition.post_install.clone(),
post_install_url: definition.post_install_url.clone(),
action_label,
target_version,
}
})
.collect()
}
pub fn build_update_candidates(
definitions: &[Software],
updates: Vec<Software>,
) -> Vec<UpdateCandidate> {
let definition_map: HashMap<String, &Software> = definitions
.iter()
.map(|item| (item.id.to_ascii_lowercase(), item))
.collect();
let mut result: Vec<UpdateCandidate> = updates
.into_iter()
.map(|update| {
let definition = definition_map.get(&update.id.to_ascii_lowercase()).copied();
UpdateCandidate {
id: update.id.clone(),
name: update.name.clone(),
description: definition.and_then(|item| item.description.clone()),
version: update.version.clone(),
available_version: update.available_version.clone(),
icon_url: update.icon_url.clone().or_else(|| definition.and_then(|item| item.icon_url.clone())),
use_manifest: definition.map(|item| item.use_manifest).unwrap_or(false),
manifest_url: definition.and_then(|item| item.manifest_url.clone()),
post_install: definition.and_then(|item| item.post_install.clone()),
post_install_url: definition.and_then(|item| item.post_install_url.clone()),
action_label: "更新".to_string(),
target_version: update.available_version.clone(),
}
})
.collect();
result.sort_by(|left, right| left.name.locale_compare(&right.name));
result
}
trait LocaleCompare {
fn locale_compare(&self, other: &str) -> std::cmp::Ordering;
}
impl LocaleCompare for String {
fn locale_compare(&self, other: &str) -> std::cmp::Ordering {
self.to_lowercase().cmp(&other.to_lowercase())
}
}
fn compare_versions(left: Option<&str>, right: Option<&str>) -> Option<std::cmp::Ordering> {
let left = left?;
let right = right?;
if left == right {
return Some(std::cmp::Ordering::Equal);
}
let clean = |value: &str| {
value
.trim_start_matches('v')
.trim_start_matches('V')
.split(['-', '+'])
.next()
.unwrap_or(value)
.split('.')
.map(|item| item.parse::<u32>().unwrap_or(0))
.collect::<Vec<_>>()
};
let left_parts = clean(left);
let right_parts = clean(right);
let max_len = left_parts.len().max(right_parts.len());
for index in 0..max_len {
let left_value = *left_parts.get(index).unwrap_or(&0);
let right_value = *right_parts.get(index).unwrap_or(&0);
match left_value.cmp(&right_value) {
std::cmp::Ordering::Equal => continue,
ordering => return Some(ordering),
}
}
Some(std::cmp::Ordering::Equal)
}

View File

@@ -1,6 +1,8 @@
use tauri::AppHandle;
use crate::domain::models::{DashboardSnapshot, EssentialsStatusItem, UpdateCandidate};
use crate::providers::winget_client;
use crate::services::{essentials_service, reconcile_service};
use crate::winget::Software;
pub async fn initialize_app(app: AppHandle) -> Result<bool, String> {
@@ -33,3 +35,28 @@ pub async fn get_software_icon(app: AppHandle, id: String, name: String) -> Opti
.await
.unwrap_or(None)
}
pub async fn get_dashboard_snapshot(app: AppHandle) -> DashboardSnapshot {
let repo = essentials_service::get_essentials(&app);
let app_for_installed = app.clone();
let app_for_updates = app.clone();
let installed_handle =
tokio::task::spawn_blocking(move || winget_client::list_installed_packages(&app_for_installed));
let updates_handle =
tokio::task::spawn_blocking(move || winget_client::list_upgrade_candidates(&app_for_updates));
let installed_software = installed_handle.await.unwrap_or_default();
let updates = updates_handle.await.unwrap_or_default();
reconcile_service::build_dashboard_snapshot(repo, installed_software, updates)
}
pub async fn get_essentials_status(app: AppHandle) -> (String, Vec<EssentialsStatusItem>) {
let snapshot = get_dashboard_snapshot(app).await;
(snapshot.essentials_version, snapshot.essentials)
}
pub async fn get_update_candidates(app: AppHandle) -> Vec<UpdateCandidate> {
get_dashboard_snapshot(app).await.updates
}