342 lines
12 KiB
Rust
342 lines
12 KiB
Rust
mod cmd;
|
|
mod model;
|
|
mod pkg;
|
|
mod pkg_store;
|
|
mod util;
|
|
mod download_handler;
|
|
mod appdata;
|
|
mod modules;
|
|
mod profiles;
|
|
mod patcher;
|
|
|
|
use std::sync::OnceLock;
|
|
use anyhow::anyhow;
|
|
use closure::closure;
|
|
use appdata::{AppData, ToggleAction};
|
|
use model::misc::Game;
|
|
use pkg::PkgKey;
|
|
use pkg_store::Payload;
|
|
use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent};
|
|
use tauri_plugin_deep_link::DeepLinkExt;
|
|
use tauri_plugin_cli::CliExt;
|
|
use tokio::{fs, sync::Mutex, try_join};
|
|
|
|
static EXIT_REQUESTED: OnceLock<()> = OnceLock::new();
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub async fn run(_args: Vec<String>) {
|
|
let tauri = tauri::Builder::default()
|
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
|
let _ = app
|
|
.get_webview_window("main")
|
|
.expect("No main window")
|
|
.set_focus();
|
|
if args.len() == 2 {
|
|
deep_link(app.clone(), args);
|
|
}
|
|
}))
|
|
.plugin(tauri_plugin_cli::init())
|
|
.plugin(tauri_plugin_fs::init())
|
|
.plugin(tauri_plugin_deep_link::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.plugin(tauri_plugin_shell::init())
|
|
.plugin(tauri_plugin_opener::init())
|
|
.setup(|app| {
|
|
let apph = app.handle();
|
|
|
|
util::init_dirs(&apph);
|
|
|
|
let mut app_data = AppData::new(app.handle().clone());
|
|
|
|
log::info!(
|
|
"running from {}",
|
|
std::env::current_dir()
|
|
.unwrap_or_default()
|
|
.to_str()
|
|
.unwrap_or_default()
|
|
);
|
|
|
|
let start_immediately;
|
|
|
|
if let Ok(matches) = app.cli().matches() {
|
|
let start_arg = matches.args.get("start").expect("Invalid argument configuration");
|
|
let game_arg = matches.args.get("game").expect("Invalid argument configuration");
|
|
let name_arg = matches.args.get("profile").expect("Invalid argument configuration");
|
|
log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg);
|
|
if start_arg.occurrences > 0 {
|
|
start_immediately = true;
|
|
} else {
|
|
open_window(apph.clone())?;
|
|
start_immediately = false;
|
|
}
|
|
|
|
if game_arg.occurrences == 1 && name_arg.occurrences == 1 {
|
|
let game = game_arg.value.as_str().unwrap();
|
|
let name = name_arg.value.as_str().unwrap();
|
|
|
|
app_data.switch_profile(
|
|
Game::from_str(game).ok_or_else(|| anyhow!("Invalid game"))?,
|
|
name.to_owned()
|
|
)?;
|
|
}
|
|
} else {
|
|
return Err(anyhow!("Invalid command line arguments").into());
|
|
}
|
|
|
|
app.manage(Mutex::new(app_data));
|
|
app.deep_link().register_all()?;
|
|
|
|
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| {
|
|
let raw = ev.payload();
|
|
log::debug!("download-end triggered: {}", raw);
|
|
let key = PkgKey(raw[1..raw.len()-1].to_owned());
|
|
let apph = apph.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let mutex = apph.state::<Mutex<AppData>>();
|
|
let mut appd = mutex.lock().await;
|
|
let res = appd.pkgs.install_package(&key, true, false).await;
|
|
log::debug!("download-end install {:?}", res);
|
|
});
|
|
}));
|
|
|
|
app.listen("launch-end", closure!(clone apph, |_| {
|
|
log::debug!("launch-end triggered");
|
|
let apph = apph.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let mutex = apph.state::<Mutex<AppData>>();
|
|
let appd = mutex.lock().await;
|
|
if !appd.state.remain_open {
|
|
apph.exit(0);
|
|
}
|
|
});
|
|
}));
|
|
|
|
app.listen("install-end-prelude", closure!(clone apph, |ev| {
|
|
let payload = serde_json::from_str::<Payload>(ev.payload());
|
|
log::debug!("install-end-prelude triggered: {:?}", payload);
|
|
let apph = apph.clone();
|
|
if let Ok(payload) = payload {
|
|
tauri::async_runtime::spawn(async move {
|
|
let mutex = apph.state::<Mutex<AppData>>();
|
|
let mut appd = mutex.lock().await;
|
|
let res = appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf);
|
|
log::debug!(
|
|
"install-end-prelude toggle {:?}",
|
|
res
|
|
);
|
|
use tauri::Emitter;
|
|
let res = apph.emit("install-end", payload);
|
|
log::debug!("install-end {:?}", res);
|
|
});
|
|
} else {
|
|
log::error!("install-end-prelude: invalid payload: {}", ev.payload());
|
|
}
|
|
}));
|
|
|
|
if start_immediately == true {
|
|
let apph = apph.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let mtx = apph.state::<Mutex<AppData>>();
|
|
{
|
|
let mut appd = mtx.lock().await;
|
|
if let Err(e) = appd.pkgs.reload_all().await {
|
|
log::error!("unable to reload packages: {}", e);
|
|
apph.exit(1);
|
|
}
|
|
}
|
|
if let Err(e) = cmd::startline(apph.clone(), false).await {
|
|
log::error!("unable to launch: {}", e);
|
|
_ = open_window(apph.clone());
|
|
// stupid but effective
|
|
std::thread::sleep(std::time::Duration::from_secs(3));
|
|
_ = apph.emit("launch-error", e.to_string());
|
|
} else {
|
|
let mut appd = mtx.lock().await;
|
|
appd.state.remain_open = false;
|
|
log::info!("started quietly");
|
|
}
|
|
});
|
|
} else {
|
|
let apph = apph.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
update(apph).await.unwrap();
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
cmd::start_check,
|
|
cmd::startline,
|
|
cmd::kill,
|
|
|
|
cmd::get_package,
|
|
cmd::get_all_packages,
|
|
cmd::get_game_packages,
|
|
cmd::reload_all_packages,
|
|
cmd::fetch_listings,
|
|
cmd::install_package,
|
|
cmd::delete_package,
|
|
cmd::toggle_package,
|
|
|
|
cmd::list_profiles,
|
|
cmd::init_profile,
|
|
cmd::load_profile,
|
|
cmd::rename_profile,
|
|
cmd::duplicate_profile,
|
|
cmd::delete_profile,
|
|
cmd::get_current_profile,
|
|
cmd::sync_current_profile,
|
|
cmd::save_current_profile,
|
|
cmd::load_segatools_ini,
|
|
cmd::create_shortcut,
|
|
|
|
cmd::get_global_config,
|
|
cmd::set_global_config,
|
|
|
|
cmd::list_displays,
|
|
cmd::list_platform_capabilities,
|
|
cmd::list_directories,
|
|
cmd::file_exists,
|
|
cmd::open_file,
|
|
cmd::get_changelog,
|
|
|
|
cmd::list_com_ports,
|
|
|
|
cmd::list_patches,
|
|
])
|
|
.build(tauri::generate_context!())
|
|
.expect("error while building tauri application");
|
|
|
|
tauri.run(move |app, event| {
|
|
match event {
|
|
RunEvent::ExitRequested { api, code, .. } => {
|
|
log::debug!("exit request {:?} {:?}", api, code);
|
|
if EXIT_REQUESTED.get().is_none() {
|
|
_= EXIT_REQUESTED.set(());
|
|
api.prevent_exit();
|
|
let app = app.clone();
|
|
tauri::async_runtime::spawn(async move {
|
|
let mutex = app.state::<Mutex<AppData>>();
|
|
let appd = mutex.lock().await;
|
|
if let Some(p) = &appd.profile {
|
|
let res = p.save();
|
|
log::debug!("save: {:?}", res);
|
|
app.exit(0);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
RunEvent::Exit => {
|
|
log::info!("exit");
|
|
}
|
|
_ => {}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn deep_link(app: AppHandle, args: Vec<String>) {
|
|
let url = &args[1];
|
|
let proto = "rainycolor://";
|
|
if &url[..proto.len()] == proto {
|
|
log::info!("deep link: {}", url);
|
|
|
|
let regex = regex::Regex::new(
|
|
r"rainycolor://v1/install/rainy\.patafour\.zip/([^/]+)/([^/]+)/[0-9]+\.[0-9]+\.[0-9]+/"
|
|
).expect("Invalid regex");
|
|
|
|
if let Some(caps) = regex.captures(url) {
|
|
if caps.len() == 3 {
|
|
let app = app.clone();
|
|
let key = PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()));
|
|
tauri::async_runtime::spawn(async move {
|
|
let mutex = app.state::<Mutex<AppData>>();
|
|
let mut appd = mutex.lock().await;
|
|
if appd.pkgs.is_offline() {
|
|
log::warn!("Deep link installation failed: offline");
|
|
} else if let Err(e) = appd.pkgs.install_package(&key, true, true).await {
|
|
log::warn!("Deep link installation failed: {}", e.to_string());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
|
|
let mutex = app.state::<Mutex<AppData>>();
|
|
{
|
|
let appd = mutex.lock().await;
|
|
if !appd.cfg.enable_autoupdates {
|
|
log::info!("skipping auto-update");
|
|
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
#[cfg(not(debug_assertions))]
|
|
{
|
|
use tauri_plugin_updater::UpdaterExt;
|
|
use tauri::Emitter;
|
|
if let Some(update) = app.updater()?.check().await? {
|
|
let mut downloaded = 0;
|
|
update.download_and_install(
|
|
|chunk_length, content_length| {
|
|
downloaded += chunk_length;
|
|
_ = app.emit("update-progress", (downloaded as f64) / (content_length.unwrap_or(u64::MAX) as f64));
|
|
},
|
|
|| {
|
|
log::info!("download finished");
|
|
},
|
|
)
|
|
.await?;
|
|
|
|
log::info!("update installed");
|
|
app.restart();
|
|
}
|
|
}
|
|
|
|
// One day I will write proper tests
|
|
// #[cfg(debug_assertions)]
|
|
// {
|
|
// use tauri::Emitter;
|
|
// std::thread::sleep(std::time::Duration::from_millis(5000));
|
|
// let mut downloaded = 0;
|
|
// while downloaded < 500 {
|
|
// std::thread::sleep(std::time::Duration::from_millis(10));
|
|
// downloaded += 1;
|
|
// _ = app.emit("update-progress", (downloaded as f32) / 500f32);
|
|
// }
|
|
// app.restart();
|
|
// }
|
|
|
|
log::info!("ending auto-update check");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn open_window(apph: AppHandle) -> anyhow::Result<()> {
|
|
let config = apph.config().clone();
|
|
tauri::WebviewWindowBuilder::new(&apph, "main", tauri::WebviewUrl::App("index.html".into()))
|
|
.title(format!("STARTLINER {}", config.version.unwrap_or_default()))
|
|
.inner_size(900f64, 600f64)
|
|
.min_inner_size(900f64, 600f64)
|
|
.build()?;
|
|
|
|
Ok(())
|
|
} |