feat: another settings category, launch prefixes, backup&restore and more

This commit is contained in:
neoapps-dev
2026-05-26 19:15:56 +03:00
parent 3525bd6523
commit 0c1b2beced
14 changed files with 896 additions and 68 deletions
+36
View File
@@ -53,3 +53,39 @@ pub fn import_theme(app: AppHandle) -> Result<String, String> {
Err("CANCELED".into())
}
}
#[tauri::command]
pub fn export_settings(app: AppHandle) -> Result<(), String> {
let config = config::load_config_raw(app.clone());
let json = serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?;
let file = rfd::FileDialog::new()
.add_filter("Emerald Settings", &["json"])
.set_title("Export Launcher Settings")
.set_file_name("emerald_settings.json")
.save_file();
if let Some(path) = file {
fs::write(&path, &json).map_err(|e| e.to_string())?;
Ok(())
} else {
Err("CANCELED".into())
}
}
#[tauri::command]
pub fn import_settings(app: AppHandle) -> Result<String, String> {
let file = rfd::FileDialog::new()
.add_filter("Emerald Settings", &["json"])
.set_title("Import Launcher Settings")
.pick_file();
if let Some(path) = file {
let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
let config: AppConfig =
serde_json::from_str(&content).map_err(|_| "Invalid settings JSON format".to_string())?;
config::save_config_raw(&app, &config);
Ok("Settings imported successfully! Restart the launcher to apply.".into())
} else {
Err("CANCELED".into())
}
}
+145 -22
View File
@@ -5,11 +5,12 @@ use tauri::{AppHandle, Manager, State};
use tauri_plugin_opener::OpenerExt;
use crate::commands::runners;
use crate::config;
use crate::types::AppConfig;
#[cfg(target_os = "macos")]
use crate::platform::macos;
#[cfg(unix)]
use crate::platform::linux;
use crate::playtime::{self, PlaytimeResponse};
use crate::playtime::{self, PlaytimeResponse, PlaytimeDayEntry};
use crate::state::GameState;
use crate::types::McServer;
use crate::util;
@@ -53,9 +54,9 @@ pub async fn launch_game(
#[cfg(target_os = "linux")]
{
if let Some(runner_id) = config_val.linux_runner {
if let Some(ref runner_id) = config_val.linux_runner {
let runners_list = runners::get_available_runners(app.clone());
if let Some(runner) = runners_list.into_iter().find(|r| r.id == runner_id) {
if let Some(runner) = runners_list.into_iter().find(|r| r.id == *runner_id) {
let is_proton = runner.r#type == "proton";
let program = if is_proton {
PathBuf::from(&runner.path).join("proton").to_string_lossy().to_string()
@@ -81,11 +82,16 @@ pub async fn launch_game(
(&program, &[])
};
let mut cmd = tokio::process::Command::new(prog);
let mut all_args: Vec<String> = Vec::new();
for a in runner_args {
cmd.arg(a);
all_args.push(a.to_string());
}
for a in &args {
all_args.extend(args.clone());
all_args.push(game_exe.to_string_lossy().to_string());
all_args.extend(extra_args.clone());
let (final_prog, final_args) = apply_launch_prefix(prog, all_args, &config_val);
let mut cmd = tokio::process::Command::new(&final_prog);
for a in &final_args {
cmd.arg(a);
}
@@ -110,10 +116,7 @@ pub async fn launch_game(
cmd.env_remove("QT_PLUGIN_PATH");
}
cmd.arg(&game_exe);
for a in &extra_args {
cmd.arg(a);
}
apply_launch_env_vars(&mut cmd, &config_val);
cmd.current_dir(&working_dir);
let playtime_start = std::time::Instant::now();
let child = cmd.spawn().map_err(|e| e.to_string())?;
@@ -179,25 +182,30 @@ pub async fn launch_game(
.map(|pp| pp.to_path_buf())
.ok_or_else(|| "Unable to locate wine bin directory inside runtime.".to_string())?;
let mut cmd = if let Some(wrapper) = gptk_no_hud {
let (mac_prog, mut mac_args): (String, Vec<String>) = if let Some(ref wrapper) = gptk_no_hud {
let win_path = util::unix_path_to_wine_z_path(&game_exe);
let mut c = tokio::process::Command::new(wrapper);
c.arg(&prefix_dir);
c.arg(win_path);
c
(wrapper.to_string_lossy().to_string(), vec![
prefix_dir.to_string_lossy().to_string(),
win_path,
])
} else {
let mut c = tokio::process::Command::new(&wine_binary);
c.env("WINEPREFIX", &prefix_dir);
c.arg(&game_exe);
c
(wine_binary.to_string_lossy().to_string(), vec![
game_exe.to_string_lossy().to_string(),
])
};
for a in &extra_args {
mac_args.extend(extra_args.clone());
let (final_prog, final_args) = apply_launch_prefix(&mac_prog, mac_args, &config_val);
let mut cmd = tokio::process::Command::new(&final_prog);
for a in &final_args {
cmd.arg(a);
}
#[cfg(unix)]
cmd.process_group(0);
apply_launch_env_vars(&mut cmd, &config_val);
cmd.current_dir(&working_dir);
cmd.env("WINEPREFIX", &prefix_dir);
cmd.env("WINEDEBUG", "-all");
@@ -270,12 +278,16 @@ pub async fn launch_game(
#[cfg(all(not(target_os = "macos"), not(target_os = "linux")))]
{
let mut cmd = tokio::process::Command::new(&game_exe);
for a in &extra_args {
let exe_str = game_exe.to_string_lossy().to_string();
let all_args: Vec<String> = extra_args.clone();
let (final_prog, final_args) = apply_launch_prefix(&exe_str, all_args, &config_val);
let mut cmd = tokio::process::Command::new(&final_prog);
for a in &final_args {
cmd.arg(a);
}
#[cfg(unix)]
cmd.process_group(0);
apply_launch_env_vars(&mut cmd, &config_val);
cmd.current_dir(&working_dir);
let playtime_start = std::time::Instant::now();
let child = cmd.spawn().map_err(|e| e.to_string())?;
@@ -386,6 +398,93 @@ pub fn get_playtime(app: AppHandle, instance_id: String) -> PlaytimeResponse {
playtime::get_playtime(&app, &instance_id)
}
#[tauri::command]
pub fn get_playtime_daily(app: AppHandle, instance_id: String, days: u64) -> Vec<PlaytimeDayEntry> {
playtime::get_playtime_daily(&app, &instance_id, days)
}
#[tauri::command]
pub fn backup_instance(app: AppHandle, instance_id: String) -> Result<(), String> {
let instance_dir = util::get_instance_working_dir(&app, &instance_id);
if !instance_dir.exists() {
return Err("Instance not found".into());
}
let backup_name = format!("{}_backup.tar.gz", instance_id);
let file = rfd::FileDialog::new()
.set_title("Save Instance Backup")
.set_file_name(&backup_name)
.add_filter("Tar Gzip Archive", &["tar.gz"])
.save_file();
if let Some(path) = file {
let parent = instance_dir.parent().ok_or("Invalid instance path")?;
let dir_name = instance_dir.file_name().ok_or("Invalid dir name")?;
let status = std::process::Command::new("tar")
.args(["-czf", path.to_str().unwrap(), "-C", parent.to_str().unwrap(), dir_name.to_str().unwrap()])
.status()
.map_err(|e| format!("Failed to create backup: {}", e))?;
if status.success() {
Ok(())
} else {
Err("Backup command failed".into())
}
} else {
Err("CANCELED".into())
}
}
#[tauri::command]
pub fn restore_instance(app: AppHandle) -> Result<String, String> {
let file = rfd::FileDialog::new()
.set_title("Restore Instance Backup")
.add_filter("Tar Gzip Archive", &["tar.gz"])
.add_filter("Zip Archive", &["zip"])
.pick_file();
if let Some(path) = file {
let instances_dir = util::get_app_dir(&app).join("instances");
std::fs::create_dir_all(&instances_dir).map_err(|e| e.to_string())?;
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
let status = if ext == "zip" {
std::process::Command::new("unzip")
.arg("-o")
.arg(path.to_str().unwrap())
.arg("-d")
.arg(instances_dir.to_str().unwrap())
.status()
.map_err(|e| format!("Failed to extract backup: {}", e))?
} else {
std::process::Command::new("tar")
.args(["-xzf", path.to_str().unwrap(), "-C", instances_dir.to_str().unwrap()])
.status()
.map_err(|e| format!("Failed to extract backup: {}", e))?
};
if status.success() {
let extracted: Vec<_> = std::fs::read_dir(&instances_dir)
.map_err(|e| e.to_string())?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir() && e.path().join("Minecraft.Client.exe").exists())
.collect();
if let Some(new_instance) = extracted.into_iter().next() {
let id = new_instance.file_name().to_string_lossy().to_string();
Ok(id)
} else {
Ok("restored".into())
}
} else {
Err("Restore command failed".into())
}
} else {
Err("CANCELED".into())
}
}
pub fn ensure_server_list(instance_dir: &PathBuf, servers: Vec<McServer>) {
let servers_db = instance_dir.join("servers.db");
let mut all_servers = Vec::new();
@@ -503,6 +602,30 @@ fn perform_dlc_sync(app: &AppHandle, instance_dir: &PathBuf) -> Result<(), Strin
}
}
fn apply_launch_prefix(program: &str, args: Vec<String>, config: &AppConfig) -> (String, Vec<String>) {
if let Some(ref prefix) = config.launch_prefix {
let trimmed = prefix.trim();
if !trimmed.is_empty() {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if !parts.is_empty() {
let mut all_args: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
all_args.push(program.to_string());
all_args.extend(args);
return (parts[0].to_string(), all_args);
}
}
}
(program.to_string(), args)
}
fn apply_launch_env_vars(cmd: &mut tokio::process::Command, config: &AppConfig) {
if let Some(ref env_vars) = config.launch_env_vars {
for (k, v) in env_vars {
cmd.env(k, v);
}
}
}
async fn perform_instance_sync(app: &AppHandle, instance_id: &str) -> Result<(), String> {
let target_dir = util::get_instance_working_dir(app, instance_id);
if !target_dir.exists() {
+3
View File
@@ -30,6 +30,9 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig {
legacy_mode: Some(false),
mangohud_enabled: None,
saved_servers: None,
extra_launch_args: None,
launch_prefix: None,
launch_env_vars: None,
}
}
+5
View File
@@ -57,6 +57,8 @@ pub fn run() {
runners::get_available_runners,
config_cmds::get_external_palettes,
config_cmds::import_theme,
config_cmds::export_settings,
config_cmds::import_settings,
file_dialogs::pick_folder,
download::download_runner,
game::delete_instance,
@@ -82,6 +84,9 @@ pub fn run() {
proxy_cmd::http_proxy_request,
game::get_instance_path,
game::get_playtime,
game::get_playtime_daily,
game::backup_instance,
game::restore_instance,
commands::console2lce::import_world,
stun::stun_discover,
direct::start_direct_proxy,
+43
View File
@@ -72,3 +72,46 @@ pub fn get_playtime(app: &AppHandle, instance_id: &str) -> PlaytimeResponse {
PlaytimeResponse { total_seconds, week_seconds, day_seconds }
}
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PlaytimeDayEntry {
pub label: String,
pub seconds: u64,
}
pub fn get_playtime_daily(app: &AppHandle, instance_id: &str, days: u64) -> Vec<PlaytimeDayEntry> {
let data = load(app);
let sessions = data.sessions.get(instance_id);
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let day_secs = 24 * 60 * 60;
let mut entries: Vec<(u64, u64)> = (0..days).map(|d| {
let day_start = now - (d * day_secs) - (now % day_secs);
(day_start, 0)
}).collect();
if let Some(sessions) = sessions {
for session in sessions {
for entry in entries.iter_mut() {
let day_end = entry.0 + day_secs;
if session.start < day_end && session.end > entry.0 {
let overlap_start = std::cmp::max(session.start, entry.0);
let overlap_end = std::cmp::min(session.end, day_end);
if overlap_end > overlap_start {
entry.1 += overlap_end - overlap_start;
}
}
}
}
}
entries.reverse();
let day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
entries.into_iter().map(|(ts, secs)| {
let weekday = ((ts / day_secs) + 4) % 7;
PlaytimeDayEntry {
label: day_names[weekday as usize].to_string(),
seconds: secs,
}
}).collect()
}
+3
View File
@@ -45,6 +45,9 @@ pub struct AppConfig {
pub legacy_mode: Option<bool>,
pub mangohud_enabled: Option<bool>,
pub saved_servers: Option<Vec<McServer>>,
pub extra_launch_args: Option<Vec<String>>,
pub launch_prefix: Option<String>,
pub launch_env_vars: Option<std::collections::HashMap<String, String>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]