From 1191cdd95ce6baa41b704d024898426f4bbd254a Mon Sep 17 00:00:00 2001 From: akanyan Date: Tue, 18 Mar 2025 23:27:17 +0000 Subject: [PATCH] feat: misc improvements --- rust/src/appdata.rs | 36 ++++++++++++--- rust/src/cmd.rs | 35 ++++++++++++--- rust/src/lib.rs | 70 +++++++++++++++++++++++++---- rust/src/model/profile.rs | 10 +++-- rust/src/modules/network.rs | 2 +- rust/src/modules/segatools.rs | 30 ++++++++++++- rust/src/pkg.rs | 7 +-- rust/src/pkg_store.rs | 6 +-- rust/src/profiles/mod.rs | 22 ++++++++- src/components/ModTitlecard.vue | 11 ++--- src/components/OptionList.vue | 79 ++++++++++++++++++++++++--------- src/components/OptionRow.vue | 11 ++++- src/components/StartButton.vue | 1 + src/stores.ts | 7 ++- src/types.ts | 5 ++- 15 files changed, 264 insertions(+), 68 deletions(-) diff --git a/rust/src/appdata.rs b/rust/src/appdata.rs index 611ea8f..b2c4c37 100644 --- a/rust/src/appdata.rs +++ b/rust/src/appdata.rs @@ -1,5 +1,6 @@ use std::hash::{DefaultHasher, Hash, Hasher}; use crate::model::config::GlobalConfig; +use crate::pkg::{Feature, Status}; use crate::profiles::AnyProfile; use crate::{model::misc::Game, pkg::PkgKey}; use crate::pkg_store::PackageStore; @@ -18,6 +19,13 @@ pub struct AppData { pub state: GlobalState, } +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum ToggleAction { + Disable, + EnableSelf, + EnableRecursive, +} + impl AppData { pub fn new(apph: AppHandle) -> AppData { let cfg = std::fs::read_to_string(util::config_dir().join("config.json")) @@ -59,26 +67,34 @@ impl AppData { } } - pub fn toggle_package(&mut self, key: PkgKey, enable: bool) -> Result<()> { - log::debug!("toggle: {} {}", key, enable); + pub fn toggle_package(&mut self, key: PkgKey, action: ToggleAction) -> Result<()> { + log::debug!("toggle: {} {:?}", key, action); let profile = self.profile.as_mut().ok_or_else(|| anyhow!("No profile"))?; - if enable { + if action != ToggleAction::Disable { let pkg = self.pkgs.get(&key)?; let loc = pkg.loc .clone() .ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?; - profile.mod_pkgs_mut().insert(key); - for d in &loc.dependencies { - _ = self.toggle_package(d.clone(), true); + + if let Status::OK(feature_set) = loc.status { + log::debug!("{:?}", feature_set); + if feature_set.contains(Feature::Mod) { + profile.mod_pkgs_mut().insert(key); + } + } + if action == ToggleAction::EnableRecursive { + for d in &loc.dependencies { + _ = self.toggle_package(d.clone(), action); + } } } else { profile.mod_pkgs_mut().remove(&key); for (ckey, pkg) in self.pkgs.get_all() { if let Some(loc) = pkg.loc { if loc.dependencies.contains(&key) { - self.toggle_package(ckey, false)?; + self.toggle_package(ckey, action)?; } } } @@ -99,4 +115,10 @@ impl AppData { } hasher.finish().to_string() } + + pub fn fix(&mut self) { + if let Some(p) = &mut self.profile { + p.fix(&self.pkgs); + } + } } diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index 5c80627..aff1f8d 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -8,7 +8,7 @@ use crate::pkg::{Package, PkgKey}; use crate::pkg_store::{InstallResult, PackageStore}; use crate::profiles::ongeki::OngekiProfile; use crate::profiles::{self, AnyProfile, Profile, ProfileMeta, ProfilePaths}; -use crate::appdata::AppData; +use crate::appdata::{AppData, ToggleAction}; use crate::model::misc::StartCheckError; use crate::util; @@ -103,6 +103,9 @@ pub async fn delete_package(state: State<'_, tokio::sync::Mutex>, key: let mut appd = state.lock().await; appd.pkgs.delete_package(&key, true) .await + .map_err(|e| e.to_string())?; + + appd.toggle_package(key, ToggleAction::Disable) .map_err(|e| e.to_string()) } @@ -121,7 +124,7 @@ pub async fn toggle_package(state: State<'_, tokio::sync::Mutex>, key: log::debug!("invoke: toggle_package({}, {})", key, enable); let mut appd = state.lock().await; - appd.toggle_package(key, enable) + appd.toggle_package(key, if enable { ToggleAction::EnableRecursive } else { ToggleAction::Disable }) .map_err(|e| e.to_string()) } @@ -205,6 +208,7 @@ pub async fn load_profile(state: State<'_, Mutex>, game: Game, name: St let mut appd = state.lock().await; appd.switch_profile(game, name).map_err(|e| e.to_string())?; + appd.fix(); Ok(()) } @@ -290,14 +294,31 @@ pub async fn get_current_profile(state: State<'_, Mutex>) -> Result>, profile: AnyProfile) -> Result<(), String> { +pub async fn sync_current_profile(state: State<'_, Mutex>, profile: AnyProfile) -> Result<(), String> { + log::debug!("invoke: sync_current_profile"); + + let mut appd = state.lock().await; + if let Some(p) = &mut appd.profile { + p.sync(profile); + } + + Ok(()) +} + +#[tauri::command] +pub async fn save_current_profile(state: State<'_, Mutex>) -> Result<(), String> { log::debug!("invoke: save_current_profile"); let mut appd = state.lock().await; - profile.save().map_err(|e| e.to_string())?; - appd.profile = Some(profile); - - Ok(()) + appd.fix(); + match &mut appd.profile { + Some(p) => { + p.save().map_err(|e| e.to_string()) + }, + None => { + Err("no profile to save".to_owned()) + } + } } #[tauri::command] diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 4ff29fa..0ed7893 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -8,15 +8,19 @@ mod appdata; mod modules; mod profiles; +use std::sync::OnceLock; use anyhow::anyhow; use closure::closure; -use appdata::AppData; +use appdata::{AppData, ToggleAction}; use model::misc::Game; use pkg::PkgKey; -use tauri::{AppHandle, Listener, Manager}; +use pkg_store::Payload; +use tauri::{AppHandle, Listener, Manager, RunEvent}; use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_cli::CliExt; -use tokio::{sync::Mutex, fs, try_join}; +use tokio::{fs, sync::Mutex, try_join}; + +static EXIT_REQUESTED: OnceLock<()> = OnceLock::new(); #[cfg_attr(mobile, tauri::mobile_entry_point)] pub async fn run(_args: Vec) { @@ -30,7 +34,7 @@ pub async fn run(_args: Vec) { .unwrap_or_default() ); - tauri::Builder::default() + let tauri = tauri::Builder::default() .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { let _ = app .get_webview_window("main") @@ -65,8 +69,8 @@ pub async fn run(_args: Vec) { } else { tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into())) .title("STARTLINER") - .inner_size(600f64, 500f64) - .min_inner_size(600f64, 500f64) + .inner_size(640f64, 480f64) + .min_inner_size(640f64, 480f64) .build()?; start_immediately = false; } @@ -102,17 +106,19 @@ pub async fn run(_args: Vec) { }); app.listen("download-end", closure!(clone apph, |ev| { + log::debug!("download-end triggered: {}", ev.payload()); let raw = ev.payload(); let key = PkgKey(raw[1..raw.len()-1].to_owned()); let apph = apph.clone(); tauri::async_runtime::spawn(async move { let mutex = apph.state::>(); let mut appd = mutex.lock().await; - _ = appd.pkgs.install_package(&key, true, false).await; + log::debug!("download-end install {:?}", appd.pkgs.install_package(&key, true, false).await); }); })); app.listen("launch-end", closure!(clone apph, |_| { + log::debug!("launch-end triggered"); let apph = apph.clone(); tauri::async_runtime::spawn(async move { let mutex = apph.state::>(); @@ -123,6 +129,26 @@ pub async fn run(_args: Vec) { }); })); + app.listen("install-end-prelude", closure!(clone apph, |ev| { + log::debug!("install-end-prelude triggered: {}", ev.payload()); + let payload = serde_json::from_str::(ev.payload()); + let apph = apph.clone(); + if let Ok(payload) = payload { + tauri::async_runtime::spawn(async move { + let mutex = apph.state::>(); + let mut appd = mutex.lock().await; + log::debug!( + "install-end-prelude toggle {:?}", + appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf) + ); + use tauri::Emitter; + log::debug!("install-end {:?}", apph.emit("install-end", payload)); + }); + } else { + log::error!("install-end-prelude: invalid payload: {}", ev.payload()); + } + })); + if start_immediately == true { let apph = apph.clone(); tauri::async_runtime::spawn(async move { @@ -163,14 +189,40 @@ pub async fn run(_args: Vec) { cmd::duplicate_profile, cmd::delete_profile, cmd::get_current_profile, + cmd::sync_current_profile, cmd::save_current_profile, cmd::list_displays, cmd::list_platform_capabilities, cmd::list_directories, ]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while building tauri application"); + + tauri.run(move |app, event| { + match event { + RunEvent::ExitRequested { api, code, .. } => { + log::debug!("exit request {:?} {:?}", api, code); + if EXIT_REQUESTED.get().is_none() { + _= EXIT_REQUESTED.set(()); + api.prevent_exit(); + let app = app.clone(); + tauri::async_runtime::spawn(async move { + let mutex = app.state::>(); + let appd = mutex.lock().await; + if let Some(p) = &appd.profile { + log::debug!("save: {:?}", p.save()); + app.exit(0); + } + }); + } + }, + RunEvent::Exit => { + log::info!("exit"); + } + _ => {} + } + }); } fn deep_link(app: AppHandle, args: Vec) { diff --git a/rust/src/model/profile.rs b/rust/src/model/profile.rs index aa77f87..84b5802 100644 --- a/rust/src/model/profile.rs +++ b/rust/src/model/profile.rs @@ -2,9 +2,10 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use crate::pkg::PkgKey; -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Default, PartialEq)] pub enum Aime { - BuiltIn, + Disabled, + #[default] BuiltIn, AMNet(PkgKey), Other(PkgKey), } @@ -27,7 +28,8 @@ pub struct Segatools { pub target: PathBuf, pub hook: Option, pub io: Option, - pub aime: Option, + #[serde(default)] + pub aime: Aime, pub amfs: PathBuf, pub option: PathBuf, pub appdata: PathBuf, @@ -45,7 +47,7 @@ impl Default for Segatools { amfs: PathBuf::default(), option: PathBuf::default(), appdata: PathBuf::from("appdata"), - aime: Some(Aime::BuiltIn), + aime: Aime::default(), intel: false, amnet: AMNet::default(), } diff --git a/rust/src/modules/network.rs b/rust/src/modules/network.rs index 7540af8..41ba16f 100644 --- a/rust/src/modules/network.rs +++ b/rust/src/modules/network.rs @@ -60,7 +60,7 @@ impl Network { cmd.arg(&self.local_path); cmd.current_dir(artemis_dir); cmd.spawn() - .map_err(|e| anyhow!("Unable to spawn artemis: {}", e))?; + .map_err(|e| anyhow!("unable to spawn artemis: {}", e))?; } else { log::warn!("unable to parse the artemis hostname"); } diff --git a/rust/src/modules/segatools.rs b/rust/src/modules/segatools.rs index d86178f..72027ed 100644 --- a/rust/src/modules/segatools.rs +++ b/rust/src/modules/segatools.rs @@ -3,8 +3,34 @@ use std::path::PathBuf; use anyhow::{anyhow, Result}; use ini::Ini; use crate::{model::{profile::{Aime, Segatools}, segatools_base::segatools_base}, profiles::ProfilePaths, util::{self, PathStr}}; +use crate::pkg_store::PackageStore; impl Segatools { + pub fn fix(&mut self, store: &PackageStore) { + macro_rules! remove_if_nonpresent { + ($item:expr,$key:expr,$emptyval:expr,$store:expr) => { + if let Ok(pkg) = $store.get($key) { + if pkg.loc.is_none() { + $item = $emptyval; + } + } else { + $item = $emptyval; + } + } + } + + if let Some(key) = &self.hook { + remove_if_nonpresent!(self.hook, key, None, store); + } + if let Some(key) = &self.io { + remove_if_nonpresent!(self.io, key, None, store); + } + match &self.aime { + Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store), + Aime::Other(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store), + _ => {}, + } + } pub async fn line_up(&self, p: &impl ProfilePaths) -> Result { log::debug!("begin line-up: segatools"); @@ -50,11 +76,11 @@ impl Segatools { pfx_dir.join("BepInEx").join("core").join("BepInEx.Preloader.dll").stringify()? ); - if let Some(aime) = &self.aime { + if self.aime != Aime::Disabled { ini_out.with_section(Some("aime")) .set("enable", "1") .set("aimePath", p.config_dir().join("aime.txt").stringify()?); - if let Aime::AMNet(key) = aime { + if let Aime::AMNet(key) = &self.aime { let mut aimeio = ini_out.with_section(Some("aimeio")); aimeio .set("path", util::pkg_dir().join(key.to_string()).join("segatools").join("aimeio.dll").stringify()?) diff --git a/rust/src/pkg.rs b/rust/src/pkg.rs index 69235ae..571de35 100644 --- a/rust/src/pkg.rs +++ b/rust/src/pkg.rs @@ -20,7 +20,6 @@ pub struct Package { pub namespace: String, pub name: String, pub description: String, - pub icon: String, pub loc: Option, pub rmt: Option } @@ -50,6 +49,7 @@ pub struct Local { pub path: PathBuf, pub dependencies: BTreeSet, pub status: Status, + pub icon: String, } #[derive(Clone, Serialize, Deserialize)] @@ -58,6 +58,7 @@ pub struct Remote { pub version: String, pub package_url: String, pub download_url: String, + pub icon: String, pub deprecated: bool, pub nsfw: bool, pub categories: Vec, @@ -76,11 +77,11 @@ impl Package { namespace: p.owner, name: v.name, description: v.description, - icon: v.icon, loc: None, rmt: Some(Remote { package_url: p.package_url, download_url: v.download_url, + icon: v.icon, deprecated: p.is_deprecated, nsfw: p.has_nsfw_content, version: v.version_number, @@ -107,10 +108,10 @@ impl Package { namespace: Self::dir_to_namespace(&dir)?, name: mft.name.clone(), description: mft.description.clone(), - icon, loc: Some(Local { version: mft.version_number, path: dir.to_owned(), + icon, status, dependencies }), diff --git a/rust/src/pkg_store.rs b/rust/src/pkg_store.rs index 56de818..d2bbb61 100644 --- a/rust/src/pkg_store.rs +++ b/rust/src/pkg_store.rs @@ -22,7 +22,7 @@ pub struct Payload { pub pkg: PkgKey } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug)] pub enum InstallResult { Ready, Deferred } @@ -167,7 +167,7 @@ impl PackageStore { archive.extract(path)?; self.reload_package(key.to_owned()).await; - self.app.emit("install-end", Payload { + self.app.emit("install-end-prelude", Payload { pkg: key.to_owned() })?; @@ -189,7 +189,7 @@ impl PackageStore { let rv = Self::clean_up_package(&path).await; if rv.is_ok() { - self.app.emit("install-end", Payload { + self.app.emit("install-end-prelude", Payload { pkg: key.to_owned() })?; log::info!("deleted {}", key); diff --git a/rust/src/profiles/mod.rs b/rust/src/profiles/mod.rs index aa39450..07bb386 100644 --- a/rust/src/profiles/mod.rs +++ b/rust/src/profiles/mod.rs @@ -3,7 +3,7 @@ use ongeki::OngekiProfile; use serde::{Deserialize, Serialize}; use tauri::AppHandle; use std::{collections::BTreeSet, path::{Path, PathBuf}}; -use crate::{model::misc::Game, modules::package::prepare_packages, pkg::PkgKey, util}; +use crate::{model::misc::Game, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util}; pub mod ongeki; @@ -83,6 +83,26 @@ impl AnyProfile { } res } + pub fn fix(&mut self, store: &PackageStore) { + match self { + Self::OngekiProfile(p) => p.sgt.fix(store) + } + } + pub fn sync(&mut self, source: AnyProfile) { + match self { + Self::OngekiProfile(p) => { + #[allow(irrefutable_let_patterns)] + if let AnyProfile::OngekiProfile(source) = source { + p.bepinex = source.bepinex; + p.display = source.display; + p.network = source.network; + p.sgt = source.sgt; + } else { + log::error!("sync: invalid profile type {:?}", source); + } + } + } + } pub async fn line_up(&self, pkg_hash: String, _app: AppHandle) -> Result<()> { match self { Self::OngekiProfile(_p) => { diff --git a/src/components/ModTitlecard.vue b/src/components/ModTitlecard.vue index c950678..966cf3d 100644 --- a/src/components/ModTitlecard.vue +++ b/src/components/ModTitlecard.vue @@ -1,4 +1,5 @@