diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff7bd7..d6815f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.13.0 + +- Added profile imports/exports +- Fixed error when trying to open an empty Ongeki profile +- Switched the default color scheme from invisible to purple + ## 0.12.1 - Chunithm: fixed crash when using mempatcher diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index 6e1cb36..79b4cfc 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -410,6 +410,33 @@ pub async fn create_shortcut(app: AppHandle, profile_meta: ProfileMeta) -> Resul util::create_shortcut(app, &profile_meta).map_err(|e| e.to_string()) } +#[tauri::command] +pub async fn export_profile(state: State<'_, Mutex>, export_keychip: bool, files: Vec) -> Result<(), String> { + log::debug!("invoke: export_profile({:?}, {:?} files)", export_keychip, files.len()); + + let appd = state.lock().await; + match &appd.profile { + Some(p) => { + p.export(export_keychip, files) + .map_err(|e| e.to_string())?; + } + None => { + let err = "export_profile: no profile".to_owned(); + log::error!("{}", err); + return Err(err); + } + } + + Ok(()) +} + +#[tauri::command] +pub async fn import_profile(state: State<'_, Mutex>, path: PathBuf) -> Result<(), String> { + log::debug!("invoke: import_profile({:?})", path); + + Profile::import(path).map_err(|e| e.to_string()) +} + #[tauri::command] pub async fn list_platform_capabilities() -> Result, ()> { log::debug!("invoke: list_platform_capabilities"); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 26050fc..4ce573a 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -205,6 +205,8 @@ pub async fn run(_args: Vec) { cmd::save_current_profile, cmd::load_segatools_ini, cmd::create_shortcut, + cmd::export_profile, + cmd::import_profile, cmd::get_global_config, cmd::set_global_config, diff --git a/rust/src/profiles/mod.rs b/rust/src/profiles/mod.rs index bc1a306..1cf573f 100644 --- a/rust/src/profiles/mod.rs +++ b/rust/src/profiles/mod.rs @@ -10,6 +10,7 @@ use std::fs::File; use tokio::process::Command; use tokio::task::JoinSet; +pub mod template; pub mod types; impl Profile { @@ -43,12 +44,6 @@ impl Profile { std::fs::create_dir_all(p.config_dir())?; std::fs::create_dir_all(p.data_dir())?; - if meta.game == Game::Ongeki { - if let Err(e) = Self::load_existing_mu3_ini(&p.data, &p.meta) { - log::error!("unable to load existing mu3.ini: {e}"); - } - } - match meta.game { Game::Ongeki => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-ongeki.ini"))?, Game::Chunithm => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-chunithm.ini"))?, @@ -431,12 +426,14 @@ impl Profile { } fn load_existing_mu3_ini(data: &ProfileData, meta: &ProfileMeta) -> Result<()> { - let mu3_ini_target_path = data.sgt.target.parent().ok_or_else(|| anyhow!("invalid target directory"))?.join("mu3.ini"); - let mu3_ini_profile_path = util::profile_config_dir(meta.game, &meta.name).join("mu3.ini"); - log::debug!("mu3.ini paths: {:?} {:?}", mu3_ini_target_path, mu3_ini_profile_path); - if mu3_ini_target_path.exists() && !mu3_ini_profile_path.exists() { - std::fs::copy(&mu3_ini_target_path, &mu3_ini_profile_path)?; - log::info!("copied mu3.ini from {:?}", &mu3_ini_target_path); + if let Some(parent) = data.sgt.target.parent() { + let mu3_ini_target_path = parent.join("mu3.ini"); + let mu3_ini_profile_path = util::profile_config_dir(meta.game, &meta.name).join("mu3.ini"); + log::debug!("mu3.ini paths: {:?} {:?}", mu3_ini_target_path, mu3_ini_profile_path); + if mu3_ini_target_path.exists() && !mu3_ini_profile_path.exists() { + std::fs::copy(&mu3_ini_target_path, &mu3_ini_profile_path)?; + log::info!("copied mu3.ini from {:?}", &mu3_ini_target_path); + } } Ok(()) } diff --git a/rust/src/profiles/template.rs b/rust/src/profiles/template.rs new file mode 100644 index 0000000..6b0cf88 --- /dev/null +++ b/rust/src/profiles/template.rs @@ -0,0 +1,90 @@ +use std::{fs::File, io::{Read, Write}, path::PathBuf}; +use zip::{write::FileOptions, ZipArchive, ZipWriter}; +use crate::util; +use super::{Profile, ProfilePaths}; + +impl Profile { + fn find_template_json(archive: &mut ZipArchive) -> anyhow::Result { + if let Ok(mut file) = archive.by_name("template.json") { + let mut contents = Vec::new(); + file.read_to_end(&mut contents)?; + Ok(String::from_utf8(contents)?) + } else { + anyhow::bail!("invalid template: no template.json found") + } + } + pub fn import(path: PathBuf) -> anyhow::Result<()> { + let file = File::open(path)?; + let mut archive = ZipArchive::new(file)?; + + match Self::find_template_json(&mut archive) { + Ok(raw_p) => { + let p = serde_json::from_str::(&raw_p)?; + let dir = util::config_dir().join(format!("profile-{}-{}", &p.meta.game, &p.meta.name)); + if dir.exists() { + anyhow::bail!("profile {} already exists", &p.meta.name); + } + std::fs::create_dir(&dir)?; + archive.extract(&dir)?; + std::fs::remove_file(dir.join("template.json"))?; + std::fs::write(dir.join("profile.json"), serde_json::to_string_pretty(&p.data)?)?; + } + Err(e) => { + return Err(e); + } + } + + Ok(()) + } + pub fn export(&self, export_keychip: bool, extra_files: Vec) -> anyhow::Result<()> { + let mut prf = self.clone(); + + let dir = util::config_dir().join("exports"); + + if !dir.exists() { + std::fs::create_dir(&dir)?; + } + + let path = dir.join(format!("{}-{}-template.zip", &self.meta.game, &self.meta.name)); + + { + let sgt = &mut prf.data.sgt; + sgt.target = PathBuf::new(); + if sgt.amfs.is_absolute() { + sgt.amfs = PathBuf::new(); + } + if sgt.option.is_absolute() { + sgt.option = PathBuf::new(); + } + if sgt.appdata.is_absolute() { + sgt.appdata = PathBuf::new(); + } + } + + { + let network = &mut prf.data.network; + if network.local_path.is_absolute() { + network.local_path = PathBuf::new(); + } + if !export_keychip { + network.keychip = String::new(); + } + } + + let file = File::create(&path)?; + let mut zip = ZipWriter::new(file); + let options: FileOptions<'_, ()> = FileOptions::default(); + zip.start_file("template.json", options)?; + zip.write_all(&serde_json::to_string_pretty(&prf)?.as_bytes())?; + + for file in extra_files { + log::debug!("extra file: {file}"); + zip.start_file(&file, options)?; + zip.write_all(&std::fs::read(self.config_dir().join(file))?)?; + } + + zip.finish()?; + + Ok(()) + } +} \ No newline at end of file diff --git a/rust/tauri.conf.json b/rust/tauri.conf.json index 5479fa6..20710b7 100644 --- a/rust/tauri.conf.json +++ b/rust/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "STARTLINER", - "version": "0.12.1", + "version": "0.13.0", "identifier": "zip.patafour.startliner", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/components/ProfileList.vue b/src/components/ProfileList.vue index 4f13142..f023b88 100644 --- a/src/components/ProfileList.vue +++ b/src/components/ProfileList.vue @@ -1,29 +1,154 @@