feat: less bad installations

This commit is contained in:
2025-02-23 20:54:47 +01:00
parent caead1e70f
commit 6236b8ef96
13 changed files with 124 additions and 58 deletions

View File

@ -1,12 +1,23 @@
## STARTLINER ## 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. 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)). (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. Made with Rust (Tauri) and Vue. Contributions welcome.
### Usage
wipwipwipwipwipwipwipwipwipwipwipwip
```sh
bun install
bun run tauri dev
```
### Package format ### Package format
- [Package format requirements](https://rainy.patafour.zip/package/create/docs/) - [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 ### Features
- Clean data modding - Clean data modding
- Multi-platform - Technically multi-platform
### Todo ### Todo

View File

@ -26,6 +26,13 @@ impl AppData {
} }
} else { } else {
profile.mods.remove(&key); 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(()) Ok(())

View File

@ -29,24 +29,17 @@ pub async fn startline(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
#[tauri::command] #[tauri::command]
pub async fn install_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<InstallResult, String> { pub async fn install_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<InstallResult, String> {
log::debug!("invoke: install_package"); log::debug!("invoke: install_package({})", key);
let mut appd = state.lock().await; let mut appd = state.lock().await;
let rv = appd.pkgs.install_package(&key, true) appd.pkgs.install_package(&key, true, true)
.await .await
.map_err(|e| e.to_string()); .map_err(|e| e.to_string())
// if rv.is_ok() {
// _ = appd.toggle_package(key, true);
// }
rv
} }
#[tauri::command] #[tauri::command]
pub async fn delete_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<(), String> { pub async fn delete_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<(), String> {
log::debug!("invoke: delete_package"); log::debug!("invoke: delete_package({})", key);
let mut appd = state.lock().await; let mut appd = state.lock().await;
appd.pkgs.delete_package(&key, true) appd.pkgs.delete_package(&key, true)
@ -56,7 +49,7 @@ pub async fn delete_package(state: State<'_, tokio::sync::Mutex<AppData>>, key:
#[tauri::command] #[tauri::command]
pub async fn get_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<Package, String> { pub async fn get_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey) -> Result<Package, String> {
log::debug!("invoke: get_package"); log::debug!("invoke: get_package({})", key);
let appd = state.lock().await; let appd = state.lock().await;
appd.pkgs.get(key) appd.pkgs.get(key)
@ -66,7 +59,7 @@ pub async fn get_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: Pkg
#[tauri::command] #[tauri::command]
pub async fn toggle_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey, enable: bool) -> Result<(), String> { pub async fn toggle_package(state: State<'_, tokio::sync::Mutex<AppData>>, key: PkgKey, enable: bool) -> Result<(), String> {
log::debug!("invoke: toggle_package"); log::debug!("invoke: toggle_package({}, {})", key, enable);
let mut appd = state.lock().await; let mut appd = state.lock().await;
appd.toggle_package(key, enable) appd.toggle_package(key, enable)

View File

@ -23,6 +23,7 @@ impl DownloadHandler {
.ok_or_else(|| anyhow!("Attempted to download a package without remote data"))? .ok_or_else(|| anyhow!("Attempted to download a package without remote data"))?
.clone(); .clone();
if self.set.contains(zip_path.to_string_lossy().as_ref()) { 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")) Err(anyhow!("Already downloading"))
} else { } else {
self.set.insert(zip_path.to_string_lossy().to_string()); self.set.insert(zip_path.to_string_lossy().to_string());

View File

@ -58,7 +58,7 @@ pub async fn run(_args: Vec<String>) {
let mutex = apph.state::<Mutex<AppData>>(); let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await; let mut appd = mutex.lock().await;
_ = appd.pkgs.fetch_listings().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()); log::warn!("Fail: {}", e.to_string());
} }
}); });
@ -89,7 +89,7 @@ pub async fn run(_args: Vec<String>) {
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>(); let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await; let mut appd = mutex.lock().await;
_ = appd.pkgs.install_package(&key, true).await; _ = appd.pkgs.install_package(&key, true, false).await;
}); });
})); }));

