15 Commits
Author SHA1 Message Date
Kr4ight 1436950b90 Merge pull request #13 from saturnclientmc/patches-and-impl
Tauri Release Build / Build (ubuntu-latest) (release) Failing after 33s
Tauri Release Build / Build (macos-latest) (release) Canceled after 0s
Tauri Release Build / Build (windows-latest) (release) Canceled after 0s
Tauri Release Build / Upload to GitHub Release (release) Canceled after 0s
Patches and impl
2026-04-05 19:40:27 +02:00
Kr4ight 7e9b663705 Logo with no bg 2026-04-05 19:30:39 +02:00
Kr4ight a134d9b67e Updated icons 2026-04-05 19:27:51 +02:00
Kr4ight 3f2054c721 Changed logo and readme 2026-04-05 19:25:58 +02:00
Kr4ight f33aadb3e0 Refactored directory system 2026-04-05 19:01:46 +02:00
Kr4ight 807bbbc7e2 slight change in settings 2026-04-05 18:50:19 +02:00
Kr4ight 03815cce1e Removed v from version in mods section 2026-04-05 18:42:03 +02:00
Kr4ight 443a22bbed Updated version 2026-04-05 18:41:33 +02:00
Kr4ight 262bb89899 Improved mod install verbosity 2026-04-05 18:36:09 +02:00
Kr4ight d77e0c8924 Fix: remove disabled mod 2026-04-05 18:31:24 +02:00
Kr4ight 06f53935a0 Working drag and drop 2026-04-05 18:26:17 +02:00
Kr4ight e71617d535 Drag and drop UI 2026-04-05 18:17:49 +02:00
Kr4ight d71c0c437d Refactoring Install paths for a frontend experience 2026-04-05 17:37:21 +02:00
Kr4ight 7167211f06 Remove mod #11 2026-04-05 17:16:09 +02:00
Kr4ight 167bb33144 Drag and drop to install #12 2026-04-05 17:05:51 +02:00
71 changed files with 386 additions and 113 deletions
+24 -4
View File
@@ -1,7 +1,27 @@
# Tauri + React + Typescript <p align="center">
<img src="src/assets/logo.svg" alt="Saturn Launcher Logo" width="150px"/>
</p>
This template should help get you started developing with Tauri, React and Typescript in Vite. <h1 align="center">Saturn Launcher</h1>
## Recommended IDE Setup <p align="center">
A fast, modern, and customizable launcher built for performance and flexibility, built with Rust.
</p>
---
## ✨ Features
- ⚡ **High Performance** – Optimized for speed and responsiveness
- 🎨 **Modern UI** – Clean, minimal, and aesthetic interface
- 🧩 **Modular Architecture** – Easily extend and maintain mods
- 📦 **Efficient File Handling** – Smart installation and management system
---
## 🧱 Tech Stack
- **Frontend:** React / TypeScript / TailwindCSS
- **Backend:** Rust (Tauri)
- **Build Tooling:** Vite
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
+27 -1
View File
@@ -16,6 +16,7 @@
"lucide-react": "^1.7.0", "lucide-react": "^1.7.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hot-toast": "^2.6.0",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2" "tailwindcss": "^4.2.2"
}, },
@@ -1776,7 +1777,6 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
@@ -1945,6 +1945,15 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/goober": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -2384,6 +2393,23 @@
"react": "^19.2.4" "react": "^19.2.4"
} }
}, },
"node_modules/react-hot-toast": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+2 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "saturn-launcher", "name": "saturn-launcher",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -18,6 +18,7 @@
"lucide-react": "^1.7.0", "lucide-react": "^1.7.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hot-toast": "^2.6.0",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2" "tailwindcss": "^4.2.2"
}, },
+1 -12
View File
@@ -874,15 +874,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "directories"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
dependencies = [
"dirs-sys",
]
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "6.0.0" version = "6.0.0"
@@ -3896,12 +3887,10 @@ dependencies = [
[[package]] [[package]]
name = "saturn-launcher" name = "saturn-launcher"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"directories",
"futures", "futures",
"lyceris", "lyceris",
"once_cell",
"rayon", "rayon",
"reqwest 0.12.28", "reqwest 0.12.28",
"serde", "serde",
+3 -5
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "saturn-launcher" name = "saturn-launcher"
version = "0.1.0" version = "0.1.1"
description = "A Tauri App" description = "A minecraft launcher for Saturn Client"
authors = ["you"] authors = ["SaturnClient"]
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -22,8 +22,6 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
directories = "6.0"
once_cell = "1.21"
lyceris = "1.1.3" lyceris = "1.1.3"
rayon = "1.10.0" rayon = "1.10.0"
reqwest = "0.12.9" reqwest = "0.12.9"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+62 -9
View File
@@ -1,10 +1,10 @@
use std::path::PathBuf; use std::path::{Path, PathBuf};
use serde::Deserialize; use serde::Deserialize;
use tauri::State; use tauri::{AppHandle, Emitter, State};
use tokio::fs; use tokio::fs;
use crate::{launcher::LAUNCHER_DIR, save_state, LauncherSettings, Mod, SharedState}; use crate::{launcher::launcher_dir, save_state, LauncherSettings, Mod, SharedState};
#[derive(Deserialize)] #[derive(Deserialize)]
struct ModrinthVersion { struct ModrinthVersion {
@@ -25,12 +25,12 @@ struct ModrinthFile {
pub async fn get_installed_mods(version_id: String) -> Result<Vec<Mod>, String> { pub async fn get_installed_mods(version_id: String) -> Result<Vec<Mod>, String> {
let mut mods = Vec::new(); let mut mods = Vec::new();
let base_dir = LAUNCHER_DIR.data_dir().join(&version_id).join("mods"); let base_dir = launcher_dir().join(&version_id).join("mods");
let enabled_dir = base_dir.clone(); let enabled_dir = base_dir.clone();
let disabled_dir = base_dir.join("disabled_mods"); let disabled_dir = base_dir.join("disabled_mods");
let cache_dir = LAUNCHER_DIR.data_dir().join("cache"); let cache_dir = launcher_dir().join("cache");
// process a directory async // process a directory async
async fn process_dir(dir: PathBuf, enabled: bool, cache_dir: PathBuf) -> Vec<Mod> { async fn process_dir(dir: PathBuf, enabled: bool, cache_dir: PathBuf) -> Vec<Mod> {
@@ -105,10 +105,32 @@ fn fallback_mod(file_name: &str, enabled: bool) -> Mod {
} }
} }
#[tauri::command]
pub async fn remove_mod(version: String, file_name: String) -> Result<(), String> {
// mods folder
let mods_dir: PathBuf = launcher_dir().join(&version).join("mods");
// File to remove
let mut source = mods_dir.join(&file_name);
if !source.exists() {
source = mods_dir.join("disabled_mods").join(&file_name);
}
if !source.exists() {
return Err(format!("Mod file not found: {}", file_name));
}
// move file
fs::remove_file(&source).await.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command] #[tauri::command]
pub async fn disable_mod(version: String, file_name: String) -> Result<(), String> { pub async fn disable_mod(version: String, file_name: String) -> Result<(), String> {
// mods folder // mods folder
let mods_dir: PathBuf = LAUNCHER_DIR.data_dir().join(&version).join("mods"); let mods_dir: PathBuf = launcher_dir().join(&version).join("mods");
// disabled_mods folder // disabled_mods folder
let disabled_dir = mods_dir.join("disabled_mods"); let disabled_dir = mods_dir.join("disabled_mods");
@@ -135,7 +157,7 @@ pub async fn disable_mod(version: String, file_name: String) -> Result<(), Strin
#[tauri::command] #[tauri::command]
pub async fn enable_mod(version: String, file_name: String) -> Result<(), String> { pub async fn enable_mod(version: String, file_name: String) -> Result<(), String> {
let mods_dir = LAUNCHER_DIR.data_dir().join(&version).join("mods"); let mods_dir = launcher_dir().join(&version).join("mods");
let disabled_dir = mods_dir.join("disabled_mods"); let disabled_dir = mods_dir.join("disabled_mods");
@@ -159,7 +181,7 @@ pub async fn install_mod(mut mod_meta: Mod, versions: Vec<String>) -> Result<(),
println!("Installing {} for {:?}", mod_meta.name, versions); println!("Installing {} for {:?}", mod_meta.name, versions);
let cache_dir = LAUNCHER_DIR.data_dir().join("cache"); let cache_dir = launcher_dir().join("cache");
fs::create_dir_all(&cache_dir) fs::create_dir_all(&cache_dir)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -206,7 +228,7 @@ pub async fn install_mod(mut mod_meta: Mod, versions: Vec<String>) -> Result<(),
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let dir: PathBuf = LAUNCHER_DIR.data_dir().join(&mc_version).join("mods"); let dir: PathBuf = launcher_dir().join(&mc_version).join("mods");
fs::create_dir_all(&dir).await.map_err(|e| e.to_string())?; fs::create_dir_all(&dir).await.map_err(|e| e.to_string())?;
@@ -269,3 +291,34 @@ pub fn update_version(state: State<SharedState>, version: String) {
save_state(&state); save_state(&state);
} }
#[tauri::command]
pub async fn install_paths(
app: AppHandle,
state: State<'_, SharedState>,
paths: Vec<String>,
) -> Result<(), String> {
for path in &paths {
let file = Path::new(&path)
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| String::from("File name not found"))?;
let version_dir = launcher_dir().join(&state.lock().unwrap().version);
let mc_child = if path.ends_with(".jar") {
"mods"
} else {
return Err(String::from("Invalid file type"));
};
fs::copy(path, version_dir.join(mc_child).join(file))
.await
.map_err(|e| e.to_string())?;
app.emit("path-installed", path.to_string())
.map_err(|e| e.to_string())?;
}
Ok(())
}
+4 -3
View File
@@ -5,10 +5,10 @@ use tauri::{command, AppHandle, Emitter, Manager, Url};
use lyceris::auth::microsoft::{authenticate, create_link, refresh, validate, MinecraftAccount}; use lyceris::auth::microsoft::{authenticate, create_link, refresh, validate, MinecraftAccount};
use crate::launcher::LAUNCHER_DIR; use crate::launcher::launcher_dir;
fn accounts_path() -> PathBuf { fn accounts_path() -> PathBuf {
LAUNCHER_DIR.data_dir().join("accounts.json") launcher_dir().join("accounts.json")
} }
fn load_accounts() -> Vec<MinecraftAccount> { fn load_accounts() -> Vec<MinecraftAccount> {
@@ -75,7 +75,8 @@ pub fn auth_create_link(app: AppHandle) -> Result<String, String> {
true true
}) })
.build() .build()
.map_err(|e| e.to_string()).unwrap(); .map_err(|e| e.to_string())
.unwrap();
}); });
Ok(link) Ok(link)
+28 -9
View File
@@ -2,7 +2,8 @@ pub mod auth;
pub mod downloader; pub mod downloader;
pub mod install; pub mod install;
use directories::ProjectDirs; use std::{env, path::PathBuf};
use lyceris::{ use lyceris::{
auth::microsoft::MinecraftAccount, auth::microsoft::MinecraftAccount,
minecraft::{ minecraft::{
@@ -12,15 +13,35 @@ use lyceris::{
loader::fabric::Fabric, loader::fabric::Fabric,
}, },
}; };
use once_cell::sync::Lazy;
use tauri::{Emitter as _, State}; use tauri::{Emitter as _, State};
use crate::{GameVersion, SharedState}; use crate::{GameVersion, SharedState};
pub static LAUNCHER_DIR: Lazy<ProjectDirs> = Lazy::new(|| { pub fn launcher_dir() -> PathBuf {
ProjectDirs::from("org", "SaturnLauncher", "SaturnLauncher") if cfg!(target_os = "macos") {
.expect("Failed to create project directories") let home = env::var("HOME")
}); .map(PathBuf::from)
.expect("HOME environment variable not set");
home.join("Library/Application Support/SaturnLauncher")
} else if cfg!(target_os = "windows") {
env::var("APPDATA")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."))
.join("SaturnLauncher")
} else {
env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("."))
.join(".local/share")
})
.join("SaturnLauncher")
}
}
#[tauri::command] #[tauri::command]
pub async fn launch_game( pub async fn launch_game(
@@ -70,10 +91,8 @@ pub async fn launch_game(
}) })
.await; .await;
let current_dir = LAUNCHER_DIR.data_dir();
let config = ConfigBuilder::new( let config = ConfigBuilder::new(
current_dir.join(&version.id), launcher_dir().join(&version.id),
version.id.clone(), version.id.clone(),
lyceris::auth::AuthMethod::Microsoft { lyceris::auth::AuthMethod::Microsoft {
username: account.username, username: account.username,
+10 -6
View File
@@ -2,11 +2,15 @@ pub mod commands;
pub mod launcher; pub mod launcher;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf, sync::Mutex}; use std::{
fs,
path::PathBuf,
sync::{Arc, Mutex},
};
use crate::launcher::LAUNCHER_DIR; use crate::launcher::launcher_dir;
type SharedState = Mutex<AppState>; pub type SharedState = Arc<Mutex<AppState>>;
// --- Types --- // --- Types ---
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
@@ -51,7 +55,7 @@ impl Default for AppState {
// Helper to get config path // Helper to get config path
fn config_path() -> PathBuf { fn config_path() -> PathBuf {
LAUNCHER_DIR.config_dir().join("launcher.json") launcher_dir().join("launcher.json")
} }
// --- Load --- // --- Load ---
@@ -61,7 +65,7 @@ pub fn load_state() -> SharedState {
if path.exists() { if path.exists() {
match fs::read_to_string(&path) { match fs::read_to_string(&path) {
Ok(content) => match serde_json::from_str::<AppState>(&content) { Ok(content) => match serde_json::from_str::<AppState>(&content) {
Ok(state) => return Mutex::new(state), Ok(state) => return Arc::new(Mutex::new(state)),
Err(err) => { Err(err) => {
eprintln!("Failed to parse config, using default: {err}"); eprintln!("Failed to parse config, using default: {err}");
} }
@@ -73,7 +77,7 @@ pub fn load_state() -> SharedState {
} }
// If missing or failed → create default // If missing or failed → create default
let default = Mutex::new(AppState::default()); let default = Arc::new(Mutex::new(AppState::default()));
save_state(&default); save_state(&default);
default default
+44 -1
View File
@@ -2,6 +2,7 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use saturn_launcher_lib::load_state; use saturn_launcher_lib::load_state;
use tauri::{Emitter, Manager, WindowEvent};
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
fn main() { fn main() {
@@ -11,13 +12,15 @@ fn main() {
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
// Saturn Launcher Commands // Saturn Launcher Commands
saturn_launcher_lib::commands::get_installed_mods, saturn_launcher_lib::commands::get_installed_mods,
saturn_launcher_lib::commands::install_mod,
saturn_launcher_lib::commands::enable_mod, saturn_launcher_lib::commands::enable_mod,
saturn_launcher_lib::commands::disable_mod, saturn_launcher_lib::commands::disable_mod,
saturn_launcher_lib::commands::install_mod, saturn_launcher_lib::commands::remove_mod,
saturn_launcher_lib::commands::get_settings, saturn_launcher_lib::commands::get_settings,
saturn_launcher_lib::commands::update_settings, saturn_launcher_lib::commands::update_settings,
saturn_launcher_lib::commands::get_version, saturn_launcher_lib::commands::get_version,
saturn_launcher_lib::commands::update_version, saturn_launcher_lib::commands::update_version,
saturn_launcher_lib::commands::install_paths,
// Launcher Commands // Launcher Commands
saturn_launcher_lib::launcher::launch_game, saturn_launcher_lib::launcher::launch_game,
// Auth Commands // Auth Commands
@@ -29,6 +32,46 @@ fn main() {
saturn_launcher_lib::launcher::auth::auth_validate, saturn_launcher_lib::launcher::auth::auth_validate,
saturn_launcher_lib::launcher::auth::auth_get_valid saturn_launcher_lib::launcher::auth::auth_get_valid
]) ])
.setup(|app| {
match app.get_webview_window("main") {
Some(window) => {
let handle = app.handle().clone();
window.on_window_event(move |event| {
if let WindowEvent::DragDrop(file_event) = event {
if let tauri::DragDropEvent::Drop { paths, .. } = file_event {
println!("Installing files: {:?}", paths);
if handle
.emit(
"install-paths",
paths
.iter()
.filter_map(|p| {
if p.is_file() {
Some(p.to_str()?.to_string())
} else {
None
}
})
.collect::<Vec<_>>(),
)
.is_err()
{
println!("Failed emitting files to frontend");
}
}
}
});
}
None => {
println!("WARN: Drag and drop feature will not work");
}
}
Ok(())
})
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }
+4 -3
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "saturn-launcher", "productName": "saturn-launcher",
"version": "0.1.0", "version": "0.1.1",
"identifier": "org.saturnclient.saturn-launcher", "identifier": "org.saturnclient.saturn-launcher",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
@@ -12,11 +12,12 @@
"app": { "app": {
"windows": [ "windows": [
{ {
"title": "saturn-launcher", "title": "Saturn Launcher v0.1.1",
"width": 1000, "width": 1000,
"height": 650, "height": 650,
"backgroundColor": "#0a0a0a", "backgroundColor": "#0a0a0a",
"visible": false "visible": false,
"dragDropEnabled": true
} }
], ],
"security": { "security": {
+48 -1
View File
@@ -7,8 +7,10 @@ import SettingsSection from "./components/SettingsSection";
import { versions } from "./lib/launcher"; import { versions } from "./lib/launcher";
import { GameVersion } from "./lib/types"; import { GameVersion } from "./lib/types";
import { MinecraftAccount } from "./lib/auth"; import { MinecraftAccount } from "./lib/auth";
import { getVersion, updateVersion } from "./lib/saturn"; import { getVersion, installPaths, updateVersion } from "./lib/saturn";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { listen } from "@tauri-apps/api/event";
import toast, { Toaster } from "react-hot-toast";
type Section = "play" | "mods" | "settings"; type Section = "play" | "mods" | "settings";
@@ -68,7 +70,51 @@ export default function App() {
window.show(); window.show();
}, []); }, []);
useEffect(() => {
if (!selectedVersion?.id) return;
const version = selectedVersion?.id;
const unlistenInstalls = listen<string[]>("install-paths", (event) => {
event.payload.forEach((path) => {
const fileName = path.split(/[\\/]/).pop();
toast.promise(installPaths(version, [path]), {
loading: `${fileName}`,
success: `${fileName}`,
error: (e) => `${fileName} Failed: ${e}`
}).catch(console.error);
});
});
return () => {
unlistenInstalls.then((f) => f());
};
}, [selectedVersion]);
return ( return (
<>
<Toaster position="bottom-right" toastOptions={{
loading: {
style: {
backgroundColor: 'var(--color-saturn-panel)',
color: 'var(--color-saturn-text-primary)'
}
},
success: {
style: {
backgroundColor: 'var(--color-saturn-panel)',
color: 'var(--color-saturn-text-primary)'
}
},
error: {
style: {
backgroundColor: 'var(--color-saturn-panel)',
color: 'var(--color-saturn-text-primary)'
}
}
}} />
<div className="flex h-screen"> <div className="flex h-screen">
<Sidebar active={active} setActive={setActive} /> <Sidebar active={active} setActive={setActive} />
@@ -100,5 +146,6 @@ export default function App() {
</div> </div>
</main> </main>
</div> </div>
</>
); );
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 612 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+19 -3
View File
@@ -14,12 +14,14 @@ import {
enableMod, enableMod,
installMod, installMod,
disableMod, disableMod,
removeMod,
} from "@/lib/saturn"; } from "@/lib/saturn";
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { GameVersion, Mod } from "@/lib/types"; import { GameVersion, Mod } from "@/lib/types";
import { versions } from "@/lib/launcher"; import { versions } from "@/lib/launcher";
import toast from "react-hot-toast";
function cn(...inputs: ClassValue[]) { function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@@ -46,6 +48,10 @@ const ModsSection: React.FC<ModsSectionProps> = ({ version }) => {
if (activeTab === "installed") { if (activeTab === "installed") {
const m = await getInstalledMods(version.id); const m = await getInstalledMods(version.id);
setMods(m); setMods(m);
if (activeTab === "installed") {
// Fix: If discover mods takes too long and the user switches tabs it glitches
setMods(m);
}
} else { } else {
const m = await discoverMods(searchQuery); const m = await discoverMods(searchQuery);
if (activeTab === "discover") { if (activeTab === "discover") {
@@ -72,6 +78,11 @@ const ModsSection: React.FC<ModsSectionProps> = ({ version }) => {
setMods(m); setMods(m);
}; };
const handleRemove = async (modId: string) => {
await removeMod(version.id, modId);
loadMods();
}
const openInstallDialog = (mod: Mod) => { const openInstallDialog = (mod: Mod) => {
setSelectedModForInstall(mod); setSelectedModForInstall(mod);
setSelectedVersionsForInstall([version.id]); setSelectedVersionsForInstall([version.id]);
@@ -80,12 +91,17 @@ const ModsSection: React.FC<ModsSectionProps> = ({ version }) => {
const handleInstall = async () => { const handleInstall = async () => {
if (selectedModForInstall && selectedVersionsForInstall.length > 0) { if (selectedModForInstall && selectedVersionsForInstall.length > 0) {
await installMod(selectedModForInstall, selectedVersionsForInstall);
setIsInstallDialogOpen(false); setIsInstallDialogOpen(false);
toast.promise(installMod(selectedModForInstall, selectedVersionsForInstall), {
loading: `Installing mod: ${selectedModForInstall.name}`,
success: `Mod sucessfully installed: ${selectedModForInstall.name}`,
error: (e) => `${selectedModForInstall.name} Failed: ${e}`
}).then(async () => {
if (activeTab === "installed") { if (activeTab === "installed") {
const m = await getInstalledMods(version.id); const m = await getInstalledMods(version.id);
setMods(m); setMods(m);
} }
}).catch(console.error);
} }
}; };
@@ -170,7 +186,7 @@ const ModsSection: React.FC<ModsSectionProps> = ({ version }) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="font-bold text-sm truncate">{mod.name}</h3> <h3 className="font-bold text-sm truncate">{mod.name}</h3>
<span className="text-[10px] text-saturn-text-secondary font-medium px-1.5 py-0.5 rounded bg-white/5"> <span className="text-[10px] text-saturn-text-secondary font-medium px-1.5 py-0.5 rounded bg-white/5">
v{mod.version} {mod.version}
</span> </span>
<span className="text-[10px] text-saturn-text-secondary"> <span className="text-[10px] text-saturn-text-secondary">
by {mod.author} by {mod.author}
@@ -200,7 +216,7 @@ const ModsSection: React.FC<ModsSectionProps> = ({ version }) => {
)} )}
{mod.enabled ? "Enabled" : "Disabled"} {mod.enabled ? "Enabled" : "Disabled"}
</button> </button>
<button className="p-2 text-saturn-text-secondary hover:text-red-400 transition-colors"> <button className="p-2 text-saturn-text-secondary hover:text-red-400 transition-colors" onClick={() => handleRemove(mod.id)}>
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
</> </>
+2 -2
View File
@@ -45,9 +45,9 @@ const SettingsSection: React.FC = () => {
<div className="max-w-4xl mx-auto h-full flex flex-col gap-8"> <div className="max-w-4xl mx-auto h-full flex flex-col gap-8">
<div className="flex items-center justify-between border-b border-saturn-border pb-6"> <div className="flex items-center justify-between border-b border-saturn-border pb-6">
<div> <div>
<h1 className="text-2xl font-bold">Performance Settings</h1> <h1 className="text-2xl font-bold">Settings</h1>
<p className="text-sm text-saturn-text-secondary"> <p className="text-sm text-saturn-text-secondary">
Configure how Saturn Client interacts with your hardware. Configure how Saturn Launcher launches your game
</p> </p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
+1 -1
View File
@@ -1,5 +1,5 @@
import { Play, Puzzle, Settings } from "lucide-react"; import { Play, Puzzle, Settings } from "lucide-react";
import Logo from "@/assets/logo.png"; import Logo from "@/assets/logo_no_bg.png";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export default function Sidebar({ active, setActive }: any) { export default function Sidebar({ active, setActive }: any) {
+11
View File
@@ -17,6 +17,13 @@ export async function disableMod(
await invoke("disable_mod", { version, fileName: modId }); await invoke("disable_mod", { version, fileName: modId });
} }
export async function removeMod(
version: string,
modId: string,
): Promise<void> {
await invoke("remove_mod", { version, fileName: modId });
}
export async function discoverMods(query: string): Promise<Mod[]> { export async function discoverMods(query: string): Promise<Mod[]> {
const url = new URL("https://api.modrinth.com/v2/search"); const url = new URL("https://api.modrinth.com/v2/search");
@@ -73,3 +80,7 @@ export async function getVersion(): Promise<string> {
export async function updateVersion(version: string): Promise<void> { export async function updateVersion(version: string): Promise<void> {
await invoke("update_version", { version }); await invoke("update_version", { version });
} }
export async function installPaths(version: string, paths: string[]): Promise<void> {
await invoke("install_paths", { version, paths });
}