From 6236b8ef96aaa49a7b15c9c3dc5bf5ae935bf239 Mon Sep 17 00:00:00 2001 From: akanyan Date: Sun, 23 Feb 2025 20:54:47 +0100 Subject: [PATCH] feat: less bad installations --- README.md | 15 +++++++- rust/src/appdata.rs | 7 ++++ rust/src/cmd.rs | 19 +++------- rust/src/download_handler.rs | 1 + rust/src/lib.rs | 4 +- rust/src/pkg_store.rs | 63 +++++++++++++++++++++++++++----- rust/src/profile.rs | 2 +- rust/src/start.rs | 13 ++++--- rust/tauri.conf.json | 2 +- src/components/InstallButton.vue | 13 +++++-- src/components/ModListEntry.vue | 3 +- src/stores.ts | 37 +++++++++---------- src/types.ts | 3 ++ 13 files changed, 124 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 6f83db4..daf8108 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,23 @@ ## STARTLINER -A simple and easy to use mod manager for [many games](https://silentblue.remywiki.com/ONGEKI:bright_MEMORY) using [Rainycolor Watercolor](https://rainy.patafour.zip). +wipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwipwip + +A simple (_not yet_) and easy to use (_not yet_) mod manager for [many games](https://silentblue.remywiki.com/ONGEKI:bright_MEMORY) (_more to come_) using [Rainycolor Watercolor](https://rainy.patafour.zip). Intended for those who just want a glorified `start.bat` clicker, without VHDs, keychips etc. (for an all-in-one solution, check out the [BlueSteel launcher](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)). Made with Rust (Tauri) and Vue. Contributions welcome. +### Usage + +wipwipwipwipwipwipwipwipwipwipwipwip + +```sh +bun install +bun run tauri dev +``` + ### Package format - [Package format requirements](https://rainy.patafour.zip/package/create/docs/) @@ -31,7 +42,7 @@ Arbitrary scripts are not supported by design and that will probably never chang ### Features - Clean data modding -- Multi-platform +- Technically multi-platform ### Todo diff --git a/rust/src/appdata.rs b/rust/src/appdata.rs index 9cd34b3..a66ac05 100644 --- a/rust/src/appdata.rs +++ b/rust/src/appdata.rs @@ -26,6 +26,13 @@ impl AppData { } } else { profile.mods.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)?; + } + } + } } Ok(()) diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index c2f2df7..4ab8f4f 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -29,24 +29,17 @@ pub async fn startline(state: State<'_, Mutex>) -> Result<(), String> { #[tauri::command] pub async fn install_package(state: State<'_, tokio::sync::Mutex>, key: PkgKey) -> Result { - log::debug!("invoke: install_package"); + log::debug!("invoke: install_package({})", key); let mut appd = state.lock().await; - let rv = appd.pkgs.install_package(&key, true) + appd.pkgs.install_package(&key, true, true) .await - .map_err(|e| e.to_string()); - - - // if rv.is_ok() { - // _ = appd.toggle_package(key, true); - // } - - rv + .map_err(|e| e.to_string()) } #[tauri::command] pub async fn delete_package(state: State<'_, tokio::sync::Mutex>, key: PkgKey) -> Result<(), String> { - log::debug!("invoke: delete_package"); + log::debug!("invoke: delete_package({})", key); let mut appd = state.lock().await; appd.pkgs.delete_package(&key, true) @@ -56,7 +49,7 @@ pub async fn delete_package(state: State<'_, tokio::sync::Mutex>, key: #[tauri::command] pub async fn get_package(state: State<'_, tokio::sync::Mutex>, key: PkgKey) -> Result { - log::debug!("invoke: get_package"); + log::debug!("invoke: get_package({})", key); let appd = state.lock().await; appd.pkgs.get(key) @@ -66,7 +59,7 @@ pub async fn get_package(state: State<'_, tokio::sync::Mutex>, key: Pkg #[tauri::command] pub async fn toggle_package(state: State<'_, tokio::sync::Mutex>, key: PkgKey, enable: bool) -> Result<(), String> { - log::debug!("invoke: toggle_package"); + log::debug!("invoke: toggle_package({}, {})", key, enable); let mut appd = state.lock().await; appd.toggle_package(key, enable) diff --git a/rust/src/download_handler.rs b/rust/src/download_handler.rs index a730e75..a6d5b94 100644 --- a/rust/src/download_handler.rs +++ b/rust/src/download_handler.rs @@ -23,6 +23,7 @@ impl DownloadHandler { .ok_or_else(|| anyhow!("Attempted to download a package without remote data"))? .clone(); if self.set.contains(zip_path.to_string_lossy().as_ref()) { + // Todo when there is a clear cache button, it should clear the set Err(anyhow!("Already downloading")) } else { self.set.insert(zip_path.to_string_lossy().to_string()); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 1c1e55a..933e205 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -58,7 +58,7 @@ pub async fn run(_args: Vec) { let mutex = apph.state::>(); let mut appd = mutex.lock().await; _ = appd.pkgs.fetch_listings().await; - if let Err(e) = appd.pkgs.install_package(&key, true).await { + if let Err(e) = appd.pkgs.install_package(&key, true, true).await { log::warn!("Fail: {}", e.to_string()); } }); @@ -89,7 +89,7 @@ pub async fn run(_args: Vec) { tauri::async_runtime::spawn(async move { let mutex = apph.state::>(); let mut appd = mutex.lock().await; - _ = appd.pkgs.install_package(&key, true).await; + _ = appd.pkgs.install_package(&key, true, false).await; }); })); diff --git a/rust/src/pkg_store.rs b/rust/src/pkg_store.rs index c6909e0..5bd3fe2 100644 --- a/rust/src/pkg_store.rs +++ b/rust/src/pkg_store.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::Path; use anyhow::{Result, anyhow}; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter}; @@ -118,7 +119,7 @@ impl PackageStore { Ok(()) } - pub async fn install_package(&mut self, key: &PkgKey, force: bool) -> Result { + pub async fn install_package(&mut self, key: &PkgKey, force: bool, install_deps: bool) -> Result { log::debug!("Installing {}", key); let pkg = self.store.get(key) @@ -128,14 +129,18 @@ impl PackageStore { if pkg.loc.is_some() && !force { return Ok(InstallResult::Ready); } + + self.app.emit("install-start", Payload { + pkg: key.to_owned() + })?; + let rmt = pkg.rmt.as_ref() //clone() .ok_or_else(|| anyhow!("Attempted to install a pkg without remote data"))?; - for dep in &rmt.dependencies { - self.app.emit("install-start", Payload { - pkg: dep.to_owned() - })?; - Box::pin(self.install_package(&dep, false)).await?; + if install_deps { + for dep in &rmt.dependencies { + Box::pin(self.install_package(&dep, false, true)).await?; + } } let zip_path = util::cache_dir().join(format!( @@ -145,6 +150,7 @@ impl PackageStore { if !zip_path.exists() { self.dlh.download_zip(&zip_path, &pkg)?; + log::debug!("Deferring {}", key); return Ok(InstallResult::Deferred); } @@ -168,16 +174,16 @@ impl PackageStore { } pub async fn delete_package(&mut self, key: &PkgKey, force: bool) -> Result<()> { + log::debug!("Will delete {} {}", key, force); + let pkg = self.store.get_mut(key) .ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?; let path = pkg.path(); if path.exists() && path.join("manifest.json").exists() { - // TODO don't rm -r - use a file whitelist - log::debug!("rm -r'ing {}", path.to_string_lossy()); pkg.loc = None; - let rv = tokio::fs::remove_dir_all(&path).await - .map_err(|e| anyhow!("Could not delete a package: {}", e)); + + let rv = Self::clean_up_package(&path).await; if rv.is_ok() { self.app.emit("install-end", Payload { @@ -201,4 +207,41 @@ impl PackageStore { } self.store.insert(key, new); } + + async fn clean_up_dir(path: impl AsRef, name: &str) -> Result<()> { + let path = path.as_ref().join(name); + if path.exists() { + tokio::fs::remove_dir_all(path) + .await + .map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?; + } + + Ok(()) + } + + async fn clean_up_file(path: impl AsRef, name: &str, force: bool) -> Result<()> { + let path = path.as_ref().join(name); + if force || path.exists() { + tokio::fs::remove_file(path) + .await + .map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?; + } + + Ok(()) + } + + async fn clean_up_package(path: impl AsRef) -> Result<()> { + // todo case sensitivity for linux + Self::clean_up_dir(&path, "app").await?; + Self::clean_up_dir(&path, "option").await?; + Self::clean_up_file(&path, "icon.png", true).await?; + Self::clean_up_file(&path, "manifest.json", true).await?; + Self::clean_up_file(&path, "README.md", true).await?; + + tokio::fs::remove_dir(path.as_ref()) + .await + .map_err(|e| anyhow!("Could not delete {}: {}", path.as_ref().to_string_lossy(), e))?; + + Ok(()) + } } diff --git a/rust/src/profile.rs b/rust/src/profile.rs index 359afa2..6cc7eea 100644 --- a/rust/src/profile.rs +++ b/rust/src/profile.rs @@ -26,7 +26,7 @@ impl Profile { mods: HashSet::new(), #[cfg(target_os = "linux")] - wine_runtime: Some(Path::new("/usr/bin/wine").to_path_buf()), + wine_runtime: Some(std::path::Path::new("/usr/bin/wine").to_path_buf()), #[cfg(target_os = "windows")] wine_runtime: None, diff --git a/rust/src/start.rs b/rust/src/start.rs index b7626a4..eb8743c 100644 --- a/rust/src/start.rs +++ b/rust/src/start.rs @@ -1,24 +1,27 @@ use anyhow::Result; -use std::process::Stdio; -use tokio::task::JoinSet; use tokio::process::Command; use crate::profile::Profile; use crate::util; #[cfg(target_os = "linux")] -pub fn start(p: &Profile) -> Result { - Ok(Command::new(p.wine_runtime.as_ref().unwrap()) +pub fn start(p: &Profile) -> Result<()> { + Command::new(p.wine_runtime.as_ref().unwrap()) .env( "SEGATOOLS_CONFIG_PATH", util::profile_dir(&p).join("segatools.ini"), ) .env("WINEPREFIX", p.wine_prefix.as_ref().unwrap()) .arg(p.exe_dir.join("start.bat")) - .spawn()?) + .spawn()?; + + Ok(()) } #[cfg(target_os = "windows")] pub fn start(p: &Profile) -> Result<()> { + use std::process::Stdio; + use tokio::task::JoinSet; + let ini_path = util::profile_dir(&p).join("segatools.ini"); log::debug!("With path {}", ini_path.to_string_lossy()); diff --git a/rust/tauri.conf.json b/rust/tauri.conf.json index 37db1df..b285737 100644 --- a/rust/tauri.conf.json +++ b/rust/tauri.conf.json @@ -35,7 +35,7 @@ }, "assetProtocol": { "enable": true, - "scope": ["**", "**/*", "**/.*/**/*", "**\\*", "**\\.*\\**\\*"] + "scope": ["**", "**/*", "**/.*/**/*"] } } }, diff --git a/src/components/InstallButton.vue b/src/components/InstallButton.vue index 6ba6b0d..72d5259 100644 --- a/src/components/InstallButton.vue +++ b/src/components/InstallButton.vue @@ -13,7 +13,14 @@ const install = async () => { return; } - await invoke('install_package', { key: pkgKey(props.pkg) }); + try { + await invoke('install_package', { key: pkgKey(props.pkg) }); + } catch (err) { + console.error(err); + if (props.pkg !== undefined) { + props.pkg.js.busy = false; + } + } //if (rv === 'Deferred') { /* download progress */ } }; @@ -39,7 +46,7 @@ const remove = async () => { size="small" class="self-center ml-4" style="width: 2rem; height: 2rem" - :loading="false" + :loading="pkg?.js.busy" v-on:click="remove()" /> @@ -52,7 +59,7 @@ const remove = async () => { size="small" class="self-center ml-4" style="width: 2rem; height: 2rem" - :loading="false" + :loading="pkg?.js.busy" v-on:click="install()" /> diff --git a/src/components/ModListEntry.vue b/src/components/ModListEntry.vue index a899379..ffec695 100644 --- a/src/components/ModListEntry.vue +++ b/src/components/ModListEntry.vue @@ -13,8 +13,9 @@ const props = defineProps({ pkg: Object as () => Package, }); -const toggle = (value: boolean) => { +const toggle = async (value: boolean) => { store.toggle(props.pkg, value); + await store.reloadProfile(); }; diff --git a/src/stores.ts b/src/stores.ts index 18983e0..82a4561 100644 --- a/src/stores.ts +++ b/src/stores.ts @@ -29,12 +29,18 @@ export const usePkgStore = defineStore('pkg', { actions: { setupListeners() { listen('install-start', async (ev) => { - await this.reload(ev.payload.pkg); + const key = ev.payload.pkg; + await this.reload(key); + this.pkg[key].js.busy = true; + console.log('install-start' + key); }); listen('install-end', async (ev) => { - await this.reload(ev.payload.pkg); + const key = ev.payload.pkg; + await this.reload(key); await this.reloadProfile(); + this.pkg[key].js.busy = false; + console.log('install-end' + key); }); }, @@ -44,37 +50,28 @@ export const usePkgStore = defineStore('pkg', { [key: string]: Package; }; - for (const k of Object.keys(this)) { - delete this.pkg[k]; + for (const [k, v] of Object.entries(data)) { + this.reloadWith(k, v); } - - Object.assign(this.pkg, data); - - if (this.prf !== null) - for (const [k, v] of Object.entries(this)) { - if (this.prf.mods.includes(k)) { - if (v.loc) { - v.loc.enabled = true; - } else { - console.error(`${k} enabled but not present`); - } - } - } }, async reload(pkgOrKey: string | Package) { const key = typeof pkgOrKey === 'string' ? pkgOrKey : pkgKey(pkgOrKey); - const rv: Package = await invoke('get_package', { + const pkg: Package = await invoke('get_package', { key, }); + this.reloadWith(key, pkg); + }, + + async reloadWith(key: string, pkg: Package) { if (this.pkg[key] === undefined) { - this.pkg[key] = {} as Package; + this.pkg[key] = { js: { busy: false } } as Package; } else { this.pkg[key].loc = null; this.pkg[key].rmt = null; } - Object.assign(this.pkg[key], rv); + Object.assign(this.pkg[key], pkg); }, async initProfile(exePath: string) { diff --git a/src/types.ts b/src/types.ts index 01cf1fa..7f8fb09 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,9 @@ export interface Package { download_url: string; deprecated: boolean; } | null; + js: { + busy: boolean; + }; } export interface Profile {