View File

@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
@ -118,7 +119,7 @@ impl PackageStore {
Ok(()) Ok(())
} }
pub async fn install_package(&mut self, key: &PkgKey, force: bool) -> Result<InstallResult> { pub async fn install_package(&mut self, key: &PkgKey, force: bool, install_deps: bool) -> Result<InstallResult> {
log::debug!("Installing {}", key); log::debug!("Installing {}", key);
let pkg = self.store.get(key) let pkg = self.store.get(key)
@ -128,14 +129,18 @@ impl PackageStore {
if pkg.loc.is_some() && !force { if pkg.loc.is_some() && !force {
return Ok(InstallResult::Ready); return Ok(InstallResult::Ready);
} }
self.app.emit("install-start", Payload {
pkg: key.to_owned()
})?;
let rmt = pkg.rmt.as_ref() //clone() let rmt = pkg.rmt.as_ref() //clone()
.ok_or_else(|| anyhow!("Attempted to install a pkg without remote data"))?; .ok_or_else(|| anyhow!("Attempted to install a pkg without remote data"))?;
for dep in &rmt.dependencies { if install_deps {
self.app.emit("install-start", Payload { for dep in &rmt.dependencies {
pkg: dep.to_owned() Box::pin(self.install_package(&dep, false, true)).await?;
})?; }
Box::pin(self.install_package(&dep, false)).await?;
} }
let zip_path = util::cache_dir().join(format!( let zip_path = util::cache_dir().join(format!(
@ -145,6 +150,7 @@ impl PackageStore {
if !zip_path.exists() { if !zip_path.exists() {
self.dlh.download_zip(&zip_path, &pkg)?; self.dlh.download_zip(&zip_path, &pkg)?;
log::debug!("Deferring {}", key);
return Ok(InstallResult::Deferred); return Ok(InstallResult::Deferred);
} }
@ -168,16 +174,16 @@ impl PackageStore {
} }
pub async fn delete_package(&mut self, key: &PkgKey, force: bool) -> Result<()> { 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) let pkg = self.store.get_mut(key)
.ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?; .ok_or_else(|| anyhow!("Attempted to delete a nonexistent pkg"))?;
let path = pkg.path(); let path = pkg.path();
if path.exists() && path.join("manifest.json").exists() { 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; 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() { if rv.is_ok() {
self.app.emit("install-end", Payload { self.app.emit("install-end", Payload {
@ -201,4 +207,41 @@ impl PackageStore {
} }
self.store.insert(key, new); self.store.insert(key, new);
} }
async fn clean_up_dir(path: impl AsRef<Path>, 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<Path>, 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<Path>) -> 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(())
}
} }

View File

@ -26,7 +26,7 @@ impl Profile {
mods: HashSet::new(), mods: HashSet::new(),
#[cfg(target_os = "linux")] #[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")] #[cfg(target_os = "windows")]
wine_runtime: None, wine_runtime: None,

View File

@ -1,24 +1,27 @@
use anyhow::Result; use anyhow::Result;
use std::process::Stdio;
use tokio::task::JoinSet;
use tokio::process::Command; use tokio::process::Command;
use crate::profile::Profile; use crate::profile::Profile;
use crate::util; use crate::util;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn start(p: &Profile) -> Result<Child> { pub fn start(p: &Profile) -> Result<()> {
Ok(Command::new(p.wine_runtime.as_ref().unwrap()) Command::new(p.wine_runtime.as_ref().unwrap())
.env( .env(
"SEGATOOLS_CONFIG_PATH", "SEGATOOLS_CONFIG_PATH",
util::profile_dir(&p).join("segatools.ini"), util::profile_dir(&p).join("segatools.ini"),
) )
.env("WINEPREFIX", p.wine_prefix.as_ref().unwrap()) .env("WINEPREFIX", p.wine_prefix.as_ref().unwrap())
.arg(p.exe_dir.join("start.bat")) .arg(p.exe_dir.join("start.bat"))
.spawn()?) .spawn()?;
Ok(())
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn start(p: &Profile) -> Result<()> { pub fn start(p: &Profile) -> Result<()> {
use std::process::Stdio;
use tokio::task::JoinSet;
let ini_path = util::profile_dir(&p).join("segatools.ini"); let ini_path = util::profile_dir(&p).join("segatools.ini");
log::debug!("With path {}", ini_path.to_string_lossy()); log::debug!("With path {}", ini_path.to_string_lossy());

View File

@ -35,7 +35,7 @@
}, },
"assetProtocol": { "assetProtocol": {
"enable": true, "enable": true,
"scope": ["**", "**/*", "**/.*/**/*", "**\\*", "**\\.*\\**\\*"] "scope": ["**", "**/*", "**/.*/**/*"]
} }
} }
}, },

View File

@ -13,7 +13,14 @@ const install = async () => {
return; 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 */ } //if (rv === 'Deferred') { /* download progress */ }
}; };
@ -39,7 +46,7 @@ const remove = async () => {
size="small" size="small"
class="self-center ml-4" class="self-center ml-4"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
:loading="false" :loading="pkg?.js.busy"
v-on:click="remove()" v-on:click="remove()"
/> />
@ -52,7 +59,7 @@ const remove = async () => {
size="small" size="small"
class="self-center ml-4" class="self-center ml-4"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
:loading="false" :loading="pkg?.js.busy"
v-on:click="install()" v-on:click="install()"
/> />
</template> </template>

View File

@ -13,8 +13,9 @@ const props = defineProps({
pkg: Object as () => Package, pkg: Object as () => Package,
}); });
const toggle = (value: boolean) => { const toggle = async (value: boolean) => {
store.toggle(props.pkg, value); store.toggle(props.pkg, value);
await store.reloadProfile();
}; };
</script> </script>

View File

@ -29,12 +29,18 @@ export const usePkgStore = defineStore('pkg', {
actions: { actions: {
setupListeners() { setupListeners() {
listen<InstallStatus>('install-start', async (ev) => { listen<InstallStatus>('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<InstallStatus>('install-end', async (ev) => { listen<InstallStatus>('install-end', async (ev) => {
await this.reload(ev.payload.pkg); const key = ev.payload.pkg;
await this.reload(key);
await this.reloadProfile(); 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; [key: string]: Package;
}; };
for (const k of Object.keys(this)) { for (const [k, v] of Object.entries(data)) {
delete this.pkg[k]; 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) { async reload(pkgOrKey: string | Package) {
const key = const key =
typeof pkgOrKey === 'string' ? pkgOrKey : pkgKey(pkgOrKey); typeof pkgOrKey === 'string' ? pkgOrKey : pkgKey(pkgOrKey);
const rv: Package = await invoke('get_package', { const pkg: Package = await invoke('get_package', {
key, key,
}); });
this.reloadWith(key, pkg);
},
async reloadWith(key: string, pkg: Package) {
if (this.pkg[key] === undefined) { if (this.pkg[key] === undefined) {
this.pkg[key] = {} as Package; this.pkg[key] = { js: { busy: false } } as Package;
} else { } else {
this.pkg[key].loc = null; this.pkg[key].loc = null;
this.pkg[key].rmt = null; this.pkg[key].rmt = null;
} }
Object.assign(this.pkg[key], rv); Object.assign(this.pkg[key], pkg);
}, },
async initProfile(exePath: string) { async initProfile(exePath: string) {

View File

@ -14,6 +14,9 @@ export interface Package {
download_url: string; download_url: string;
deprecated: boolean; deprecated: boolean;
} | null; } | null;
js: {
busy: boolean;
};
} }
export interface Profile { export interface Profile {