feat: groundwork for multi-profile support
This commit is contained in:
298
public/sticker-chunithm.svg
Normal file
298
public/sticker-chunithm.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 244 KiB |
@ -1,16 +1,64 @@
|
|||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
|
use crate::{model::misc::Game, pkg::PkgKey};
|
||||||
use crate::pkg::PkgKey;
|
|
||||||
use crate::pkg_store::PackageStore;
|
use crate::pkg_store::PackageStore;
|
||||||
use crate::Profile;
|
use crate::{util, Profile};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::AppHandle;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||||
|
pub struct GlobalConfig {
|
||||||
|
pub recent_profile: Option<(Game, String)>
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
pub profile: Option<Profile>,
|
pub profile: Option<Profile>,
|
||||||
pub pkgs: PackageStore,
|
pub pkgs: PackageStore,
|
||||||
|
pub cfg: GlobalConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
|
pub fn new(app: AppHandle) -> AppData {
|
||||||
|
let path = util::get_dirs()
|
||||||
|
.config_dir()
|
||||||
|
.join("config.json");
|
||||||
|
|
||||||
|
let cfg = std::fs::read_to_string(&path)
|
||||||
|
.and_then(|s| Ok(serde_json::from_str::<GlobalConfig>(&s)?))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let profile = match cfg.recent_profile {
|
||||||
|
Some((ref game, ref name)) => Profile::load(game, name).ok(),
|
||||||
|
None => None
|
||||||
|
};
|
||||||
|
|
||||||
|
AppData {
|
||||||
|
profile,
|
||||||
|
pkgs: PackageStore::new(app),
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&self) -> Result<(), std::io::Error> {
|
||||||
|
let path = util::get_dirs()
|
||||||
|
.config_dir()
|
||||||
|
.join("config.json");
|
||||||
|
|
||||||
|
std::fs::write(&path, serde_json::to_string(&self.cfg)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_profile(&mut self, game: &Game, name: &str) -> Result<()> {
|
||||||
|
self.profile = Profile::load(game, name).ok();
|
||||||
|
if self.profile.is_some() {
|
||||||
|
self.cfg.recent_profile = Some((game.to_owned(), name.to_owned()));
|
||||||
|
} else {
|
||||||
|
self.cfg.recent_profile = None;
|
||||||
|
}
|
||||||
|
self.write()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn toggle_package(&mut self, key: PkgKey, enable: bool) -> Result<()> {
|
pub fn toggle_package(&mut self, key: PkgKey, enable: bool) -> Result<()> {
|
||||||
log::debug!("toggle: {} {}", key, enable);
|
log::debug!("toggle: {} {}", key, enable);
|
||||||
|
|
||||||
@ -22,12 +70,12 @@ impl AppData {
|
|||||||
let loc = pkg.loc
|
let loc = pkg.loc
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
.ok_or_else(|| anyhow!("Attempted to enable a non-existent package"))?;
|
||||||
profile.mods.insert(key);
|
profile.data.mods.insert(key);
|
||||||
for d in &loc.dependencies {
|
for d in &loc.dependencies {
|
||||||
_ = self.toggle_package(d.clone(), true);
|
_ = self.toggle_package(d.clone(), true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
profile.mods.remove(&key);
|
profile.data.mods.remove(&key);
|
||||||
for (ckey, pkg) in self.pkgs.get_all() {
|
for (ckey, pkg) in self.pkgs.get_all() {
|
||||||
if let Some(loc) = pkg.loc {
|
if let Some(loc) = pkg.loc {
|
||||||
if loc.dependencies.contains(&key) {
|
if loc.dependencies.contains(&key) {
|
||||||
@ -42,7 +90,7 @@ impl AppData {
|
|||||||
|
|
||||||
pub fn sum_packages(&self, p: &Profile) -> String {
|
pub fn sum_packages(&self, p: &Profile) -> String {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
for pkg in &p.mods {
|
for pkg in &p.data.mods {
|
||||||
let x = self.pkgs.get(pkg).unwrap().loc.as_ref().unwrap();
|
let x = self.pkgs.get(pkg).unwrap().loc.as_ref().unwrap();
|
||||||
pkg.hash(&mut hasher);
|
pkg.hash(&mut hasher);
|
||||||
x.version.hash(&mut hasher);
|
x.version.hash(&mut hasher);
|
||||||
|
@ -4,6 +4,7 @@ use std::path::PathBuf;
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
use crate::model::misc::Game;
|
||||||
use crate::pkg::{Package, PkgKey};
|
use crate::pkg::{Package, PkgKey};
|
||||||
use crate::pkg_store::InstallResult;
|
use crate::pkg_store::InstallResult;
|
||||||
use crate::profile::Profile;
|
use crate::profile::Profile;
|
||||||
@ -106,6 +107,23 @@ pub async fn fetch_listings(state: State<'_, Mutex<AppData>>) -> Result<(), Stri
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_profiles() -> Result<Vec<(Game, String)>, String> {
|
||||||
|
log::debug!("invoke: list_profiles");
|
||||||
|
|
||||||
|
let list = Profile::list().await.map_err(|e| e.to_string())?;
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn load_profile(state: State<'_, Mutex<AppData>>, game: Game, name: String) -> Result<(), String> {
|
||||||
|
log::debug!("invoke: load_profile({} {:?})", game, name);
|
||||||
|
|
||||||
|
let mut appd = state.lock().await;
|
||||||
|
appd.switch_profile(&game, &name).map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<Profile>, ()> {
|
pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Option<Profile>, ()> {
|
||||||
log::debug!("invoke: get_current_profile");
|
log::debug!("invoke: get_current_profile");
|
||||||
@ -115,8 +133,8 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_profile(state: State<'_, Mutex<AppData>>) -> Result<(), ()> {
|
pub async fn save_current_profile(state: State<'_, Mutex<AppData>>) -> Result<(), ()> {
|
||||||
log::debug!("invoke: save_profile");
|
log::debug!("invoke: save_current_profile");
|
||||||
|
|
||||||
let appd = state.lock().await;
|
let appd = state.lock().await;
|
||||||
if let Some(p) = &appd.profile {
|
if let Some(p) = &appd.profile {
|
||||||
@ -133,18 +151,17 @@ pub async fn init_profile(
|
|||||||
state: State<'_, Mutex<AppData>>,
|
state: State<'_, Mutex<AppData>>,
|
||||||
exe_path: PathBuf
|
exe_path: PathBuf
|
||||||
) -> Result<Profile, String> {
|
) -> Result<Profile, String> {
|
||||||
log::debug!("invoke: init_profile({})", exe_path.to_string_lossy());
|
log::debug!("invoke: init_profile({:?})", exe_path);
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
let new_profile = Profile::new(exe_path);
|
if let Some(new_profile) = Profile::new(exe_path) {
|
||||||
|
new_profile.save().await;
|
||||||
|
appd.profile = Some(new_profile.clone());
|
||||||
|
|
||||||
new_profile.save().await;
|
Ok(new_profile)
|
||||||
appd.profile = Some(new_profile.clone());
|
} else {
|
||||||
|
Err("Unrecognized game".to_owned())
|
||||||
fs::create_dir(new_profile.dir()).await
|
}
|
||||||
.map_err(|e| format!("Unable to create profile directory: {}", e))?;
|
|
||||||
|
|
||||||
Ok(new_profile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[tauri::command]
|
// #[tauri::command]
|
||||||
@ -184,7 +201,7 @@ pub async fn write_profile_data(
|
|||||||
content: String
|
content: String
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let appd = state.lock().await;
|
let appd = state.lock().await;
|
||||||
|
|
||||||
if let Some(p) = &appd.profile {
|
if let Some(p) = &appd.profile {
|
||||||
fs::write(p.dir().join(&path), content).await
|
fs::write(p.dir().join(&path), content).await
|
||||||
.map_err(|e| format!("Unable to write to {:?}: {}", path, e))?;
|
.map_err(|e| format!("Unable to write to {:?}: {}", path, e))?;
|
||||||
@ -204,7 +221,7 @@ pub async fn set_cfg(
|
|||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
if let Some(p) = &mut appd.profile {
|
if let Some(p) = &mut appd.profile {
|
||||||
p.cfg.insert(key, value);
|
p.data.cfg.insert(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -12,7 +12,6 @@ mod appdata;
|
|||||||
use closure::closure;
|
use closure::closure;
|
||||||
use appdata::AppData;
|
use appdata::AppData;
|
||||||
use pkg::PkgKey;
|
use pkg::PkgKey;
|
||||||
use pkg_store::PackageStore;
|
|
||||||
use profile::Profile;
|
use profile::Profile;
|
||||||
use tauri::{Listener, Manager};
|
use tauri::{Listener, Manager};
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
@ -47,6 +46,7 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
// Todo deindent this chimera
|
// Todo deindent this chimera
|
||||||
let url = &args[1];
|
let url = &args[1];
|
||||||
if &url[..13] == "rainycolor://" {
|
if &url[..13] == "rainycolor://" {
|
||||||
|
log::info!("Deep link: {}", url);
|
||||||
let regex = regex::Regex::new(
|
let regex = regex::Regex::new(
|
||||||
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
|
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
|
||||||
).expect("Invalid regex");
|
).expect("Invalid regex");
|
||||||
@ -72,10 +72,7 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let app_data = AppData {
|
let app_data = AppData::new(app.handle().clone());
|
||||||
profile: Profile::load(),
|
|
||||||
pkgs: PackageStore::new(app.handle().clone())
|
|
||||||
};
|
|
||||||
|
|
||||||
app.manage(Mutex::new(app_data));
|
app.manage(Mutex::new(app_data));
|
||||||
app.deep_link().register_all()?;
|
app.deep_link().register_all()?;
|
||||||
@ -103,9 +100,11 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
cmd::install_package,
|
cmd::install_package,
|
||||||
cmd::delete_package,
|
cmd::delete_package,
|
||||||
cmd::toggle_package,
|
cmd::toggle_package,
|
||||||
cmd::get_current_profile,
|
cmd::list_profiles,
|
||||||
cmd::init_profile,
|
cmd::init_profile,
|
||||||
cmd::save_profile,
|
cmd::load_profile,
|
||||||
|
cmd::get_current_profile,
|
||||||
|
cmd::save_current_profile,
|
||||||
cmd::read_profile_data,
|
cmd::read_profile_data,
|
||||||
cmd::write_profile_data,
|
cmd::write_profile_data,
|
||||||
cmd::startline,
|
cmd::startline,
|
||||||
|
@ -46,7 +46,7 @@ async fn prepare_packages(p: &Profile) -> Result<()> {
|
|||||||
fs::remove_dir_all(dir_out.join("BepInEx")).await?;
|
fs::remove_dir_all(dir_out.join("BepInEx")).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for m in &p.mods {
|
for m in &p.data.mods {
|
||||||
log::debug!("Preparing {}", m);
|
log::debug!("Preparing {}", m);
|
||||||
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
let (namespace, name) = m.0.split_at(m.0.find("-").expect("Invalid mod definition"));
|
||||||
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
|
let bpx_dir = util::pkg_dir_of(namespace, &name[1..]) // cut the hyphen
|
||||||
@ -65,13 +65,15 @@ async fn prepare_packages(p: &Profile) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("prepare packages: done");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn prepare_config(p: &Profile) -> Result<()> {
|
pub async fn prepare_config(p: &Profile) -> Result<()> {
|
||||||
let dir_out = p.dir();
|
let dir_out = p.dir();
|
||||||
|
|
||||||
let ini_in_raw = fs::read_to_string(p.exe_dir.join("segatools.ini")).await?;
|
let ini_in_raw = fs::read_to_string(p.data.exe_dir.join("segatools.ini")).await?;
|
||||||
let ini_in = Ini::load_from_str(&ini_in_raw)?;
|
let ini_in = Ini::load_from_str(&ini_in_raw)?;
|
||||||
let mut opt_dir_in = PathBuf::from(
|
let mut opt_dir_in = PathBuf::from(
|
||||||
ini_in.section(Some("vfs"))
|
ini_in.section(Some("vfs"))
|
||||||
@ -80,7 +82,7 @@ pub async fn prepare_config(p: &Profile) -> Result<()> {
|
|||||||
.ok_or_else(|| anyhow!("No option specified in segatools.ini"))?
|
.ok_or_else(|| anyhow!("No option specified in segatools.ini"))?
|
||||||
);
|
);
|
||||||
if opt_dir_in.is_relative() {
|
if opt_dir_in.is_relative() {
|
||||||
opt_dir_in = p.exe_dir.join(opt_dir_in);
|
opt_dir_in = p.data.exe_dir.join(opt_dir_in);
|
||||||
}
|
}
|
||||||
let opt_dir_out = &dir_out.join("option");
|
let opt_dir_out = &dir_out.join("option");
|
||||||
|
|
||||||
@ -104,11 +106,13 @@ pub async fn prepare_config(p: &Profile) -> Result<()> {
|
|||||||
|
|
||||||
ini_out.write_to_file(dir_out.join("segatools.ini"))?;
|
ini_out.write_to_file(dir_out.join("segatools.ini"))?;
|
||||||
|
|
||||||
log::debug!("Option dir: {} -> {}", opt_dir_in.to_string_lossy(), opt_dir_out.to_string_lossy());
|
log::debug!("Option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
||||||
for opt in opt_dir_in.read_dir()? {
|
for opt in opt_dir_in.read_dir()? {
|
||||||
let opt = opt?;
|
let opt = opt?;
|
||||||
symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?;
|
symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log::debug!("prepare config: done");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
@ -2,6 +2,27 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub enum Game {
|
pub enum Game {
|
||||||
|
#[serde(rename = "ongeki")]
|
||||||
Ongeki,
|
Ongeki,
|
||||||
|
#[serde(rename = "chunithm")]
|
||||||
Chunithm,
|
Chunithm,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Game {
|
||||||
|
pub fn from_str(s: &str) -> Option<Game> {
|
||||||
|
match s {
|
||||||
|
"ongeki" => Some(Game::Ongeki),
|
||||||
|
"chunithm" => Some(Game::Chunithm),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Game {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Game::Ongeki => write!(f, "ongeki"),
|
||||||
|
Game::Chunithm => write!(f, "chunithm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Result, anyhow};
|
||||||
use std::{collections::{BTreeSet, HashMap}, path::PathBuf};
|
use std::{collections::{BTreeSet, HashMap}, path::{Path, PathBuf}};
|
||||||
use crate::{model::misc, pkg::PkgKey, util};
|
use crate::{model::misc::{self, Game}, pkg::PkgKey, util};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
// {game}-profile-{name}.json
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct Profile {
|
pub struct Profile {
|
||||||
pub game: misc::Game,
|
pub game: misc::Game,
|
||||||
pub exe_dir: PathBuf,
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub data: ProfileData
|
||||||
|
}
|
||||||
|
|
||||||
|
// The contents of profile-{game}-{name}.json
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
pub struct ProfileData {
|
||||||
|
pub exe_dir: PathBuf,
|
||||||
pub mods: BTreeSet<PkgKey>,
|
pub mods: BTreeSet<PkgKey>,
|
||||||
pub wine_runtime: Option<PathBuf>,
|
pub wine_runtime: Option<PathBuf>,
|
||||||
pub wine_prefix: Option<PathBuf>,
|
pub wine_prefix: Option<PathBuf>,
|
||||||
@ -21,80 +24,126 @@ pub struct Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Profile {
|
impl Profile {
|
||||||
pub fn new(exe_path: PathBuf) -> Profile {
|
pub fn new(exe_path: PathBuf) -> Option<Profile> {
|
||||||
Profile {
|
let game;
|
||||||
game: misc::Game::Ongeki,
|
if exe_path.ends_with("mu3.exe") {
|
||||||
exe_dir: exe_path.parent().unwrap().to_owned(),
|
game = misc::Game::Ongeki
|
||||||
name: "ongeki-default".to_owned(),
|
} else if exe_path.ends_with("chusanApp.exe") {
|
||||||
mods: BTreeSet::new(),
|
// game = misc::Game::Chunithm;
|
||||||
|
return None;
|
||||||
#[cfg(target_os = "linux")]
|
} else {
|
||||||
wine_runtime: Some(std::path::Path::new("/usr/bin/wine").to_path_buf()),
|
return None;
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
wine_runtime: None,
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
wine_prefix: Some(
|
|
||||||
directories::UserDirs::new()
|
|
||||||
.expect("No home directory")
|
|
||||||
.home_dir()
|
|
||||||
.join(".wine"),
|
|
||||||
),
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
wine_prefix: None,
|
|
||||||
cfg: HashMap::new()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(Profile {
|
||||||
|
name: format!("{}", "default"),
|
||||||
|
game,
|
||||||
|
data: ProfileData {
|
||||||
|
exe_dir: exe_path.parent().unwrap().to_owned(),
|
||||||
|
mods: BTreeSet::new(),
|
||||||
|
wine_runtime: None,
|
||||||
|
wine_prefix: None,
|
||||||
|
cfg: HashMap::new()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dir(&self) -> PathBuf {
|
pub fn dir(&self) -> PathBuf {
|
||||||
util::get_dirs()
|
util::get_dirs()
|
||||||
.data_dir()
|
.data_dir()
|
||||||
.join("profile-".to_owned() + &self.name)
|
.join(format!("profile-{}-{}", self.game, self.name))
|
||||||
.to_owned()
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load() -> Option<Profile> {
|
pub async fn list() -> Result<Vec<(Game, String)>> {
|
||||||
|
let path = std::fs::read_dir(
|
||||||
|
util::get_dirs().config_dir()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut res = Vec::new();
|
||||||
|
|
||||||
|
for f in path {
|
||||||
|
let f = f?;
|
||||||
|
|
||||||
|
if let Some(pair) = Self::name_from_path(f.path()) {
|
||||||
|
res.push(pair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(game: &Game, name: &str) -> Result<Profile> {
|
||||||
let path = util::get_dirs()
|
let path = util::get_dirs()
|
||||||
.config_dir()
|
.config_dir()
|
||||||
.join("profile-ongeki-default.json");
|
.join(format!("profile-{}-{}.json", game, name));
|
||||||
if let Ok(s) = std::fs::read_to_string(path) {
|
if let Ok(s) = std::fs::read_to_string(&path) {
|
||||||
Some(serde_json::from_str(&s).expect("Invalid profile json"))
|
let (game, name) = Self::name_from_path(&path)
|
||||||
|
.ok_or_else(|| anyhow!("Invalid filename: {:?}", path.file_name()))?;
|
||||||
|
|
||||||
|
let data = serde_json::from_str::<ProfileData>(&s)
|
||||||
|
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
|
||||||
|
|
||||||
|
Ok(Profile {
|
||||||
|
game,
|
||||||
|
name,
|
||||||
|
data
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
Err(anyhow!("Unable to open {:?}", path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&self) {
|
pub async fn save(&self) {
|
||||||
let path = util::get_dirs()
|
let path = util::get_dirs()
|
||||||
.config_dir()
|
.config_dir()
|
||||||
.join("profile-ongeki-default.json");
|
.join(format!("profile-{}-{}.json", self.game, self.name));
|
||||||
let s = serde_json::to_string_pretty(self).unwrap();
|
|
||||||
|
let s = serde_json::to_string_pretty(&self.data).unwrap();
|
||||||
fs::write(&path, s).await.unwrap();
|
fs::write(&path, s).await.unwrap();
|
||||||
log::info!("Written to {}", path.to_string_lossy());
|
log::info!("Written to {}", path.to_string_lossy());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn get_cfg(&self, key: &str) -> Result<&serde_json::Value> {
|
pub fn get_cfg(&self, key: &str) -> Result<&serde_json::Value> {
|
||||||
self.cfg.get(key)
|
self.data.cfg.get(key)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Invalid config entry {}", key))
|
.ok_or_else(|| anyhow::anyhow!("Invalid config entry {}", key))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bool(&self, key: &str, default: bool) -> bool {
|
pub fn get_bool(&self, key: &str, default: bool) -> bool {
|
||||||
self.cfg.get(key)
|
self.data.cfg.get(key)
|
||||||
.and_then(|c| c.as_bool())
|
.and_then(|c| c.as_bool())
|
||||||
.unwrap_or(default)
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_int(&self, key: &str, default: i64) -> i64 {
|
pub fn get_int(&self, key: &str, default: i64) -> i64 {
|
||||||
self.cfg.get(key)
|
self.data.cfg.get(key)
|
||||||
.and_then(|c| c.as_i64())
|
.and_then(|c| c.as_i64())
|
||||||
.unwrap_or(default)
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_str(&self, key: &str, default: &str) -> String {
|
pub fn get_str(&self, key: &str, default: &str) -> String {
|
||||||
self.cfg.get(key)
|
self.data.cfg.get(key)
|
||||||
.and_then(|c| c.as_str())
|
.and_then(|c| c.as_str())
|
||||||
.unwrap_or(default)
|
.unwrap_or(default)
|
||||||
.to_owned()
|
.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn name_from_path(path: impl AsRef<Path>) -> Option<(Game, String)> {
|
||||||
|
let regex = regex::Regex::new(
|
||||||
|
r"profile-([^\-]+)-([^\-]+)\.json"
|
||||||
|
).expect("Invalid regex");
|
||||||
|
|
||||||
|
let fname = path.as_ref().file_name().unwrap_or_default().to_string_lossy();
|
||||||
|
|
||||||
|
if let Some(caps) = regex.captures(&fname) {
|
||||||
|
let game = caps.get(1).unwrap().as_str();
|
||||||
|
let name = caps.get(2).unwrap().as_str().to_owned();
|
||||||
|
if let Some(game) = Game::from_str(game) {
|
||||||
|
return Some((game, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::path::PathBuf;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
@ -26,13 +27,13 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
}
|
}
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let wine = p.wine_runtime.as_ref()
|
let wine = p.data.wine_runtime.clone()
|
||||||
.expect("No wine path specified");
|
.unwrap_or_else(|| PathBuf::from("/usr/bin/wine"));
|
||||||
|
|
||||||
game_builder = Command::new(wine);
|
game_builder = Command::new(&wine);
|
||||||
amd_builder = Command::new(wine);
|
amd_builder = Command::new(&wine);
|
||||||
|
|
||||||
game_builder.arg(p.exe_dir.join("inject.exe"));
|
game_builder.arg(p.data.exe_dir.join("inject.exe"));
|
||||||
amd_builder.arg("cmd.exe");
|
amd_builder.arg("cmd.exe");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,10 +43,10 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
"SEGATOOLS_CONFIG_PATH",
|
"SEGATOOLS_CONFIG_PATH",
|
||||||
&ini_path,
|
&ini_path,
|
||||||
)
|
)
|
||||||
.current_dir(&p.exe_dir)
|
.current_dir(&p.data.exe_dir)
|
||||||
.args([
|
.args([
|
||||||
"/C",
|
"/C",
|
||||||
&util::path_to_str(p.exe_dir.join("inject.exe"))?, "-d", "-k", "mu3hook.dll",
|
&util::path_to_str(p.data.exe_dir.join("inject.exe"))?, "-d", "-k", "mu3hook.dll",
|
||||||
"amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
"amdaemon.exe", "-f", "-c", "config_common.json", "config_server.json", "config_client.json"
|
||||||
]);
|
]);
|
||||||
game_builder
|
game_builder
|
||||||
@ -53,7 +54,7 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
"SEGATOOLS_CONFIG_PATH",
|
"SEGATOOLS_CONFIG_PATH",
|
||||||
ini_path,
|
ini_path,
|
||||||
)
|
)
|
||||||
.current_dir(&p.exe_dir)
|
.current_dir(&p.data.exe_dir)
|
||||||
.args([
|
.args([
|
||||||
"-d", "-k", "mu3hook.dll",
|
"-d", "-k", "mu3hook.dll",
|
||||||
"mu3.exe", "-monitor 1",
|
"mu3.exe", "-monitor 1",
|
||||||
@ -68,10 +69,14 @@ pub fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let wineprefix = p.wine_prefix.as_ref()
|
let wineprefix = p.data.wine_prefix.clone().unwrap_or_else(||
|
||||||
.expect("No wineprefix specified");
|
directories::UserDirs::new()
|
||||||
amd_builder.env("WINEPREFIX", wineprefix);
|
.expect("No home directory")
|
||||||
game_builder.env("WINEPREFIX", wineprefix);
|
.home_dir()
|
||||||
|
.join(".wine")
|
||||||
|
);
|
||||||
|
amd_builder.env("WINEPREFIX", &wineprefix);
|
||||||
|
game_builder.env("WINEPREFIX", &wineprefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,53 +7,31 @@ import TabPanel from 'primevue/tabpanel';
|
|||||||
import TabPanels from 'primevue/tabpanels';
|
import TabPanels from 'primevue/tabpanels';
|
||||||
import Tabs from 'primevue/tabs';
|
import Tabs from 'primevue/tabs';
|
||||||
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
|
||||||
import ModList from './ModList.vue';
|
import ModList from './ModList.vue';
|
||||||
import ModStore from './ModStore.vue';
|
import ModStore from './ModStore.vue';
|
||||||
import Options from './Options.vue';
|
import Options from './Options.vue';
|
||||||
|
import ProfileList from './ProfileList.vue';
|
||||||
import StartButton from './StartButton.vue';
|
import StartButton from './StartButton.vue';
|
||||||
import { usePkgStore } from '../stores';
|
import { usePkgStore, usePrfStore } from '../stores';
|
||||||
import { changePrimaryColor } from '../util';
|
|
||||||
|
|
||||||
const store = usePkgStore();
|
const pkg = usePkgStore();
|
||||||
store.setupListeners();
|
const prf = usePrfStore();
|
||||||
|
|
||||||
|
pkg.setupListeners();
|
||||||
|
prf.setupListeners();
|
||||||
|
|
||||||
const currentTab = ref('3');
|
const currentTab = ref('3');
|
||||||
|
|
||||||
const loadProfile = async (openWindow: boolean) => {
|
const isProfileDisabled = computed(() => prf.current === null);
|
||||||
await store.reloadProfile();
|
|
||||||
|
|
||||||
if (store.profile === null && openWindow) {
|
|
||||||
const exePath = await open({
|
|
||||||
multiple: false,
|
|
||||||
directory: false,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'mu3.exe' /* or chusanApp.exe'*/,
|
|
||||||
extensions: ['exe'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
if (exePath !== null) {
|
|
||||||
await store.initProfile(exePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (store.profile !== null) {
|
|
||||||
changePrimaryColor(store.profile.game);
|
|
||||||
currentTab.value = '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
await store.reloadAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isProfileDisabled = computed(() => store.profile === null);
|
|
||||||
|
|
||||||
onOpenUrl((urls) => {
|
|
||||||
console.log('deep link:', urls);
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProfile(false);
|
await prf.reloadList();
|
||||||
|
await prf.reload();
|
||||||
|
|
||||||
|
if (prf.current !== null) {
|
||||||
|
await pkg.reloadAll();
|
||||||
|
currentTab.value = '0';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -93,14 +71,10 @@ onMounted(async () => {
|
|||||||
missing.<br />Existing features are expected to break any
|
missing.<br />Existing features are expected to break any
|
||||||
time.
|
time.
|
||||||
<div v-if="isProfileDisabled">
|
<div v-if="isProfileDisabled">
|
||||||
<br />Select <code>mu3.exe</code> to create a
|
<br />Select <code>mu3.exe</code> to create a profile:
|
||||||
profile:<br />
|
</div>
|
||||||
<Button
|
<ProfileList />
|
||||||
label="Create profile"
|
<div v-if="isProfileDisabled">
|
||||||
icon="pi pi-plus"
|
|
||||||
aria-label="open-executable"
|
|
||||||
@click="loadProfile(true)"
|
|
||||||
/><br />
|
|
||||||
<div
|
<div
|
||||||
style="
|
style="
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
@ -115,10 +89,15 @@ onMounted(async () => {
|
|||||||
(this will change in the future)
|
(this will change in the future)
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
v-if="store.profile?.game === 'Ongeki'"
|
v-if="prf.current?.game === 'ongeki'"
|
||||||
src="/sticker-ongeki.svg"
|
src="/sticker-ongeki.svg"
|
||||||
class="fixed bottom-0 right-0"
|
class="fixed bottom-0 right-0"
|
||||||
/>
|
/>
|
||||||
|
<img
|
||||||
|
v-else-if="prf.current?.game === 'chunithm'"
|
||||||
|
src="/sticker-chunithm.svg"
|
||||||
|
class="fixed bottom-0 right-0"
|
||||||
|
/>
|
||||||
<br /><br /><br />
|
<br /><br /><br />
|
||||||
<Button
|
<Button
|
||||||
style="position: fixed; left: 10px; bottom: 10px"
|
style="position: fixed; left: 10px; bottom: 10px"
|
||||||
|
@ -1,20 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Fieldset from 'primevue/fieldset';
|
import Fieldset from 'primevue/fieldset';
|
||||||
import ModListEntry from './ModListEntry.vue';
|
import ModListEntry from './ModListEntry.vue';
|
||||||
import { usePkgStore } from '../stores';
|
import { usePkgStore, usePrfStore } from '../stores';
|
||||||
import { Profile } from '../types';
|
|
||||||
|
|
||||||
defineProps({
|
const pkg = usePkgStore();
|
||||||
profile: Object as () => Profile,
|
const prf = usePrfStore();
|
||||||
});
|
|
||||||
|
|
||||||
const pkgs = usePkgStore();
|
|
||||||
|
|
||||||
const group = () => {
|
const group = () => {
|
||||||
const a = Object.assign(
|
const a = Object.assign(
|
||||||
{},
|
{},
|
||||||
Object.groupBy(
|
Object.groupBy(
|
||||||
pkgs.allLocal
|
pkg.allLocal
|
||||||
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
|
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
|
||||||
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
|
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
|
||||||
({ namespace }) => namespace
|
({ namespace }) => namespace
|
||||||
@ -23,7 +19,7 @@ const group = () => {
|
|||||||
return a;
|
return a;
|
||||||
};
|
};
|
||||||
|
|
||||||
pkgs.reloadProfile();
|
prf.reload();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -5,17 +5,17 @@ import { open } from '@tauri-apps/plugin-shell';
|
|||||||
import InstallButton from './InstallButton.vue';
|
import InstallButton from './InstallButton.vue';
|
||||||
import ModTitlecard from './ModTitlecard.vue';
|
import ModTitlecard from './ModTitlecard.vue';
|
||||||
import UpdateButton from './UpdateButton.vue';
|
import UpdateButton from './UpdateButton.vue';
|
||||||
import { usePkgStore } from '../stores';
|
import { usePrfStore } from '../stores';
|
||||||
import { Package } from '../types';
|
import { Package } from '../types';
|
||||||
|
|
||||||
const store = usePkgStore();
|
const prf = usePrfStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
pkg: Object as () => Package,
|
pkg: Object as () => Package,
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggle = async (value: boolean) => {
|
const toggle = async (value: boolean) => {
|
||||||
await store.toggle(props.pkg, value);
|
await prf.togglePkg(props.pkg, value);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ const toggle = async (value: boolean) => {
|
|||||||
class="scale-[1.33] shrink-0"
|
class="scale-[1.33] shrink-0"
|
||||||
inputId="switch"
|
inputId="switch"
|
||||||
:disabled="!pkg?.loc"
|
:disabled="!pkg?.loc"
|
||||||
:modelValue="store.isEnabled(pkg)"
|
:modelValue="prf.isPkgEnabled(pkg)"
|
||||||
v-on:value-change="toggle"
|
v-on:value-change="toggle"
|
||||||
/>
|
/>
|
||||||
<InstallButton :pkg="pkg" />
|
<InstallButton :pkg="pkg" />
|
||||||
|
@ -6,18 +6,17 @@ import InputText from 'primevue/inputtext';
|
|||||||
import RadioButton from 'primevue/radiobutton';
|
import RadioButton from 'primevue/radiobutton';
|
||||||
import Toggle from 'primevue/toggleswitch';
|
import Toggle from 'primevue/toggleswitch';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import * as path from '@tauri-apps/api/path';
|
import { usePrfStore } from '../stores';
|
||||||
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
|
||||||
import { usePkgStore } from '../stores';
|
const prf = usePrfStore();
|
||||||
|
|
||||||
const store = usePkgStore();
|
|
||||||
const _cfg = <T extends string | number | boolean>(key: string, dflt: T) =>
|
const _cfg = <T extends string | number | boolean>(key: string, dflt: T) =>
|
||||||
computed({
|
computed({
|
||||||
get() {
|
get() {
|
||||||
return (store.cfg(key) as T) ?? dflt;
|
return (prf.cfg(key) as T) ?? dflt;
|
||||||
},
|
},
|
||||||
async set(value) {
|
async set(value) {
|
||||||
await store.set_cfg(key, value ?? dflt);
|
await prf.setCfg(key, value ?? dflt);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -126,7 +125,7 @@ const aimeCodeModel = computed({
|
|||||||
<InputText
|
<InputText
|
||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="store.cfg('aime') !== true"
|
:disabled="prf.cfg('aime') !== true"
|
||||||
:maxlength="20"
|
:maxlength="20"
|
||||||
placeholder="00000000000000000000"
|
placeholder="00000000000000000000"
|
||||||
v-model="aimeCodeModel"
|
v-model="aimeCodeModel"
|
||||||
|
73
src/components/ProfileList.vue
Normal file
73
src/components/ProfileList.vue
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import { usePrfStore } from '../stores';
|
||||||
|
|
||||||
|
const prf = usePrfStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mt-4 flex flex-wrap align-middle gap-4">
|
||||||
|
<Button
|
||||||
|
:disabled="prf.list.length > 0"
|
||||||
|
label="Create profile"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
aria-label="open-executable"
|
||||||
|
class="create-button"
|
||||||
|
@click="prf.prompt"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-for="p in prf.list">
|
||||||
|
<Button
|
||||||
|
:disabled="
|
||||||
|
prf.current?.game === p.game && prf.current?.name === p.name
|
||||||
|
"
|
||||||
|
:label="p.name"
|
||||||
|
:class="
|
||||||
|
(p.game === 'chunithm'
|
||||||
|
? 'chunithm-button'
|
||||||
|
: 'ongeki-button') +
|
||||||
|
' ' +
|
||||||
|
'self-center grow'
|
||||||
|
"
|
||||||
|
@click="prf.switchTo(p.game, p.name)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.create-button {
|
||||||
|
background-color: var(--p-green-400);
|
||||||
|
border-color: var(--p-green-400);
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-button:hover,
|
||||||
|
.create-button:active {
|
||||||
|
background-color: var(--p-green-300) !important;
|
||||||
|
border-color: var(--p-green-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ongeki-button {
|
||||||
|
background-color: var(--p-pink-400);
|
||||||
|
border-color: var(--p-pink-400);
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ongeki-button:hover,
|
||||||
|
.ongeki-button:active {
|
||||||
|
background-color: var(--p-pink-300) !important;
|
||||||
|
border-color: var(--p-pink-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chunithm-button {
|
||||||
|
background-color: var(--p-yellow-400);
|
||||||
|
border-color: var(--p-yellow-400);
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
.chunithm-button:hover,
|
||||||
|
.chunithm-button:active {
|
||||||
|
background-color: var(--p-yellow-300) !important;
|
||||||
|
border-color: var(--p-yellow-300) !important;
|
||||||
|
}
|
||||||
|
</style>
|
121
src/stores.ts
121
src/stores.ts
@ -1,18 +1,18 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { Package, Profile } from './types';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { pkgKey } from './util';
|
import { Game, Package, Profile, ProfileMeta } from './types';
|
||||||
|
import { changePrimaryColor, pkgKey } from './util';
|
||||||
|
|
||||||
type InstallStatus = {
|
type InstallStatus = {
|
||||||
pkg: string;
|
pkg: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePkgStore = defineStore('pkg', {
|
export const usePkgStore = defineStore('pkg', {
|
||||||
state: (): { pkg: { [key: string]: Package }; prf: Profile | null } => {
|
state: (): { pkg: { [key: string]: Package } } => {
|
||||||
return {
|
return {
|
||||||
pkg: {},
|
pkg: {},
|
||||||
prf: null,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@ -22,10 +22,6 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
all: (state) => Object.values(state),
|
all: (state) => Object.values(state),
|
||||||
allLocal: (state) => Object.values(state.pkg).filter((p) => p.loc),
|
allLocal: (state) => Object.values(state.pkg).filter((p) => p.loc),
|
||||||
allRemote: (state) => Object.values(state.pkg).filter((p) => p.rmt),
|
allRemote: (state) => Object.values(state.pkg).filter((p) => p.rmt),
|
||||||
profile: (state) => state.prf,
|
|
||||||
isEnabled: (state) => (pkg: Package | undefined) =>
|
|
||||||
pkg !== undefined && state.prf?.mods.includes(pkgKey(pkg)),
|
|
||||||
cfg: (state) => (key: string) => state.prf?.cfg[key],
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setupListeners() {
|
setupListeners() {
|
||||||
@ -38,7 +34,6 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
listen<InstallStatus>('install-end', async (ev) => {
|
listen<InstallStatus>('install-end', async (ev) => {
|
||||||
const key = ev.payload.pkg;
|
const key = ev.payload.pkg;
|
||||||
await this.reload(key);
|
await this.reload(key);
|
||||||
await this.reloadProfile();
|
|
||||||
this.pkg[key].js.busy = false;
|
this.pkg[key].js.busy = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -73,36 +68,108 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
Object.assign(this.pkg[key], pkg);
|
Object.assign(this.pkg[key], pkg);
|
||||||
},
|
},
|
||||||
|
|
||||||
async initProfile(exePath: string) {
|
|
||||||
this.prf = await invoke('init_profile', { exePath });
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveProfile() {
|
|
||||||
await invoke('save_profile');
|
|
||||||
},
|
|
||||||
|
|
||||||
async reloadProfile() {
|
|
||||||
this.prf = await invoke('get_current_profile');
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetch() {
|
async fetch() {
|
||||||
await invoke('fetch_listings');
|
await invoke('fetch_listings');
|
||||||
await this.reloadAll();
|
await this.reloadAll();
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
async toggle(pkg: Package | undefined, enable: boolean) {
|
export const usePrfStore = defineStore('prf', {
|
||||||
|
state: (): { prf: Profile | null; list: ProfileMeta[] } => {
|
||||||
|
return {
|
||||||
|
prf: null,
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
current: (state) => state.prf,
|
||||||
|
isPkgEnabled: (state) => (pkg: Package | undefined) =>
|
||||||
|
pkg !== undefined && state.prf?.data.mods.includes(pkgKey(pkg)),
|
||||||
|
cfg: (state) => (key: string) => state.prf?.data.cfg[key],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setupListeners() {
|
||||||
|
listen<InstallStatus>('install-end', async () => {
|
||||||
|
await this.reload();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async prompt() {
|
||||||
|
const exePath = await open({
|
||||||
|
multiple: false,
|
||||||
|
directory: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'mu3.exe or chusanApp.exe',
|
||||||
|
extensions: ['exe'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (exePath !== null) {
|
||||||
|
await this.create(exePath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(exePath: string) {
|
||||||
|
try {
|
||||||
|
await invoke('init_profile', { exePath });
|
||||||
|
await this.reload();
|
||||||
|
await this.reloadList();
|
||||||
|
} catch (e) {
|
||||||
|
this.prf = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.prf !== null) {
|
||||||
|
const pkgs = usePkgStore();
|
||||||
|
pkgs.reloadAll();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async switchTo(game: Game, name: string) {
|
||||||
|
await invoke('load_profile', { game, name });
|
||||||
|
await this.reload();
|
||||||
|
if (this.prf !== null) {
|
||||||
|
const pkgs = usePkgStore();
|
||||||
|
pkgs.reloadAll();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
await invoke('save_current_profile');
|
||||||
|
},
|
||||||
|
|
||||||
|
async reload() {
|
||||||
|
this.prf = await invoke('get_current_profile');
|
||||||
|
if (this.prf !== null) {
|
||||||
|
changePrimaryColor(this.prf.game);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async reloadList() {
|
||||||
|
const raw = (await invoke('list_profiles')) as [Game, string][];
|
||||||
|
|
||||||
|
this.list = raw.map(([game, name]) => {
|
||||||
|
return {
|
||||||
|
game,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async togglePkg(pkg: Package | undefined, enable: boolean) {
|
||||||
if (pkg === undefined) {
|
if (pkg === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await invoke('toggle_package', { key: pkgKey(pkg), enable });
|
await invoke('toggle_package', { key: pkgKey(pkg), enable });
|
||||||
await this.reloadProfile();
|
await this.reload();
|
||||||
await this.saveProfile();
|
await this.save();
|
||||||
},
|
},
|
||||||
|
|
||||||
async set_cfg(key: string, value: string | boolean | number) {
|
async setCfg(key: string, value: string | boolean | number) {
|
||||||
await invoke('set_cfg', { key, value });
|
await invoke('set_cfg', { key, value });
|
||||||
await this.reloadProfile();
|
await this.reload();
|
||||||
await this.saveProfile();
|
await this.save();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
15
src/types.ts
15
src/types.ts
@ -19,11 +19,16 @@ export interface Package {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Profile {
|
export type Game = 'ongeki' | 'chunithm';
|
||||||
|
|
||||||
|
export interface ProfileMeta {
|
||||||
|
game: Game;
|
||||||
name: string;
|
name: string;
|
||||||
game: 'Ongeki' | 'Chunithm';
|
|
||||||
mods: string[];
|
|
||||||
cfg: { [key: string]: string | boolean | number };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PackageC = Map<string, Package>;
|
export interface Profile extends ProfileMeta {
|
||||||
|
data: {
|
||||||
|
mods: string[];
|
||||||
|
cfg: { [key: string]: string | boolean | number };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { updatePrimaryPalette } from '@primevue/themes';
|
import { updatePrimaryPalette } from '@primevue/themes';
|
||||||
import { Package } from './types';
|
import { Package } from './types';
|
||||||
|
|
||||||
export const changePrimaryColor = (game: 'Ongeki' | 'Chunithm') => {
|
export const changePrimaryColor = (game: 'ongeki' | 'chunithm') => {
|
||||||
const color = game === 'Ongeki' ? 'pink' : 'yellow';
|
const color = game === 'ongeki' ? 'pink' : 'yellow';
|
||||||
|
|
||||||
updatePrimaryPalette({
|
updatePrimaryPalette({
|
||||||
50: `{${color}.50}`,
|
50: `{${color}.50}`,
|
||||||
|
Reference in New Issue
Block a user