feat: more breaking changes
This commit is contained in:
562
rust/Cargo.lock
generated
562
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -46,4 +46,4 @@ tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
|||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
winsafe = { version = "0.0.23", features = ["user"] }
|
winsafe = { version = "0.0.23", features = ["user"] }
|
||||||
displayz = "0.1.0"
|
displayz = "^0.2.0"
|
@ -14,56 +14,51 @@ pub struct GlobalConfig {
|
|||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
pub profile: Option<Profile>,
|
pub profile: Option<Profile>,
|
||||||
pub pkgs: PackageStore,
|
pub pkgs: PackageStore,
|
||||||
pub cfg: GlobalConfig
|
pub cfg: GlobalConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
pub fn new(app: AppHandle) -> AppData {
|
pub fn new(apph: AppHandle) -> AppData {
|
||||||
let path = util::get_dirs()
|
let cfg = std::fs::read_to_string(util::config_dir().join("config.json"))
|
||||||
.config_dir()
|
|
||||||
.join("config.json");
|
|
||||||
|
|
||||||
let cfg = std::fs::read_to_string(&path)
|
|
||||||
.and_then(|s| Ok(serde_json::from_str::<GlobalConfig>(&s)?))
|
.and_then(|s| Ok(serde_json::from_str::<GlobalConfig>(&s)?))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let profile = match cfg.recent_profile {
|
let profile = match cfg.recent_profile {
|
||||||
Some((ref game, ref name)) => Profile::load(game, name).ok(),
|
Some((ref game, ref name)) => Profile::load(game.clone(), name.clone()).ok(),
|
||||||
None => None
|
None => None
|
||||||
};
|
};
|
||||||
|
|
||||||
AppData {
|
AppData {
|
||||||
profile,
|
profile,
|
||||||
pkgs: PackageStore::new(app),
|
pkgs: PackageStore::new(apph.clone()),
|
||||||
cfg
|
cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&self) -> Result<(), std::io::Error> {
|
pub fn write(&self) -> Result<(), std::io::Error> {
|
||||||
let path = util::get_dirs()
|
std::fs::write(util::config_dir().join("config.json"), serde_json::to_string(&self.cfg)?)
|
||||||
.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<()> {
|
pub fn switch_profile(&mut self, game: Game, name: String) -> Result<()> {
|
||||||
self.profile = Profile::load(game, name).ok();
|
match Profile::load(game.clone(), name.clone()) {
|
||||||
if self.profile.is_some() {
|
Ok(profile) => {
|
||||||
self.cfg.recent_profile = Some((game.to_owned(), name.to_owned()));
|
self.profile = Some(profile);
|
||||||
} else {
|
self.cfg.recent_profile = Some((game, name));
|
||||||
self.cfg.recent_profile = None;
|
self.write()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.profile = None;
|
||||||
|
self.cfg.recent_profile = None;
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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);
|
||||||
|
|
||||||
let profile = self.profile.as_mut()
|
let profile = self.profile.as_mut().ok_or_else(|| anyhow!("No profile"))?;
|
||||||
.ok_or_else(|| anyhow!("No profile"))?;
|
|
||||||
|
|
||||||
if enable {
|
if enable {
|
||||||
let pkg = self.pkgs.get(&key)?;
|
let pkg = self.pkgs.get(&key)?;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use log;
|
use log;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
@ -9,7 +8,7 @@ use crate::pkg::{Package, PkgKey};
|
|||||||
use crate::pkg_store::InstallResult;
|
use crate::pkg_store::InstallResult;
|
||||||
use crate::profile::Profile;
|
use crate::profile::Profile;
|
||||||
use crate::appdata::AppData;
|
use crate::appdata::AppData;
|
||||||
use crate::{liner, start};
|
use crate::{liner, start, util};
|
||||||
|
|
||||||
use tauri::{AppHandle, Manager, State};
|
use tauri::{AppHandle, Manager, State};
|
||||||
|
|
||||||
@ -121,7 +120,7 @@ pub async fn load_profile(state: State<'_, Mutex<AppData>>, game: Game, name: St
|
|||||||
log::debug!("invoke: load_profile({} {:?})", game, name);
|
log::debug!("invoke: load_profile({} {:?})", game, name);
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
appd.switch_profile(&game, &name).map_err(|e| e.to_string())?;
|
appd.switch_profile(game, name).map_err(|e| e.to_string())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,17 +132,6 @@ pub async fn get_current_profile(state: State<'_, Mutex<AppData>>) -> Result<Opt
|
|||||||
Ok(appd.profile.clone())
|
Ok(appd.profile.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_current_profile_dir(state: State<'_, Mutex<AppData>>) -> Result<PathBuf, &str> {
|
|
||||||
let appd = state.lock().await;
|
|
||||||
|
|
||||||
if let Some(p) = &appd.profile {
|
|
||||||
Ok(p.dir())
|
|
||||||
} else {
|
|
||||||
Err("No profile loaded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn save_current_profile(state: State<'_, Mutex<AppData>>) -> Result<(), ()> {
|
pub async fn save_current_profile(state: State<'_, Mutex<AppData>>) -> Result<(), ()> {
|
||||||
log::debug!("invoke: save_current_profile");
|
log::debug!("invoke: save_current_profile");
|
||||||
@ -161,54 +149,23 @@ pub async fn save_current_profile(state: State<'_, Mutex<AppData>>) -> Result<()
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn init_profile(
|
pub async fn init_profile(
|
||||||
state: State<'_, Mutex<AppData>>,
|
state: State<'_, Mutex<AppData>>,
|
||||||
exe_path: PathBuf
|
game: Game,
|
||||||
|
name: String
|
||||||
) -> Result<Profile, String> {
|
) -> Result<Profile, String> {
|
||||||
log::debug!("invoke: init_profile({:?})", exe_path);
|
log::debug!("invoke: init_profile({}, {})", game, name);
|
||||||
|
|
||||||
let mut appd = state.lock().await;
|
let mut appd = state.lock().await;
|
||||||
if let Some(new_profile) = Profile::new(exe_path) {
|
let new_profile = Profile::new(game, name);
|
||||||
new_profile.save().await;
|
|
||||||
appd.profile = Some(new_profile.clone());
|
|
||||||
fs::create_dir_all(new_profile.dir()).await
|
|
||||||
.map_err(|e| format!("Unable to create the profile directory: {}", e))?;
|
|
||||||
|
|
||||||
Ok(new_profile)
|
fs::create_dir_all(new_profile.config_dir()).await
|
||||||
} else {
|
.map_err(|e| format!("Unable to create the profile config directory: {}", e))?;
|
||||||
Err("Unrecognized game".to_owned())
|
fs::create_dir_all(new_profile.data_dir()).await
|
||||||
}
|
.map_err(|e| format!("Unable to create the profile data directory: {}", e))?;
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
appd.profile = Some(new_profile.clone());
|
||||||
pub async fn read_profile_data(
|
new_profile.save().await;
|
||||||
state: State<'_, Mutex<AppData>>,
|
|
||||||
path: PathBuf
|
|
||||||
) -> Result<String, String> {
|
|
||||||
let appd = state.lock().await;
|
|
||||||
|
|
||||||
if let Some(p) = &appd.profile {
|
Ok(new_profile)
|
||||||
let res = fs::read_to_string(p.dir().join(&path)).await
|
|
||||||
.map_err(|e| format!("Unable to open {:?}: {}", path, e))?;
|
|
||||||
Ok(res)
|
|
||||||
} else {
|
|
||||||
Err("No profile loaded".to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn write_profile_data(
|
|
||||||
state: State<'_, Mutex<AppData>>,
|
|
||||||
path: PathBuf,
|
|
||||||
content: String
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let appd = state.lock().await;
|
|
||||||
|
|
||||||
if let Some(p) = &appd.profile {
|
|
||||||
fs::write(p.dir().join(&path), content).await
|
|
||||||
.map_err(|e| format!("Unable to write to {:?}: {}", path, e))?;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err("No profile loaded".to_owned())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -266,3 +223,10 @@ pub async fn list_displays() -> Result<Vec<String>, ()> {
|
|||||||
|
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_directories() -> Result<util::Dirs, ()> {
|
||||||
|
log::debug!("invoke: list_directores");
|
||||||
|
|
||||||
|
Ok(util::all_dirs().clone())
|
||||||
|
}
|
@ -17,7 +17,7 @@ pub async fn prepare_display(_: &Profile) -> Result<()> {
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn prepare_display(p: &Profile) -> Result<Option<DisplayInfo>> {
|
pub async fn prepare_display(p: &Profile) -> Result<Option<DisplayInfo>> {
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use displayz::{query_displays, Orientation, Resolution};
|
use displayz::{query_displays, Orientation, Resolution, Frequency};
|
||||||
|
|
||||||
let display_name = p.get_str("display", "default");
|
let display_name = p.get_str("display", "default");
|
||||||
let rotation = p.get_int("display-rotation", 0);
|
let rotation = p.get_int("display-rotation", 0);
|
||||||
@ -57,11 +57,22 @@ pub async fn prepare_display(p: &Profile) -> Result<Option<DisplayInfo>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let frequency: u32 = p.get_int("frequency", 60)
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| anyhow!("Invalid display frequency: {}", e))?;
|
||||||
|
|
||||||
|
let width: u32 = p.get_int("rez-w", 1080)
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| anyhow!("Invalid display width: {}", e))?;
|
||||||
|
|
||||||
|
let height: u32 = p.get_int("rez-h", 1080)
|
||||||
|
.try_into()
|
||||||
|
.map_err(|e| anyhow!("Invalid display height: {}", e))?;
|
||||||
|
|
||||||
|
settings.borrow_mut().frequency = Frequency::new(frequency);
|
||||||
|
|
||||||
if p.get_str("display-mode", "borderless") == "borderless" && p.get_bool("borderless-fullscreen", false) {
|
if p.get_str("display-mode", "borderless") == "borderless" && p.get_bool("borderless-fullscreen", false) {
|
||||||
settings.borrow_mut().resolution = Resolution::new(
|
settings.borrow_mut().resolution = Resolution::new(width, height);
|
||||||
p.get_int("rez-w", 1080).try_into().expect("Negative resolution"),
|
|
||||||
p.get_int("rez-h", 1920).try_into().expect("Negative resolution")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
display_set.apply()?;
|
display_set.apply()?;
|
||||||
@ -74,6 +85,7 @@ pub async fn prepare_display(p: &Profile) -> Result<Option<DisplayInfo>> {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub async fn undo_display(info: DisplayInfo) -> Result<()> {
|
pub async fn undo_display(info: DisplayInfo) -> Result<()> {
|
||||||
|
use anyhow::anyhow;
|
||||||
use displayz::query_displays;
|
use displayz::query_displays;
|
||||||
|
|
||||||
let display_set = query_displays()?;
|
let display_set = query_displays()?;
|
||||||
|
@ -31,12 +31,6 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
);
|
);
|
||||||
|
|
||||||
try_join!(
|
|
||||||
fs::create_dir_all(util::config_dir()),
|
|
||||||
fs::create_dir_all(util::pkg_dir()),
|
|
||||||
fs::create_dir_all(util::cache_dir())
|
|
||||||
).expect("Unable to create working directories");
|
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||||
let _ = app
|
let _ = app
|
||||||
@ -74,12 +68,29 @@ 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 apph = app.handle();
|
||||||
|
|
||||||
|
util::init_dirs(&apph);
|
||||||
|
|
||||||
let app_data = AppData::new(app.handle().clone());
|
let app_data = AppData::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()?;
|
||||||
|
|
||||||
let apph = app.handle();
|
log::debug!("\n{:?}\n{:?}\n{:?}", util::config_dir(), util::pkg_dir(), util::cache_dir());
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async {
|
||||||
|
let e = try_join!(
|
||||||
|
fs::create_dir_all(util::config_dir()),
|
||||||
|
fs::create_dir_all(util::pkg_dir()),
|
||||||
|
fs::create_dir_all(util::cache_dir())
|
||||||
|
);
|
||||||
|
if let Err(e) = e {
|
||||||
|
log::error!("Unable to create base directories: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
app.listen("download-end", closure!(clone apph, |ev| {
|
app.listen("download-end", closure!(clone apph, |ev| {
|
||||||
let raw = ev.payload();
|
let raw = ev.payload();
|
||||||
@ -107,17 +118,15 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
cmd::init_profile,
|
cmd::init_profile,
|
||||||
cmd::load_profile,
|
cmd::load_profile,
|
||||||
cmd::get_current_profile,
|
cmd::get_current_profile,
|
||||||
cmd::get_current_profile_dir,
|
|
||||||
cmd::save_current_profile,
|
cmd::save_current_profile,
|
||||||
cmd::read_profile_data,
|
cmd::set_cfg,
|
||||||
cmd::write_profile_data,
|
|
||||||
|
|
||||||
cmd::startline,
|
cmd::startline,
|
||||||
cmd::kill,
|
cmd::kill,
|
||||||
|
|
||||||
cmd::list_platform_capabilities,
|
|
||||||
cmd::set_cfg,
|
|
||||||
cmd::list_displays,
|
cmd::list_displays,
|
||||||
|
cmd::list_platform_capabilities,
|
||||||
|
cmd::list_directories,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
@ -17,7 +17,7 @@ async fn symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Resul
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn line_up(p: &Profile, pkg_hash: String) -> Result<()> {
|
pub async fn line_up(p: &Profile, pkg_hash: String) -> Result<()> {
|
||||||
let dir_out = p.dir();
|
let dir_out = p.data_dir();
|
||||||
|
|
||||||
if dir_out.join("option").exists() {
|
if dir_out.join("option").exists() {
|
||||||
fs::remove_dir_all(dir_out.join("option")).await?;
|
fs::remove_dir_all(dir_out.join("option")).await?;
|
||||||
@ -25,7 +25,7 @@ pub async fn line_up(p: &Profile, pkg_hash: String) -> Result<()> {
|
|||||||
|
|
||||||
fs::create_dir_all(dir_out.join("option")).await?;
|
fs::create_dir_all(dir_out.join("option")).await?;
|
||||||
|
|
||||||
let hash_path = p.dir().join(".sl-state");
|
let hash_path = p.data_dir().join(".sl-state");
|
||||||
let prev_hash = fs::read_to_string(&hash_path).await.unwrap_or_default();
|
let prev_hash = fs::read_to_string(&hash_path).await.unwrap_or_default();
|
||||||
if prev_hash != pkg_hash {
|
if prev_hash != pkg_hash {
|
||||||
log::debug!("state {} -> {}", prev_hash, pkg_hash);
|
log::debug!("state {} -> {}", prev_hash, pkg_hash);
|
||||||
@ -40,7 +40,7 @@ pub async fn line_up(p: &Profile, pkg_hash: String) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare_packages(p: &Profile) -> Result<()> {
|
async fn prepare_packages(p: &Profile) -> Result<()> {
|
||||||
let dir_out = p.dir();
|
let dir_out = p.data_dir();
|
||||||
|
|
||||||
if dir_out.join("BepInEx").exists() {
|
if dir_out.join("BepInEx").exists() {
|
||||||
fs::remove_dir_all(dir_out.join("BepInEx")).await?;
|
fs::remove_dir_all(dir_out.join("BepInEx")).await?;
|
||||||
@ -71,25 +71,26 @@ async fn prepare_packages(p: &Profile) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare_config(p: &Profile) -> Result<()> {
|
async fn prepare_config(p: &Profile) -> Result<()> {
|
||||||
let dir_out = p.dir();
|
let dir_out = p.data_dir();
|
||||||
|
|
||||||
let ini_in_raw = fs::read_to_string(p.data.exe_dir.join("segatools.ini")).await?;
|
let target_path = PathBuf::from(p.get_str("target-path", ""));
|
||||||
|
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
||||||
|
let ini_in_raw = fs::read_to_string(p.config_dir().join("segatools-base.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(p.get_str("option", ""));
|
||||||
ini_in.section(Some("vfs"))
|
if opt_dir_in.as_os_str().len() > 0 && opt_dir_in.is_relative() {
|
||||||
.ok_or_else(|| anyhow!("No VFS section in segatools.ini"))?
|
opt_dir_in = exe_dir.join(opt_dir_in);
|
||||||
.get("option")
|
|
||||||
.ok_or_else(|| anyhow!("No option specified in segatools.ini"))?
|
|
||||||
);
|
|
||||||
if opt_dir_in.is_relative() {
|
|
||||||
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");
|
||||||
|
|
||||||
let mut ini_out = ini_in.clone();
|
let mut ini_out = ini_in.clone();
|
||||||
ini_out.with_section(Some("vfs")).set(
|
ini_out.with_section(Some("vfs"))
|
||||||
"option",
|
.set(
|
||||||
util::path_to_str(opt_dir_out)?
|
"option",
|
||||||
|
util::path_to_str(opt_dir_out)?
|
||||||
|
)
|
||||||
|
.set("amfs", p.get_str("amfs", ""))
|
||||||
|
.set("appdata", p.get_str("appdata", "appdata")
|
||||||
);
|
);
|
||||||
ini_out.with_section(Some("unity"))
|
ini_out.with_section(Some("unity"))
|
||||||
.set("enable", "1")
|
.set("enable", "1")
|
||||||
@ -107,9 +108,11 @@ 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, opt_dir_out);
|
log::debug!("Option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
||||||
for opt in opt_dir_in.read_dir()? {
|
if opt_dir_in.as_os_str().len() > 0 {
|
||||||
let opt = opt?;
|
for opt in opt_dir_in.read_dir()? {
|
||||||
symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?;
|
let opt = opt?;
|
||||||
|
symlink(&opt.path(), opt_dir_out.join(opt.file_name())).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("prepare config: done");
|
log::debug!("prepare config: done");
|
||||||
|
@ -5,7 +5,6 @@ use crate::pkg::PkgKeyVersion;
|
|||||||
// /c/{game}/api/v1/package
|
// /c/{game}/api/v1/package
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct V1Package {
|
pub struct V1Package {
|
||||||
pub owner: String,
|
pub owner: String,
|
||||||
pub package_url: String,
|
pub package_url: String,
|
||||||
@ -14,7 +13,6 @@ pub struct V1Package {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct V1Version {
|
pub struct V1Version {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
@ -14,73 +14,67 @@ pub struct Profile {
|
|||||||
// The contents of profile-{game}-{name}.json
|
// The contents of profile-{game}-{name}.json
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct ProfileData {
|
pub struct ProfileData {
|
||||||
pub exe_dir: PathBuf,
|
|
||||||
pub mods: BTreeSet<PkgKey>,
|
pub mods: BTreeSet<PkgKey>,
|
||||||
pub wine_runtime: Option<PathBuf>,
|
|
||||||
pub wine_prefix: Option<PathBuf>,
|
|
||||||
// cfg is temporarily just a map to make iteration easier
|
// cfg is temporarily just a map to make iteration easier
|
||||||
// eventually it should become strict
|
// eventually it should become strict
|
||||||
pub cfg: BTreeMap<String, serde_json::Value>
|
pub cfg: BTreeMap<String, serde_json::Value>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Profile {
|
impl Profile {
|
||||||
pub fn new(exe_path: PathBuf) -> Option<Profile> {
|
pub fn new(game: Game, mut name: String) -> Profile {
|
||||||
let game;
|
name = name.trim().replace(" ", "-");
|
||||||
if exe_path.ends_with("mu3.exe") {
|
|
||||||
game = misc::Game::Ongeki
|
while Self::config_dir_f(&game, &name).exists() {
|
||||||
} else if exe_path.ends_with("chusanApp.exe") {
|
name = format!("new-{}", name);
|
||||||
// game = misc::Game::Chunithm;
|
|
||||||
return None;
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(Profile {
|
Profile {
|
||||||
name: format!("{}", "default"),
|
name,
|
||||||
game,
|
game,
|
||||||
data: ProfileData {
|
data: ProfileData {
|
||||||
exe_dir: exe_path.parent().unwrap().to_owned(),
|
|
||||||
mods: BTreeSet::new(),
|
mods: BTreeSet::new(),
|
||||||
wine_runtime: None,
|
|
||||||
wine_prefix: None,
|
|
||||||
cfg: BTreeMap::new()
|
cfg: BTreeMap::new()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dir(&self) -> PathBuf {
|
fn config_dir_f(game: &Game, name: &str) -> PathBuf {
|
||||||
util::get_dirs()
|
util::config_dir().join(format!("profile-{}-{}", game, name))
|
||||||
.data_dir()
|
}
|
||||||
.join(format!("profile-{}-{}", self.game, self.name))
|
|
||||||
.to_owned()
|
pub fn config_dir(&self) -> PathBuf {
|
||||||
|
Self::config_dir_f(&self.game, &self.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_dir(&self) -> PathBuf {
|
||||||
|
util::data_dir().join(format!("profile-{}-{}", self.game, self.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list() -> Result<Vec<(Game, String)>> {
|
pub async fn list() -> Result<Vec<(Game, String)>> {
|
||||||
let path = std::fs::read_dir(
|
let path = std::fs::read_dir(util::config_dir())?;
|
||||||
util::get_dirs().config_dir()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut res = Vec::new();
|
let mut res = Vec::new();
|
||||||
|
|
||||||
for f in path {
|
for f in path {
|
||||||
let f = f?;
|
let f = f?;
|
||||||
|
|
||||||
if let Some(pair) = Self::name_from_path(f.path()) {
|
if let Ok(meta) = f.metadata() {
|
||||||
res.push(pair);
|
if !meta.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log::debug!("{:?}", f);
|
||||||
|
if let Some(pair) = Self::name_from_path(f.path()) {
|
||||||
|
res.push(pair);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(game: &Game, name: &str) -> Result<Profile> {
|
pub fn load(game: Game, name: String) -> Result<Profile> {
|
||||||
let path = util::get_dirs()
|
let path = Self::config_dir_f(&game, &name).join("profile.json");
|
||||||
.config_dir()
|
|
||||||
.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) {
|
||||||
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)
|
let data = serde_json::from_str::<ProfileData>(&s)
|
||||||
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
|
.map_err(|e| anyhow!("Unable to parse {:?}: {:?}", path, e))?;
|
||||||
|
|
||||||
@ -95,9 +89,7 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&self) {
|
pub async fn save(&self) {
|
||||||
let path = util::get_dirs()
|
let path = self.config_dir().join("profile.json");
|
||||||
.config_dir()
|
|
||||||
.join(format!("profile-{}-{}.json", self.game, self.name));
|
|
||||||
|
|
||||||
let s = serde_json::to_string_pretty(&self.data).unwrap();
|
let s = serde_json::to_string_pretty(&self.data).unwrap();
|
||||||
fs::write(&path, s).await.unwrap();
|
fs::write(&path, s).await.unwrap();
|
||||||
@ -131,7 +123,7 @@ impl Profile {
|
|||||||
|
|
||||||
fn name_from_path(path: impl AsRef<Path>) -> Option<(Game, String)> {
|
fn name_from_path(path: impl AsRef<Path>) -> Option<(Game, String)> {
|
||||||
let regex = regex::Regex::new(
|
let regex = regex::Regex::new(
|
||||||
r"profile-([^\-]+)-([^\-]+)\.json"
|
r"^profile-([^\-]+)-(.+)$"
|
||||||
).expect("Invalid regex");
|
).expect("Invalid regex");
|
||||||
|
|
||||||
let fname = path.as_ref().file_name().unwrap_or_default().to_string_lossy();
|
let fname = path.as_ref().file_name().unwrap_or_default().to_string_lossy();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{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;
|
||||||
@ -12,19 +13,22 @@ static CREATE_NO_WINDOW: u32 = 0x08000000;
|
|||||||
pub async fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
pub async fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
let ini_path = p.dir().join("segatools.ini");
|
let ini_path = p.data_dir().join("segatools.ini");
|
||||||
|
|
||||||
log::debug!("With path {}", ini_path.to_string_lossy());
|
log::debug!("With path {}", ini_path.to_string_lossy());
|
||||||
|
|
||||||
let mut game_builder;
|
let mut game_builder;
|
||||||
let mut amd_builder;
|
let mut amd_builder;
|
||||||
|
|
||||||
|
let target_path = PathBuf::from(p.get_str("target-path", ""));
|
||||||
|
let exe_dir = target_path.parent().ok_or_else(|| anyhow!("Invalid target path"))?;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let display_info = crate::display::prepare_display(p).await?;
|
let display_info = crate::display::prepare_display(p).await?;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
game_builder = Command::new(p.data.exe_dir.join("inject.exe"));
|
game_builder = Command::new(exe_dir.join("inject.exe"));
|
||||||
amd_builder = Command::new("cmd.exe");
|
amd_builder = Command::new("cmd.exe");
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@ -35,7 +39,7 @@ pub async fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
game_builder = Command::new(&wine);
|
game_builder = Command::new(&wine);
|
||||||
amd_builder = Command::new(&wine);
|
amd_builder = Command::new(&wine);
|
||||||
|
|
||||||
game_builder.arg(p.data.exe_dir.join("inject.exe"));
|
game_builder.arg(exe_dir.join("inject.exe"));
|
||||||
amd_builder.arg("cmd.exe");
|
amd_builder.arg("cmd.exe");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,10 +49,10 @@ pub async fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
"SEGATOOLS_CONFIG_PATH",
|
"SEGATOOLS_CONFIG_PATH",
|
||||||
&ini_path,
|
&ini_path,
|
||||||
)
|
)
|
||||||
.current_dir(&p.data.exe_dir)
|
.current_dir(&exe_dir)
|
||||||
.args([
|
.args([
|
||||||
"/C",
|
"/C",
|
||||||
&util::path_to_str(p.data.exe_dir.join("inject.exe"))?, "-d", "-k", "mu3hook.dll",
|
&util::path_to_str(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
|
||||||
@ -58,9 +62,9 @@ pub async fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
)
|
)
|
||||||
.env(
|
.env(
|
||||||
"INOHARA_CONFIG_PATH",
|
"INOHARA_CONFIG_PATH",
|
||||||
p.dir().join("inohara.cfg"),
|
p.config_dir().join("inohara.cfg"),
|
||||||
)
|
)
|
||||||
.current_dir(&p.data.exe_dir)
|
.current_dir(&exe_dir)
|
||||||
.args([
|
.args([
|
||||||
"-d", "-k", "mu3hook.dll",
|
"-d", "-k", "mu3hook.dll",
|
||||||
"mu3.exe", "-monitor 1",
|
"mu3.exe", "-monitor 1",
|
||||||
@ -86,8 +90,8 @@ pub async fn start(p: &Profile, app: AppHandle) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let amd_log = File::create(p.dir().join("amdaemon.log"))?;
|
let amd_log = File::create(p.data_dir().join("amdaemon.log"))?;
|
||||||
let game_log = File::create(p.dir().join("mu3.log"))?;
|
let game_log = File::create(p.data_dir().join("mu3.log"))?;
|
||||||
|
|
||||||
amd_builder
|
amd_builder
|
||||||
.stdout(Stdio::from(amd_log));
|
.stdout(Stdio::from(amd_log));
|
||||||
|
@ -1,26 +1,63 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use directories::ProjectDirs;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::{Path, PathBuf};
|
use tauri::{AppHandle, Manager};
|
||||||
|
use std::{path::{Path, PathBuf}, sync::OnceLock};
|
||||||
|
|
||||||
pub fn get_dirs() -> ProjectDirs {
|
#[cfg(not(target_os = "windows"))]
|
||||||
ProjectDirs::from("org", "7EVENDAYSHOLIDAYS", "STARTLINER")
|
static NAME: &str = "startliner";
|
||||||
.expect("Unable to set up config directories")
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
static NAME: &str = "STARTLINER";
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Dirs {
|
||||||
|
config_dir: PathBuf,
|
||||||
|
data_dir: PathBuf,
|
||||||
|
cache_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config_dir() -> PathBuf {
|
static DIRS: OnceLock<Dirs> = OnceLock::new();
|
||||||
get_dirs().config_dir().to_owned()
|
|
||||||
|
pub fn init_dirs(apph: &AppHandle) {
|
||||||
|
DIRS.get_or_init(|| {
|
||||||
|
if cfg!(windows) {
|
||||||
|
Dirs {
|
||||||
|
config_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME).join("cfg"),
|
||||||
|
data_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME).join("data"),
|
||||||
|
cache_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Dirs {
|
||||||
|
config_dir: apph.path().config_dir().expect("Unable to set project directories").join(NAME),
|
||||||
|
data_dir: apph.path().data_dir().expect("Unable to set project directories").join(NAME),
|
||||||
|
cache_dir: apph.path().cache_dir().expect("Unable to set project directories").join(NAME),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all_dirs() -> &'static Dirs {
|
||||||
|
&DIRS.get().expect("Directories uninitialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config_dir() -> &'static Path {
|
||||||
|
&DIRS.get().expect("Directories uninitialized").config_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data_dir() -> &'static Path {
|
||||||
|
&DIRS.get().expect("Directories uninitialized").data_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_dir() -> &'static Path {
|
||||||
|
&DIRS.get().expect("Directories uninitialized").cache_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pkg_dir() -> PathBuf {
|
pub fn pkg_dir() -> PathBuf {
|
||||||
get_dirs().data_dir().join("pkg").to_owned()
|
data_dir().join("pkg")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pkg_dir_of(namespace: &str, name: &str) -> PathBuf {
|
pub fn pkg_dir_of(namespace: &str, name: &str) -> PathBuf {
|
||||||
pkg_dir().join(format!("{}-{}", namespace, name)).to_owned()
|
pkg_dir().join(format!("{}-{}", namespace, name))
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_dir() -> PathBuf {
|
|
||||||
get_dirs().cache_dir().to_owned()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path_to_str(p: impl AsRef<Path>) -> Result<String> {
|
pub fn path_to_str(p: impl AsRef<Path>) -> Result<String> {
|
||||||
|
@ -11,10 +11,13 @@ import ModStore from './ModStore.vue';
|
|||||||
import OptionList from './OptionList.vue';
|
import OptionList from './OptionList.vue';
|
||||||
import ProfileList from './ProfileList.vue';
|
import ProfileList from './ProfileList.vue';
|
||||||
import StartButton from './StartButton.vue';
|
import StartButton from './StartButton.vue';
|
||||||
import { usePkgStore, usePrfStore } from '../stores';
|
import { invoke } from '../invoke';
|
||||||
|
import { useGeneralStore, usePkgStore, usePrfStore } from '../stores';
|
||||||
|
import { Dirs } from '../types';
|
||||||
|
|
||||||
const pkg = usePkgStore();
|
const pkg = usePkgStore();
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
|
const general = useGeneralStore();
|
||||||
|
|
||||||
pkg.setupListeners();
|
pkg.setupListeners();
|
||||||
|
|
||||||
@ -23,6 +26,10 @@ const currentTab = ref('3');
|
|||||||
const isProfileDisabled = computed(() => prf.current === null);
|
const isProfileDisabled = computed(() => prf.current === null);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
invoke('list_directories').then((d) => {
|
||||||
|
general.dirs = d as Dirs;
|
||||||
|
});
|
||||||
|
|
||||||
await prf.reloadList();
|
await prf.reloadList();
|
||||||
await prf.reload();
|
await prf.reload();
|
||||||
|
|
||||||
@ -65,27 +72,10 @@ onMounted(async () => {
|
|||||||
<OptionList />
|
<OptionList />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value="3">
|
<TabPanel value="3">
|
||||||
<strong>UNDER CONSTRUCTION</strong><br />Many features are
|
<strong>UNDER CONSTRUCTION</strong><br />Some features are
|
||||||
missing.<br />Existing features are expected to break any
|
missing.<br />Existing features are expected to break any
|
||||||
time.
|
time.
|
||||||
<div v-if="isProfileDisabled">
|
|
||||||
<br />Select <code>mu3.exe</code> to create a profile:
|
|
||||||
</div>
|
|
||||||
<ProfileList />
|
<ProfileList />
|
||||||
<div v-if="isProfileDisabled">
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
margin-top: 5px;
|
|
||||||
font-weight: bolder;
|
|
||||||
font-size: 3em;
|
|
||||||
color: red;
|
|
||||||
line-height: 1.1em;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
segatools 2024-12-23 or later has to be installed
|
|
||||||
</div>
|
|
||||||
(this will change in the future)
|
|
||||||
</div>
|
|
||||||
<img
|
<img
|
||||||
v-if="prf.current?.game === 'ongeki'"
|
v-if="prf.current?.game === 'ongeki'"
|
||||||
src="/sticker-ongeki.svg"
|
src="/sticker-ongeki.svg"
|
||||||
@ -97,13 +87,23 @@ onMounted(async () => {
|
|||||||
class="fixed bottom-0 right-0"
|
class="fixed bottom-0 right-0"
|
||||||
/>
|
/>
|
||||||
<br /><br /><br />
|
<br /><br /><br />
|
||||||
<Button
|
<footer
|
||||||
style="position: fixed; left: 10px; bottom: 10px"
|
style="
|
||||||
icon="pi pi-discord"
|
position: fixed;
|
||||||
as="a"
|
left: 0px;
|
||||||
target="_blank"
|
bottom: 0px;
|
||||||
href="https://discord.gg/jxvzHjjEmc"
|
height: 40px;
|
||||||
/>
|
width: 100%;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
style="margin-left: 6px"
|
||||||
|
icon="pi pi-discord"
|
||||||
|
as="a"
|
||||||
|
target="_blank"
|
||||||
|
href="https://discord.gg/jxvzHjjEmc"
|
||||||
|
/>
|
||||||
|
</footer>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -4,7 +4,9 @@ import Button from 'primevue/button';
|
|||||||
import * as path from '@tauri-apps/api/path';
|
import * as path from '@tauri-apps/api/path';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||||
import { invoke } from '../invoke';
|
import { usePrfStore } from '../stores';
|
||||||
|
|
||||||
|
const prf = usePrfStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
filename: String,
|
filename: String,
|
||||||
@ -53,13 +55,10 @@ const filePick = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const profileDir: string = await invoke('get_current_profile_dir');
|
|
||||||
|
|
||||||
if (props.filename === undefined) {
|
if (props.filename === undefined) {
|
||||||
throw new Error('FileEditor without a filename');
|
throw new Error('FileEditor without a filename');
|
||||||
}
|
}
|
||||||
|
target_path.value = await path.join(await prf.configDir, props.filename);
|
||||||
target_path.value = await path.join(profileDir, props.filename);
|
|
||||||
await load(target_path.value);
|
await load(target_path.value);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
@ -5,7 +5,8 @@ import InputText from 'primevue/inputtext';
|
|||||||
import Select from 'primevue/select';
|
import Select from 'primevue/select';
|
||||||
import SelectButton from 'primevue/selectbutton';
|
import SelectButton from 'primevue/selectbutton';
|
||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
import { invoke as unproxied_invoke } from '@tauri-apps/api/core';
|
import * as path from '@tauri-apps/api/path';
|
||||||
|
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||||
import FileEditor from './FileEditor.vue';
|
import FileEditor from './FileEditor.vue';
|
||||||
import FilePicker from './FilePicker.vue';
|
import FilePicker from './FilePicker.vue';
|
||||||
import OptionCategory from './OptionCategory.vue';
|
import OptionCategory from './OptionCategory.vue';
|
||||||
@ -32,20 +33,6 @@ const hookList: Ref<{ title: string; value: string }[]> = ref([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
unproxied_invoke('read_profile_data', {
|
|
||||||
path: 'aime.txt',
|
|
||||||
})
|
|
||||||
.then((v: unknown) => {
|
|
||||||
if (typeof v === 'string') {
|
|
||||||
aimeCode.value = v;
|
|
||||||
} else {
|
|
||||||
aimeCode.value = '';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
aimeCode.value = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
invoke('list_platform_capabilities')
|
invoke('list_platform_capabilities')
|
||||||
.then(async (v: unknown) => {
|
.then(async (v: unknown) => {
|
||||||
if (Array.isArray(v)) {
|
if (Array.isArray(v)) {
|
||||||
@ -72,26 +59,33 @@ const aimeCodeModel = computed({
|
|||||||
async set(value: string) {
|
async set(value: string) {
|
||||||
aimeCode.value = value;
|
aimeCode.value = value;
|
||||||
if (value.match(/^[0-9]{20}$/)) {
|
if (value.match(/^[0-9]{20}$/)) {
|
||||||
await invoke('write_profile_data', {
|
const aime_path = await path.join(await prf.configDir, 'aime.txt');
|
||||||
path: 'aime.txt',
|
await writeTextFile(aime_path, aimeCode.value);
|
||||||
content: aimeCode.value,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const extraDisplayOptionsDisabled = computed(() => {
|
||||||
|
return prf.cfg('display', 'default').value === 'default';
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const aime_path = await path.join(await prf.configDir, 'aime.txt');
|
||||||
|
aimeCode.value = await readTextFile(aime_path);
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<OptionCategory title="General">
|
<OptionCategory title="General">
|
||||||
<!-- <OptionRow title="mu3.exe">
|
<OptionRow title="mu3.exe">
|
||||||
<FilePicker
|
<FilePicker
|
||||||
field="game-dir"
|
field="target-path"
|
||||||
default=""
|
default=""
|
||||||
:directory="false"
|
:directory="false"
|
||||||
promptname="mu3.exe"
|
promptname="mu3.exe"
|
||||||
extension="exe"
|
extension="exe"
|
||||||
></FilePicker>
|
></FilePicker>
|
||||||
</OptionRow> -->
|
</OptionRow>
|
||||||
<OptionRow title="mu3hook">
|
<OptionRow title="mu3hook">
|
||||||
<Select
|
<Select
|
||||||
:model-value="prf.cfg('hook', 'segatools-mu3hook')"
|
:model-value="prf.cfg('hook', 'segatools-mu3hook')"
|
||||||
@ -135,17 +129,14 @@ const aimeCodeModel = computed({
|
|||||||
option-value="value"
|
option-value="value"
|
||||||
></Select>
|
></Select>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow id="resolution" title="Game resolution">
|
<OptionRow class="number-input" title="Game resolution">
|
||||||
<InputNumber
|
<InputNumber
|
||||||
class="shrink"
|
class="shrink"
|
||||||
size="small"
|
size="small"
|
||||||
:min="480"
|
:min="480"
|
||||||
:max="9999"
|
:max="9999"
|
||||||
:use-grouping="false"
|
:use-grouping="false"
|
||||||
:model-value="
|
:model-value="prf.cfgAny('rez-w', 1080)"
|
||||||
// prettier-ignore Because primevue fucked up
|
|
||||||
prf.cfg('rez-w', 1080) as any
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
x
|
x
|
||||||
<InputNumber
|
<InputNumber
|
||||||
@ -154,10 +145,7 @@ const aimeCodeModel = computed({
|
|||||||
:min="640"
|
:min="640"
|
||||||
:max="9999"
|
:max="9999"
|
||||||
:use-grouping="false"
|
:use-grouping="false"
|
||||||
:model-value="
|
:model-value="prf.cfgAny('rez-h', 1920)"
|
||||||
// prettier-ignore
|
|
||||||
prf.cfg('rez-h', 1920) as any
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow title="Display mode">
|
<OptionRow title="Display mode">
|
||||||
@ -185,7 +173,18 @@ const aimeCodeModel = computed({
|
|||||||
]"
|
]"
|
||||||
option-label="title"
|
option-label="title"
|
||||||
option-value="value"
|
option-value="value"
|
||||||
:disabled="prf.cfg('display', 'default').value === 'default'"
|
:disabled="extraDisplayOptionsDisabled"
|
||||||
|
/>
|
||||||
|
</OptionRow>
|
||||||
|
<OptionRow class="number-input" title="Refresh Rate">
|
||||||
|
<InputNumber
|
||||||
|
class="shrink"
|
||||||
|
size="small"
|
||||||
|
:min="60"
|
||||||
|
:max="999"
|
||||||
|
:use-grouping="false"
|
||||||
|
:model-value="prf.cfgAny('frequency', 60)"
|
||||||
|
:disabled="extraDisplayOptionsDisabled"
|
||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow
|
<OptionRow
|
||||||
@ -195,7 +194,7 @@ const aimeCodeModel = computed({
|
|||||||
<!-- @vue-expect-error -->
|
<!-- @vue-expect-error -->
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
:disabled="
|
:disabled="
|
||||||
prf.cfg('display', 'default').value === 'default' ||
|
extraDisplayOptionsDisabled ||
|
||||||
prf.cfg('display-mode', 'borderless').value != 'borderless'
|
prf.cfg('display-mode', 'borderless').value != 'borderless'
|
||||||
"
|
"
|
||||||
:model-value="prf.cfg('borderless-fullscreen', false)"
|
:model-value="prf.cfg('borderless-fullscreen', false)"
|
||||||
@ -209,10 +208,7 @@ const aimeCodeModel = computed({
|
|||||||
size="small"
|
size="small"
|
||||||
:maxlength="40"
|
:maxlength="40"
|
||||||
placeholder="192.168.1.234"
|
placeholder="192.168.1.234"
|
||||||
:model-value="
|
:model-value="prf.cfgAny<string>('dns-default', '')"
|
||||||
// prettier-ignore
|
|
||||||
prf.cfg<string>('dns-default', '') as any
|
|
||||||
"
|
|
||||||
/> </OptionRow
|
/> </OptionRow
|
||||||
><OptionRow title="Keychip">
|
><OptionRow title="Keychip">
|
||||||
<InputText
|
<InputText
|
||||||
@ -220,10 +216,7 @@ const aimeCodeModel = computed({
|
|||||||
size="small"
|
size="small"
|
||||||
:maxlength="16"
|
:maxlength="16"
|
||||||
placeholder="A123-01234567890"
|
placeholder="A123-01234567890"
|
||||||
:model-value="
|
:model-value="prf.cfgAny('keychip', '')"
|
||||||
// prettier-ignore
|
|
||||||
prf.cfg('keychip', '') as any
|
|
||||||
"
|
|
||||||
/> </OptionRow
|
/> </OptionRow
|
||||||
><OptionRow title="Subnet">
|
><OptionRow title="Subnet">
|
||||||
<InputText
|
<InputText
|
||||||
@ -231,10 +224,7 @@ const aimeCodeModel = computed({
|
|||||||
size="small"
|
size="small"
|
||||||
:maxlength="15"
|
:maxlength="15"
|
||||||
placeholder="192.168.1.0"
|
placeholder="192.168.1.0"
|
||||||
:model-value="
|
:model-value="prf.cfgAny('subnet', '')"
|
||||||
// prettier-ignore
|
|
||||||
prf.cfg('subnet', '') as any
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
</OptionCategory>
|
</OptionCategory>
|
||||||
@ -271,7 +261,7 @@ const aimeCodeModel = computed({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#resolution .p-inputnumber-input {
|
.number-input .p-inputnumber-input {
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,11 +9,17 @@ const prf = usePrfStore();
|
|||||||
<div class="mt-4 flex flex-wrap align-middle gap-4">
|
<div class="mt-4 flex flex-wrap align-middle gap-4">
|
||||||
<Button
|
<Button
|
||||||
:disabled="prf.list.length > 0"
|
:disabled="prf.list.length > 0"
|
||||||
label="Create profile"
|
label="O.N.G.E.K.I. profile"
|
||||||
icon="pi pi-plus"
|
icon="pi pi-plus"
|
||||||
aria-label="open-executable"
|
class="ongeki-button"
|
||||||
class="create-button"
|
@click="() => prf.create('ongeki')"
|
||||||
@click="prf.prompt"
|
/>
|
||||||
|
<Button
|
||||||
|
:disabled="prf.list.length > 0"
|
||||||
|
label="CHUNITHM profile"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
class="chunithm-button"
|
||||||
|
@click="() => prf.create('chunithm')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-for="p in prf.list">
|
<div v-for="p in prf.list">
|
||||||
@ -51,7 +57,7 @@ const prf = usePrfStore();
|
|||||||
.ongeki-button {
|
.ongeki-button {
|
||||||
background-color: var(--p-pink-400);
|
background-color: var(--p-pink-400);
|
||||||
border-color: var(--p-pink-400);
|
border-color: var(--p-pink-400);
|
||||||
width: 10em;
|
width: 14em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ongeki-button:hover,
|
.ongeki-button:hover,
|
||||||
@ -63,7 +69,7 @@ const prf = usePrfStore();
|
|||||||
.chunithm-button {
|
.chunithm-button {
|
||||||
background-color: var(--p-yellow-400);
|
background-color: var(--p-yellow-400);
|
||||||
border-color: var(--p-yellow-400);
|
border-color: var(--p-yellow-400);
|
||||||
width: 10em;
|
width: 14em;
|
||||||
}
|
}
|
||||||
.chunithm-button:hover,
|
.chunithm-button:hover,
|
||||||
.chunithm-button:active {
|
.chunithm-button:active {
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, ref } from 'vue';
|
import { Ref, computed, ref } from 'vue';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { invoke } from '../invoke';
|
import { invoke } from '../invoke';
|
||||||
|
import { usePrfStore } from '../stores';
|
||||||
|
|
||||||
|
const prf = usePrfStore();
|
||||||
|
|
||||||
type StartStatus = 'ready' | 'preparing' | 'running';
|
type StartStatus = 'ready' | 'preparing' | 'running';
|
||||||
const startStatus: Ref<StartStatus> = ref('ready');
|
const startStatus: Ref<StartStatus> = ref('ready');
|
||||||
@ -21,6 +24,16 @@ const kill = async () => {
|
|||||||
startStatus.value = 'ready';
|
startStatus.value = 'ready';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const disabledTooltip = computed(() => {
|
||||||
|
if (prf.cfg('target-path', '').value.length === 0) {
|
||||||
|
return 'The game path must be specified';
|
||||||
|
}
|
||||||
|
if (prf.cfg('amfs', '').value.length === 0) {
|
||||||
|
return 'The amfs path must be specified';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
listen('launch-start', () => {
|
listen('launch-start', () => {
|
||||||
startStatus.value = 'running';
|
startStatus.value = 'running';
|
||||||
});
|
});
|
||||||
@ -33,7 +46,8 @@ listen('launch-end', () => {
|
|||||||
<template>
|
<template>
|
||||||
<Button
|
<Button
|
||||||
v-if="startStatus === 'ready'"
|
v-if="startStatus === 'ready'"
|
||||||
:disabled="false"
|
v-tooltip="disabledTooltip"
|
||||||
|
:disabled="disabledTooltip !== null"
|
||||||
icon="pi pi-play"
|
icon="pi pi-play"
|
||||||
label="START"
|
label="START"
|
||||||
aria-label="start"
|
aria-label="start"
|
||||||
|
@ -3,6 +3,7 @@ import { createPinia } from 'pinia';
|
|||||||
import { definePreset } from '@primevue/themes';
|
import { definePreset } from '@primevue/themes';
|
||||||
import Theme from '@primevue/themes/aura';
|
import Theme from '@primevue/themes/aura';
|
||||||
import PrimeVue from 'primevue/config';
|
import PrimeVue from 'primevue/config';
|
||||||
|
import Tooltip from 'primevue/tooltip';
|
||||||
import App from './components/App.vue';
|
import App from './components/App.vue';
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
@ -16,4 +17,5 @@ app.use(PrimeVue, {
|
|||||||
preset: Preset,
|
preset: Preset,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
app.directive('tooltip', Tooltip);
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
@ -1,15 +1,40 @@
|
|||||||
import { Ref, computed, ref } from 'vue';
|
import { Ref, computed, ref } from 'vue';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { open } from '@tauri-apps/plugin-dialog';
|
import * as path from '@tauri-apps/api/path';
|
||||||
import { invoke } from './invoke';
|
import { invoke } from './invoke';
|
||||||
import { Game, Package, Profile, ProfileMeta } from './types';
|
import { Dirs, Game, Package, Profile, ProfileMeta } from './types';
|
||||||
import { changePrimaryColor, pkgKey } from './util';
|
import { changePrimaryColor, pkgKey } from './util';
|
||||||
|
|
||||||
type InstallStatus = {
|
type InstallStatus = {
|
||||||
pkg: string;
|
pkg: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGeneralStore = defineStore('general', () => {
|
||||||
|
const dirs: Ref<Dirs | null> = ref(null);
|
||||||
|
|
||||||
|
const configDir = computed(() => {
|
||||||
|
if (dirs.value === null) {
|
||||||
|
throw new Error('Invalid directory access');
|
||||||
|
}
|
||||||
|
return dirs.value.config_dir;
|
||||||
|
});
|
||||||
|
const dataDir = computed(() => {
|
||||||
|
if (dirs.value === null) {
|
||||||
|
throw new Error('Invalid directory access');
|
||||||
|
}
|
||||||
|
return dirs.value.data_dir;
|
||||||
|
});
|
||||||
|
const cacheDir = computed(() => {
|
||||||
|
if (dirs.value === null) {
|
||||||
|
throw new Error('Invalid directory access');
|
||||||
|
}
|
||||||
|
return dirs.value.cache_dir;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { dirs, configDir, dataDir, cacheDir };
|
||||||
|
});
|
||||||
|
|
||||||
export const usePkgStore = defineStore('pkg', {
|
export const usePkgStore = defineStore('pkg', {
|
||||||
state: (): { pkg: { [key: string]: Package } } => {
|
state: (): { pkg: { [key: string]: Package } } => {
|
||||||
return {
|
return {
|
||||||
@ -113,25 +138,15 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const prompt = async () => {
|
// Hack around PrimeVu not supporting WritableComputedRef
|
||||||
const exePath = await open({
|
const cfgAny = <T extends string | boolean | number>(
|
||||||
multiple: false,
|
key: string,
|
||||||
directory: false,
|
dflt: T
|
||||||
filters: [
|
) => cfg(key, dflt) as any;
|
||||||
{
|
|
||||||
name: 'mu3.exe or chusanApp.exe',
|
|
||||||
extensions: ['exe'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
if (exePath !== null) {
|
|
||||||
await create(exePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const create = async (exePath: string) => {
|
const create = async (game: Game) => {
|
||||||
try {
|
try {
|
||||||
await invoke('init_profile', { exePath });
|
await invoke('init_profile', { game, name: 'new-profile' });
|
||||||
await reload();
|
await reload();
|
||||||
await reloadList();
|
await reloadList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -173,6 +188,15 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
await save();
|
await save();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generalStore = useGeneralStore();
|
||||||
|
|
||||||
|
const configDir = computed(async () => {
|
||||||
|
return await path.join(
|
||||||
|
generalStore.configDir,
|
||||||
|
`profile-${current.value?.game}-${current.value?.name}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
listen<InstallStatus>('install-end', async () => {
|
listen<InstallStatus>('install-end', async () => {
|
||||||
await reload();
|
await reload();
|
||||||
});
|
});
|
||||||
@ -184,10 +208,11 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
reload,
|
reload,
|
||||||
save,
|
save,
|
||||||
cfg,
|
cfg,
|
||||||
prompt,
|
cfgAny,
|
||||||
create,
|
create,
|
||||||
switchTo,
|
switchTo,
|
||||||
reloadList,
|
reloadList,
|
||||||
togglePkg,
|
togglePkg,
|
||||||
|
configDir,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -28,8 +28,13 @@ export interface ProfileMeta {
|
|||||||
|
|
||||||
export interface Profile extends ProfileMeta {
|
export interface Profile extends ProfileMeta {
|
||||||
data: {
|
data: {
|
||||||
exe_dir: string;
|
|
||||||
mods: string[];
|
mods: string[];
|
||||||
cfg: { [key: string]: string | boolean | number };
|
cfg: { [key: string]: string | boolean | number };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Dirs {
|
||||||
|
config_dir: string;
|
||||||
|
data_dir: string;
|
||||||
|
cache_dir: string;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user