From 725ac2187f2b24eba4192faeeab54295fb558fd1 Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 19:50:56 +0200 Subject: [PATCH 01/10] Implementing features --- src-tauri/src/features/instance_sync.rs | 51 +++++++++++++++++++++++++ src-tauri/src/features/mod.rs | 48 +++++++++++++++++++++++ src-tauri/src/launcher/mod.rs | 22 +++++++---- src-tauri/src/lib.rs | 1 + src-tauri/src/main.rs | 3 +- 5 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 src-tauri/src/features/instance_sync.rs create mode 100644 src-tauri/src/features/mod.rs diff --git a/src-tauri/src/features/instance_sync.rs b/src-tauri/src/features/instance_sync.rs new file mode 100644 index 0000000..6f9dc59 --- /dev/null +++ b/src-tauri/src/features/instance_sync.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; + +use lyceris::auth::microsoft::MinecraftAccount; + +use crate::{features::Feature, GameVersion}; + +pub struct InstanceSync {} + +impl Default for InstanceSync { + fn default() -> Self { + Self {} + } +} + +impl Feature for InstanceSync { + fn launch( + &mut self, + version: &GameVersion, + account: &MinecraftAccount, + ) -> super::FeatureResult { + println!("Launching {} for {}", version.id, account.username); + + Ok(()) + } + + fn after_launch( + &mut self, + version: &GameVersion, + account: &MinecraftAccount, + ) -> super::FeatureResult { + println!("Game launched!"); + + Ok(()) + } +} + +fn parse_options(content: &str) -> HashMap { + let mut map = HashMap::new(); + + for line in content.lines() { + if line.trim().is_empty() { + continue; + } + + if let Some((key, value)) = line.split_once(':') { + map.insert(key.to_string(), value.to_string()); + } + } + + map +} diff --git a/src-tauri/src/features/mod.rs b/src-tauri/src/features/mod.rs new file mode 100644 index 0000000..6730d77 --- /dev/null +++ b/src-tauri/src/features/mod.rs @@ -0,0 +1,48 @@ +use lyceris::auth::microsoft::MinecraftAccount; +use tokio::sync::Mutex; + +use crate::{features::instance_sync::InstanceSync, GameVersion}; + +mod instance_sync; + +pub type FeatureResult = Result<(), String>; + +#[allow(unused_variables)] +pub trait Feature: Send + Sync { + fn launch(&mut self, version: &GameVersion, account: &MinecraftAccount) -> FeatureResult { + Ok(()) + } + fn after_launch(&mut self, version: &GameVersion, account: &MinecraftAccount) -> FeatureResult { + Ok(()) + } +} + +pub struct FeatureState(Mutex>>); + +impl FeatureState { + pub fn new() -> Self { + Self(Mutex::new(vec![Box::new(InstanceSync::default())])) + } +} + +impl FeatureState { + pub async fn launch(&self, version: &GameVersion, account: &MinecraftAccount) -> FeatureResult { + for feature in self.0.lock().await.iter_mut() { + feature.launch(version, account)?; + } + + Ok(()) + } + + pub async fn after_launch( + &self, + version: &GameVersion, + account: &MinecraftAccount, + ) -> FeatureResult { + for feature in self.0.lock().await.iter_mut() { + feature.after_launch(version, account)?; + } + + Ok(()) + } +} diff --git a/src-tauri/src/launcher/mod.rs b/src-tauri/src/launcher/mod.rs index de96d04..e98091d 100644 --- a/src-tauri/src/launcher/mod.rs +++ b/src-tauri/src/launcher/mod.rs @@ -16,7 +16,7 @@ use lyceris::{ use tauri::{Emitter as _, State}; -use crate::{GameVersion, SharedState}; +use crate::{features::FeatureState, GameVersion, SharedState}; pub fn launcher_dir() -> PathBuf { if cfg!(target_os = "macos") { @@ -47,6 +47,7 @@ pub fn launcher_dir() -> PathBuf { pub async fn launch_game( app: tauri::AppHandle, state: State<'_, SharedState>, + feature_state: State<'_, FeatureState>, version: GameVersion, account: MinecraftAccount, ) -> Result<(), String> { @@ -95,14 +96,14 @@ pub async fn launch_game( launcher_dir().join(&version.id), version.id.clone(), lyceris::auth::AuthMethod::Microsoft { - username: account.username, - xuid: account.xuid, - uuid: account.uuid, - access_token: account.access_token, - refresh_token: account.refresh_token, + username: account.username.clone(), + xuid: account.xuid.clone(), + uuid: account.uuid.clone(), + access_token: account.access_token.clone(), + refresh_token: account.refresh_token.clone(), }, ) - .loader(Fabric(version.loader_version).into()) + .loader(Fabric(version.loader_version.clone()).into()) .memory(lyceris::minecraft::config::Memory::Megabyte( state.lock().unwrap().settings.max_memory as u64, )) @@ -124,6 +125,8 @@ pub async fn launch_game( app.emit("launch-status", "Launching").unwrap(); + feature_state.inner().launch(&version, &account).await?; + launch(&config, Some(&emitter)) .await .map_err(|e| e.to_string())? @@ -131,6 +134,11 @@ pub async fn launch_game( .await .map_err(|e| e.to_string())?; + feature_state + .inner() + .after_launch(&version, &account) + .await?; + app.emit("is-launching", "false").unwrap(); app.emit("launch-status", "").unwrap(); emit_progress(&app, 0.0); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8cb4471..c286f72 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ pub mod commands; +pub mod features; pub mod launcher; use serde::{Deserialize, Serialize}; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 72fa828..92f17e9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,7 +1,7 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use saturn_launcher_lib::load_state; +use saturn_launcher_lib::{features::FeatureState, load_state}; use tauri::{Emitter, Manager, WindowEvent}; #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -9,6 +9,7 @@ fn main() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .manage(load_state()) + .manage(FeatureState::new()) .invoke_handler(tauri::generate_handler![ // Saturn Launcher Commands saturn_launcher_lib::commands::get_installed_mods, From 9769a2d6c1e3a9d11043c17cf10614e1051a22c6 Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 20:02:56 +0200 Subject: [PATCH 02/10] Implemented state --- src-tauri/src/features/instance_sync.rs | 5 ++++- src-tauri/src/features/mod.rs | 29 ++++++++++++++++++++----- src-tauri/src/launcher/mod.rs | 7 ++++-- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/features/instance_sync.rs b/src-tauri/src/features/instance_sync.rs index 6f9dc59..f597f98 100644 --- a/src-tauri/src/features/instance_sync.rs +++ b/src-tauri/src/features/instance_sync.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use lyceris::auth::microsoft::MinecraftAccount; +use tauri::State; -use crate::{features::Feature, GameVersion}; +use crate::{features::Feature, GameVersion, SharedState}; pub struct InstanceSync {} @@ -15,6 +16,7 @@ impl Default for InstanceSync { impl Feature for InstanceSync { fn launch( &mut self, + state: &State<'_, SharedState>, version: &GameVersion, account: &MinecraftAccount, ) -> super::FeatureResult { @@ -25,6 +27,7 @@ impl Feature for InstanceSync { fn after_launch( &mut self, + state: &State<'_, SharedState>, version: &GameVersion, account: &MinecraftAccount, ) -> super::FeatureResult { diff --git a/src-tauri/src/features/mod.rs b/src-tauri/src/features/mod.rs index 6730d77..76962ee 100644 --- a/src-tauri/src/features/mod.rs +++ b/src-tauri/src/features/mod.rs @@ -1,7 +1,8 @@ use lyceris::auth::microsoft::MinecraftAccount; +use tauri::State; use tokio::sync::Mutex; -use crate::{features::instance_sync::InstanceSync, GameVersion}; +use crate::{features::instance_sync::InstanceSync, GameVersion, SharedState}; mod instance_sync; @@ -9,10 +10,20 @@ pub type FeatureResult = Result<(), String>; #[allow(unused_variables)] pub trait Feature: Send + Sync { - fn launch(&mut self, version: &GameVersion, account: &MinecraftAccount) -> FeatureResult { + fn launch( + &mut self, + state: &State<'_, SharedState>, + version: &GameVersion, + account: &MinecraftAccount, + ) -> FeatureResult { Ok(()) } - fn after_launch(&mut self, version: &GameVersion, account: &MinecraftAccount) -> FeatureResult { + fn after_launch( + &mut self, + state: &State<'_, SharedState>, + version: &GameVersion, + account: &MinecraftAccount, + ) -> FeatureResult { Ok(()) } } @@ -26,9 +37,14 @@ impl FeatureState { } impl FeatureState { - pub async fn launch(&self, version: &GameVersion, account: &MinecraftAccount) -> FeatureResult { + pub async fn launch( + &self, + state: &State<'_, SharedState>, + version: &GameVersion, + account: &MinecraftAccount, + ) -> FeatureResult { for feature in self.0.lock().await.iter_mut() { - feature.launch(version, account)?; + feature.launch(state, version, account)?; } Ok(()) @@ -36,11 +52,12 @@ impl FeatureState { pub async fn after_launch( &self, + state: &State<'_, SharedState>, version: &GameVersion, account: &MinecraftAccount, ) -> FeatureResult { for feature in self.0.lock().await.iter_mut() { - feature.after_launch(version, account)?; + feature.after_launch(state, version, account)?; } Ok(()) diff --git a/src-tauri/src/launcher/mod.rs b/src-tauri/src/launcher/mod.rs index e98091d..5d686a7 100644 --- a/src-tauri/src/launcher/mod.rs +++ b/src-tauri/src/launcher/mod.rs @@ -125,7 +125,10 @@ pub async fn launch_game( app.emit("launch-status", "Launching").unwrap(); - feature_state.inner().launch(&version, &account).await?; + feature_state + .inner() + .launch(&state, &version, &account) + .await?; launch(&config, Some(&emitter)) .await @@ -136,7 +139,7 @@ pub async fn launch_game( feature_state .inner() - .after_launch(&version, &account) + .after_launch(&state, &version, &account) .await?; app.emit("is-launching", "false").unwrap(); From 450ade4b5387658189f198e164f6cb0770eaaa38 Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 20:36:41 +0200 Subject: [PATCH 03/10] Fix: auth not refreshing on launch #15 --- src-tauri/src/launcher/auth.rs | 7 +++++-- src/App.tsx | 7 +++---- src/components/PlaySection.tsx | 22 ++++++++++++++++------ src/components/layout/AccountDropdown.tsx | 21 ++++++++++----------- src/components/layout/props.ts | 9 ++++----- src/lib/auth.ts | 3 ++- 6 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src-tauri/src/launcher/auth.rs b/src-tauri/src/launcher/auth.rs index a1ac007..c6a2299 100644 --- a/src-tauri/src/launcher/auth.rs +++ b/src-tauri/src/launcher/auth.rs @@ -125,8 +125,11 @@ pub async fn auth_refresh(uuid: String) -> Result { } #[command] -pub fn auth_list() -> Result, String> { - Ok(load_accounts()) +pub fn auth_list() -> Result, String> { + Ok(load_accounts() + .iter() + .map(|account| (account.uuid.clone(), account.username.clone())) + .collect()) } #[command] diff --git a/src/App.tsx b/src/App.tsx index daa7827..6dfe2f3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,7 +6,6 @@ import ModsSection from "./components/ModsSection"; import SettingsSection from "./components/SettingsSection"; import { versions } from "./lib/launcher"; import { GameVersion } from "./lib/types"; -import { MinecraftAccount } from "./lib/auth"; import { getVersion, installPaths, updateVersion } from "./lib/saturn"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { listen } from "@tauri-apps/api/event"; @@ -26,8 +25,8 @@ export default function App() { const versionRef = useRef(null); const accountRef = useRef(null); - const [accounts, setAccounts] = useState([]); - const [activeAccount, setActiveAccount] = useState( + const [accounts, setAccounts] = useState<[string, string][]>([]); + const [activeAccount, setActiveAccount] = useState<[string, string] | null>( null, ); @@ -137,7 +136,7 @@ export default function App() {
{active === "play" && ( - + )} {active === "mods" && selectedVersion && ( diff --git a/src/components/PlaySection.tsx b/src/components/PlaySection.tsx index bc97ef6..55fab7d 100644 --- a/src/components/PlaySection.tsx +++ b/src/components/PlaySection.tsx @@ -1,19 +1,22 @@ import React, { useEffect, useState } from "react"; import { Play, Loader2 } from "lucide-react"; -// import { launchGame } from "../services/SaturnApi"; import Bg from "@/assets/bg.png"; import { listen } from "@tauri-apps/api/event"; import { GameVersion } from "@/lib/types"; import { launch } from "@/lib/launcher"; -import { MinecraftAccount } from "@/lib/auth"; +import { authGetValid } from "@/lib/auth"; import { cn } from "@/lib/utils"; +import { SetState } from "./layout/props"; interface PlaySectionProps { version: GameVersion | null; - account: MinecraftAccount | null; + activeAccount: [string, string] | null; + setActiveAccount: SetState<[string, string] | null>, + accounts: [string, string][], + setAccounts: SetState<[string, string][]>, } -const PlaySection: React.FC = ({ version, account }) => { +const PlaySection: React.FC = ({ version, activeAccount: account }) => { const [isLaunching, setIsLaunching] = useState(false); const [launchStatus, setLaunchStatus] = useState(""); const [progress, setProgress] = useState(0); @@ -39,6 +42,13 @@ const PlaySection: React.FC = ({ version, account }) => { }; }, []); + const handleLaunch = async () => { + if (version && account) { + const validAccount = await authGetValid(account[0]); + launch(version, validAccount); + } + } + return (
{/* Background Image */} @@ -54,12 +64,12 @@ const PlaySection: React.FC = ({ version, account }) => {
@@ -140,17 +139,17 @@ export default function AccountDropdown({ {/* Current account */} {activeAccount && ( )} {/* Other accounts */} {otherAccounts.map((acc) => ( switchAccount(acc)} /> ))} diff --git a/src/components/layout/props.ts b/src/components/layout/props.ts index c2aeed6..ff394cd 100644 --- a/src/components/layout/props.ts +++ b/src/components/layout/props.ts @@ -1,4 +1,3 @@ -import { MinecraftAccount } from "@/lib/auth"; import { GameVersion } from "@/lib/types"; export type SetState = React.Dispatch>; @@ -17,9 +16,9 @@ export interface Props { versionRef: React.RefObject; accountRef: React.RefObject; - accounts: MinecraftAccount[]; - setAccounts: SetState; + accounts: [string, string][]; + setAccounts: SetState<[string, string][]>; - activeAccount: MinecraftAccount | null; - setActiveAccount: SetState; + activeAccount: [string, string] | null; + setActiveAccount: SetState<[string, string] | null>; } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 311c8ee..d17d8d9 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -33,8 +33,9 @@ export async function authRefresh(uuid: string): Promise { /** * Get all stored accounts + * Format [UUID, Username] */ -export async function authList(): Promise { +export async function authList(): Promise<[string, string][]> { return await invoke("auth_list"); } From 950d7f0a1d5612085a4a78be936cd4fa82ff03d1 Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 20:39:59 +0200 Subject: [PATCH 04/10] Obfuscated auth code log for security --- src-tauri/src/launcher/auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/launcher/auth.rs b/src-tauri/src/launcher/auth.rs index c6a2299..70bd0c3 100644 --- a/src-tauri/src/launcher/auth.rs +++ b/src-tauri/src/launcher/auth.rs @@ -61,7 +61,7 @@ pub fn auth_create_link(app: AppHandle) -> Result { // Check for auth code if url_str.contains("code=") { if let Some(code) = extract_code(&url_str) { - println!("Auth code received: {}", code); + println!("Auth code received: {} ({})", &code[0..5], code.len()); let _ = app_handle.emit("auth-code", code); From fba51d78b815b1fc50c23b5941bcdb141ddb57ab Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 21:13:02 +0200 Subject: [PATCH 05/10] Working Instance Sync --- src-tauri/src/features/instance_sync.rs | 73 +++++++++++++++++++++++-- src-tauri/src/features/mod.rs | 15 +++++ src-tauri/src/lib.rs | 8 ++- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/features/instance_sync.rs b/src-tauri/src/features/instance_sync.rs index f597f98..1949e78 100644 --- a/src-tauri/src/features/instance_sync.rs +++ b/src-tauri/src/features/instance_sync.rs @@ -1,9 +1,9 @@ -use std::collections::HashMap; +use std::{collections::HashMap, fs, path::PathBuf}; use lyceris::auth::microsoft::MinecraftAccount; use tauri::State; -use crate::{features::Feature, GameVersion, SharedState}; +use crate::{features::Feature, launcher::launcher_dir, GameVersion, SharedState}; pub struct InstanceSync {} @@ -20,7 +20,7 @@ impl Feature for InstanceSync { version: &GameVersion, account: &MinecraftAccount, ) -> super::FeatureResult { - println!("Launching {} for {}", version.id, account.username); + sync_to_instance(version).map_err(|e| e.to_string())?; Ok(()) } @@ -31,12 +31,70 @@ impl Feature for InstanceSync { version: &GameVersion, account: &MinecraftAccount, ) -> super::FeatureResult { - println!("Game launched!"); + sync_back_from_instance(version).map_err(|e| e.to_string())?; Ok(()) } } +fn sync_to_instance(version: &GameVersion) -> std::io::Result<()> { + let global_path = options_path_global(); + let instance_path = options_path_instance(version); + + if !global_path.exists() { + if let Ok(instance_content) = fs::read_to_string(instance_path) { + fs::write(global_path, instance_content)?; + } + + return Ok(()); + } + + let global_content = fs::read_to_string(global_path)?; + + // Ensure instance dir exists + if let Some(parent) = instance_path.parent() { + fs::create_dir_all(parent)?; + } + + fs::write(instance_path, global_content)?; + + Ok(()) +} + +fn sync_back_from_instance(version: &GameVersion) -> std::io::Result<()> { + let global_path = options_path_global(); + let instance_path = options_path_instance(version); + + let global_content = fs::read_to_string(&global_path)?; + let instance_content = fs::read_to_string(&instance_path)?; + + let mut global_map = parse_options(&global_content); + let instance_map = parse_options(&instance_content); + + // Only apply changes (ignore deletions) + for (key, instance_value) in instance_map { + match global_map.get(&key) { + Some(global_value) if global_value == &instance_value => {} + _ => { + // changed or new → update global + global_map.insert(key, instance_value); + } + } + } + + fs::write(global_path, serialize_options(&global_map))?; + + Ok(()) +} + +fn options_path_global() -> PathBuf { + launcher_dir().join("options.txt") +} + +fn options_path_instance(version: &GameVersion) -> PathBuf { + launcher_dir().join(&version.id).join("options.txt") +} + fn parse_options(content: &str) -> HashMap { let mut map = HashMap::new(); @@ -52,3 +110,10 @@ fn parse_options(content: &str) -> HashMap { map } + +fn serialize_options(map: &HashMap) -> String { + map.iter() + .map(|(k, v)| format!("{k}:{v}")) + .collect::>() + .join("\n") +} diff --git a/src-tauri/src/features/mod.rs b/src-tauri/src/features/mod.rs index 76962ee..b4afedf 100644 --- a/src-tauri/src/features/mod.rs +++ b/src-tauri/src/features/mod.rs @@ -1,4 +1,5 @@ use lyceris::auth::microsoft::MinecraftAccount; +use serde::{Deserialize, Serialize}; use tauri::State; use tokio::sync::Mutex; @@ -8,6 +9,20 @@ mod instance_sync; pub type FeatureResult = Result<(), String>; +#[derive(Clone, Serialize, Deserialize)] +pub struct FeatureSettings { + // Instance Sync + sync_options: bool, +} + +impl Default for FeatureSettings { + fn default() -> Self { + Self { + sync_options: false, + } + } +} + #[allow(unused_variables)] pub trait Feature: Send + Sync { fn launch( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c286f72..c6ed123 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,7 +9,7 @@ use std::{ sync::{Arc, Mutex}, }; -use crate::launcher::launcher_dir; +use crate::{features::FeatureSettings, launcher::launcher_dir}; pub type SharedState = Arc>; @@ -36,6 +36,7 @@ pub struct GameVersion { #[derive(Clone, Serialize, Deserialize)] pub struct LauncherSettings { pub max_memory: u32, + pub features: FeatureSettings, } // --- App State --- @@ -48,7 +49,10 @@ pub struct AppState { impl Default for AppState { fn default() -> Self { Self { - settings: LauncherSettings { max_memory: 4096 }, + settings: LauncherSettings { + max_memory: 4096, + features: FeatureSettings::default(), + }, version: String::from("1.21.6"), } } From 43b9d5910ec14238cb12e57a15668299cb325f88 Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 21:31:09 +0200 Subject: [PATCH 06/10] Updated schema settings --- src/components/SettingsSection.tsx | 178 ++++++++++++++++++----------- 1 file changed, 112 insertions(+), 66 deletions(-) diff --git a/src/components/SettingsSection.tsx b/src/components/SettingsSection.tsx index ef1a077..5f40435 100644 --- a/src/components/SettingsSection.tsx +++ b/src/components/SettingsSection.tsx @@ -1,9 +1,46 @@ import React, { useState, useEffect } from "react"; -import { MemoryStick, Save, RefreshCcw, Info } from "lucide-react"; +import { Save, RefreshCcw, Info } from "lucide-react"; import { getSettings, updateSettings } from "../lib/saturn"; -import { motion } from "framer-motion"; import { LauncherSettings } from "@/lib/types"; +export type SettingType = "slider" | "toggle" | "input"; + +export interface SettingSchemaItem { + key: keyof LauncherSettings; + label: string; + description?: string; + type: SettingType; + + // Slider-specific + min?: number; + max?: number; + step?: number; + format?: (value: any) => string; + ticks?: number[]; + info?: string; + + // Default value + default: any; +} + +export const settingsSchema: SettingSchemaItem[] = [ + { + key: "max_memory", + label: "Maximum Memory (RAM)", + type: "slider", + min: 1024, + max: 16384, + step: 512, + default: 2048, + + format: (v) => `${(v / 1024).toFixed(1)} GB`, + + ticks: [1024, 4096, 8192, 12288, 16384], + + info: `Allocating too much memory can cause system instability, while too little can lead to performance issues or crashes. 2GB - 8GB is recommended for most users.`, + }, +]; + const SettingsSection: React.FC = () => { const [settings, setSettings] = useState(null); const [isSaving, setIsSaving] = useState(false); @@ -32,11 +69,7 @@ const SettingsSection: React.FC = () => { }; const handleReset = async () => { - // In a real app, this would reset to defaults in the service - const defaultSettings: LauncherSettings = { - max_memory: 4096, - }; - setSettings(defaultSettings); + setSettings(Object.fromEntries(settingsSchema.map((v) => [v.key, v.default])) as LauncherSettings); }; if (!settings) return null; @@ -70,69 +103,82 @@ const SettingsSection: React.FC = () => {
- {/* Memory Allocation */} - -
-
- -
-

Memory Allocation

-
+ {settingsSchema.map((item) => { + const value = settings[item.key]; -
-
- Maximum Memory (RAM) - - {(settings.max_memory / 1024).toFixed(1)} GB - -
- - - setSettings({ - ...settings, - max_memory: parseInt(e.target.value), - }) - } - className="w-full h-2 bg-saturn-border rounded-lg appearance-none cursor-pointer accent-saturn-accent" - /> - -
- {[1024, 4096, 8192, 12288, 16384].map((value) => { - const percent = - (((value - 1024) / (16384 - 1024)) * 100 - 50) * 0.97 + 50; - - return ( - - {value / 1024} GB + return ( +
+
+ {item.label} + {item.format && ( + + {item.format(value)} - ); - })} -
+ )} +
-
- -

- Allocating too much memory can cause system instability, while - too little can lead to performance issues or crashes. 4GB - 8GB - is recommended for most users. -

+ {item.type === "slider" && ( + <> + + setSettings({ + ...settings, + [item.key]: parseInt(e.target.value), + }) + } + className="w-full h-2 bg-saturn-border rounded-lg appearance-none cursor-pointer accent-saturn-accent" + /> + + {/* Tick labels */} + {item.ticks && ( +
+ {item.ticks.map((tick) => { + const percent = + (((tick - item.min!) / (item.max! - item.min!)) * 100 - 50) * + 0.97 + + 50; + + return ( + + {tick / 1024} GB + + ); + })} +
+ )} + + {/* Info box */} + {item.info && ( +
+ +

+ {item.info} +

+
+ )} + + )} + + {item.description && ( +
+ +

+ {item.description} +

+
+ )}
-
- + ); + })}
); From 38207a1f304ebe596d7749f7ee58af5aae82c434 Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 21:52:33 +0200 Subject: [PATCH 07/10] Improved settings --- src/components/SettingsSection.tsx | 237 +++++++++++++++++------------ src/lib/types.ts | 3 + 2 files changed, 146 insertions(+), 94 deletions(-) diff --git a/src/components/SettingsSection.tsx b/src/components/SettingsSection.tsx index 5f40435..e076293 100644 --- a/src/components/SettingsSection.tsx +++ b/src/components/SettingsSection.tsx @@ -3,44 +3,168 @@ import { Save, RefreshCcw, Info } from "lucide-react"; import { getSettings, updateSettings } from "../lib/saturn"; import { LauncherSettings } from "@/lib/types"; -export type SettingType = "slider" | "toggle" | "input"; +export type SettingSchemaItem = + | SettingSchemaLeaf + | SettingSchemaGroup; -export interface SettingSchemaItem { - key: keyof LauncherSettings; +export interface SettingSchemaBase { label: string; description?: string; - type: SettingType; +} + +export interface SettingSchemaLeaf extends SettingSchemaBase { + type: "slider" | "toggle" | "input"; + + key: string; // leaf key (resolved in context) + default: any; - // Slider-specific min?: number; max?: number; step?: number; format?: (value: any) => string; ticks?: number[]; info?: string; +} - // Default value - default: any; +export interface SettingSchemaGroup extends SettingSchemaBase { + type: "group"; + children: SettingSchemaItem[]; } export const settingsSchema: SettingSchemaItem[] = [ { - key: "max_memory", - label: "Maximum Memory (RAM)", - type: "slider", - min: 1024, - max: 16384, - step: 512, - default: 2048, + type: "group", + label: "Performance", - format: (v) => `${(v / 1024).toFixed(1)} GB`, + children: [ + { + key: "max_memory", + label: "Maximum Memory (RAM)", + type: "slider", + min: 1024, + max: 16384, + step: 512, + default: 2048, - ticks: [1024, 4096, 8192, 12288, 16384], + format: (v) => `${(v / 1024).toFixed(1)} GB`, + }, + ], + }, - info: `Allocating too much memory can cause system instability, while too little can lead to performance issues or crashes. 2GB - 8GB is recommended for most users.`, + { + type: "group", + label: "Features", + + children: [ + { + key: "features.sync_options", + label: "Sync Options", + type: "toggle", + default: true, + }, + ], }, ]; +const getValue = (obj: any, path: string) => + path.split(".").reduce((acc, key) => acc?.[key], obj); + +const setValue = (obj: any, path: string, value: any) => { + const keys = path.split("."); + const last = keys.pop()!; + const clone = structuredClone(obj); + + let current = clone; + for (const key of keys) current = current[key]; + + current[last] = value; + return clone; +}; + +const SettingItem: React.FC<{ + item: SettingSchemaItem; + settings: LauncherSettings; + setSettings: React.Dispatch< + React.SetStateAction + >; +}> = ({ item, settings, setSettings }) => { + if (item.type === "group") { + return ( +
+
+ {item.label} +
+ + {item.children.map((child, i) => ( + + ))} +
+ ); + } + + const value = getValue(settings, item.key); + + return ( +
+ {item.type === "slider" && ( + <> + {item.label} + + setSettings((prev) => + prev + ? setValue(prev, item.key, parseInt(e.target.value)) + : prev + ) + } + className="w-full" + /> + + )} + + {item.type === "toggle" && ( +
+ + setSettings((prev) => + prev ? setValue(prev, item.key, e.target.checked) : prev + ) + } + /> + {item.label} +
+ )} +
+ ); +}; + +const buildDefaultSettings = (schema: SettingSchemaItem[]): any => { + const result: any = {}; + + for (const item of schema) { + if (item.type === "group") { + Object.assign(result, buildDefaultSettings(item.children)); + } else { + result[item.key] = item.default; + } + } + + return result; +}; + const SettingsSection: React.FC = () => { const [settings, setSettings] = useState(null); const [isSaving, setIsSaving] = useState(false); @@ -69,7 +193,7 @@ const SettingsSection: React.FC = () => { }; const handleReset = async () => { - setSettings(Object.fromEntries(settingsSchema.map((v) => [v.key, v.default])) as LauncherSettings); + setSettings(buildDefaultSettings(settingsSchema) as LauncherSettings); }; if (!settings) return null; @@ -103,82 +227,7 @@ const SettingsSection: React.FC = () => {
- {settingsSchema.map((item) => { - const value = settings[item.key]; - - return ( -
-
- {item.label} - {item.format && ( - - {item.format(value)} - - )} -
- - {item.type === "slider" && ( - <> - - setSettings({ - ...settings, - [item.key]: parseInt(e.target.value), - }) - } - className="w-full h-2 bg-saturn-border rounded-lg appearance-none cursor-pointer accent-saturn-accent" - /> - - {/* Tick labels */} - {item.ticks && ( -
- {item.ticks.map((tick) => { - const percent = - (((tick - item.min!) / (item.max! - item.min!)) * 100 - 50) * - 0.97 + - 50; - - return ( - - {tick / 1024} GB - - ); - })} -
- )} - - {/* Info box */} - {item.info && ( -
- -

- {item.info} -

-
- )} - - )} - - {item.description && ( -
- -

- {item.description} -

-
- )} -
- ); - })} + {settingsSchema.map((item) => ())}
); diff --git a/src/lib/types.ts b/src/lib/types.ts index 81fd836..ad6b013 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -17,4 +17,7 @@ export interface GameVersion { export interface LauncherSettings { max_memory: number; + features: { + sync_options: boolean + } } From 2bb686baaf135b8d879e5fa859f4dca2bcc98ec6 Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 22:05:10 +0200 Subject: [PATCH 08/10] Implemented optional sync --- src-tauri/src/features/instance_sync.rs | 12 ++++++---- src/components/SettingsSection.tsx | 32 +++++++++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/features/instance_sync.rs b/src-tauri/src/features/instance_sync.rs index 1949e78..68d9413 100644 --- a/src-tauri/src/features/instance_sync.rs +++ b/src-tauri/src/features/instance_sync.rs @@ -18,9 +18,11 @@ impl Feature for InstanceSync { &mut self, state: &State<'_, SharedState>, version: &GameVersion, - account: &MinecraftAccount, + _account: &MinecraftAccount, ) -> super::FeatureResult { - sync_to_instance(version).map_err(|e| e.to_string())?; + if state.lock().unwrap().settings.features.sync_options { + sync_to_instance(version).map_err(|e| e.to_string())?; + } Ok(()) } @@ -29,9 +31,11 @@ impl Feature for InstanceSync { &mut self, state: &State<'_, SharedState>, version: &GameVersion, - account: &MinecraftAccount, + _account: &MinecraftAccount, ) -> super::FeatureResult { - sync_back_from_instance(version).map_err(|e| e.to_string())?; + if state.lock().unwrap().settings.features.sync_options { + sync_back_from_instance(version).map_err(|e| e.to_string())?; + } Ok(()) } diff --git a/src/components/SettingsSection.tsx b/src/components/SettingsSection.tsx index e076293..bcf9928 100644 --- a/src/components/SettingsSection.tsx +++ b/src/components/SettingsSection.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { Save, RefreshCcw, Info } from "lucide-react"; +import { Save, RefreshCcw } from "lucide-react"; import { getSettings, updateSettings } from "../lib/saturn"; import { LauncherSettings } from "@/lib/types"; @@ -45,6 +45,7 @@ export const settingsSchema: SettingSchemaItem[] = [ max: 16384, step: 512, default: 2048, + ticks: [1024, 4096, 8192, 12288, 16384], format: (v) => `${(v / 1024).toFixed(1)} GB`, }, @@ -112,8 +113,11 @@ const SettingItem: React.FC<{ return (
{item.type === "slider" && ( - <> - {item.label} +
+
+ {item.label} + {(item.format && item.format(value)) || value} +
- + {item.ticks && ( +
+ {item.ticks.map((tick) => { + const percent = + (((tick - item.min!) / (item.max! - item.min!)) * 100 - 50) * + 0.97 + + 50; + + return ( + + {tick / 1024} GB + + ); + })} +
+ )} +
)} {item.type === "toggle" && ( From 19b19ba08e91fe550c243b4a2f84fb5aa03a52b1 Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 22:09:20 +0200 Subject: [PATCH 09/10] Improved slider --- src/components/SettingsSection.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/SettingsSection.tsx b/src/components/SettingsSection.tsx index bcf9928..d22476e 100644 --- a/src/components/SettingsSection.tsx +++ b/src/components/SettingsSection.tsx @@ -114,9 +114,9 @@ const SettingItem: React.FC<{
{item.type === "slider" && (
-
- {item.label} - {(item.format && item.format(value)) || value} +
+ {item.label} + {(item.format && item.format(value)) || value}
{item.ticks && (
From ac9108f507f83e41b81111fcd93d1f37212f06f2 Mon Sep 17 00:00:00 2001 From: Kr4ight Date: Mon, 6 Apr 2026 22:11:24 +0200 Subject: [PATCH 10/10] Small changes --- src-tauri/src/features/mod.rs | 4 +--- src-tauri/src/lib.rs | 2 +- src/components/SettingsSection.tsx | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/features/mod.rs b/src-tauri/src/features/mod.rs index b4afedf..28a780d 100644 --- a/src-tauri/src/features/mod.rs +++ b/src-tauri/src/features/mod.rs @@ -17,9 +17,7 @@ pub struct FeatureSettings { impl Default for FeatureSettings { fn default() -> Self { - Self { - sync_options: false, - } + Self { sync_options: true } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c6ed123..613b035 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -50,7 +50,7 @@ impl Default for AppState { fn default() -> Self { Self { settings: LauncherSettings { - max_memory: 4096, + max_memory: 2048, features: FeatureSettings::default(), }, version: String::from("1.21.6"), diff --git a/src/components/SettingsSection.tsx b/src/components/SettingsSection.tsx index d22476e..eac055b 100644 --- a/src/components/SettingsSection.tsx +++ b/src/components/SettingsSection.tsx @@ -59,7 +59,7 @@ export const settingsSchema: SettingSchemaItem[] = [ children: [ { key: "features.sync_options", - label: "Sync Options", + label: "Sync options.txt", type: "toggle", default: true, },