Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
615ac0c530 | ||
|
|
6d253aaf4f | ||
|
|
ac9108f507 | ||
|
|
19b19ba08e | ||
|
|
2bb686baaf | ||
|
|
38207a1f30 | ||
|
|
43b9d5910e | ||
|
|
fba51d78b8 | ||
|
|
950d7f0a1d | ||
|
|
450ade4b53 | ||
|
|
9769a2d6c1 | ||
|
|
725ac2187f | ||
|
|
1436950b90 | ||
|
|
7e9b663705 | ||
|
|
a134d9b67e | ||
|
|
3f2054c721 | ||
|
|
f33aadb3e0 | ||
|
|
807bbbc7e2 | ||
|
|
03815cce1e | ||
|
|
443a22bbed | ||
|
|
262bb89899 | ||
|
|
d77e0c8924 | ||
|
|
06f53935a0 | ||
|
|
e71617d535 | ||
|
|
d71c0c437d | ||
|
|
7167211f06 | ||
|
|
167bb33144 | ||
|
|
c0141eed35 | ||
|
|
492a427458 | ||
|
|
d3bdd1d3a5 | ||
|
|
369d7e889c | ||
|
|
d2e49c16fe | ||
|
|
3beabfca48 |
@@ -2,7 +2,7 @@ name: Tauri Release Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [published]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -53,7 +53,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: tauri-build-${{ matrix.os }}
|
name: tauri-build-${{ matrix.os }}
|
||||||
path: |
|
path: |
|
||||||
src-tauri/target/release/bundle/**
|
src-tauri/target/release/bundle/**/!(node_modules)/
|
||||||
|
src-tauri/target/release/bundle/**/*.AppImage
|
||||||
|
src-tauri/target/release/bundle/**/*.dmg
|
||||||
|
src-tauri/target/release/bundle/**/*.msi
|
||||||
|
src-tauri/target/release/bundle/**/*.exe
|
||||||
|
src-tauri/target/release/bundle/**/*.deb
|
||||||
|
src-tauri/target/release/bundle/**/*.rpm
|
||||||
|
|
||||||
release:
|
release:
|
||||||
name: Upload to GitHub Release
|
name: Upload to GitHub Release
|
||||||
@@ -72,6 +78,7 @@ jobs:
|
|||||||
- name: Upload to Release
|
- name: Upload to Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
tag_name: ${{ github.event.release.tag_name }}
|
||||||
files: artifacts/**/*
|
files: artifacts/**/*
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default"
|
"opener:default",
|
||||||
|
"core:window:allow-show"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
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>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
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>
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 984 B |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
use std::{collections::HashMap, fs, path::PathBuf};
|
||||||
|
|
||||||
|
use lyceris::auth::microsoft::MinecraftAccount;
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::{features::Feature, launcher::launcher_dir, GameVersion, SharedState};
|
||||||
|
|
||||||
|
pub struct InstanceSync {}
|
||||||
|
|
||||||
|
impl Default for InstanceSync {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Feature for InstanceSync {
|
||||||
|
fn launch(
|
||||||
|
&mut self,
|
||||||
|
state: &State<'_, SharedState>,
|
||||||
|
version: &GameVersion,
|
||||||
|
_account: &MinecraftAccount,
|
||||||
|
) -> super::FeatureResult {
|
||||||
|
if state.lock().unwrap().settings.features.sync_options {
|
||||||
|
sync_to_instance(version).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn after_launch(
|
||||||
|
&mut self,
|
||||||
|
state: &State<'_, SharedState>,
|
||||||
|
version: &GameVersion,
|
||||||
|
_account: &MinecraftAccount,
|
||||||
|
) -> super::FeatureResult {
|
||||||
|
if state.lock().unwrap().settings.features.sync_options {
|
||||||
|
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<String, String> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_options(map: &HashMap<String, String>) -> String {
|
||||||
|
map.iter()
|
||||||
|
.map(|(k, v)| format!("{k}:{v}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use lyceris::auth::microsoft::MinecraftAccount;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::State;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::{features::instance_sync::InstanceSync, GameVersion, SharedState};
|
||||||
|
|
||||||
|
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: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
pub trait Feature: Send + Sync {
|
||||||
|
fn launch(
|
||||||
|
&mut self,
|
||||||
|
state: &State<'_, SharedState>,
|
||||||
|
version: &GameVersion,
|
||||||
|
account: &MinecraftAccount,
|
||||||
|
) -> FeatureResult {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn after_launch(
|
||||||
|
&mut self,
|
||||||
|
state: &State<'_, SharedState>,
|
||||||
|
version: &GameVersion,
|
||||||
|
account: &MinecraftAccount,
|
||||||
|
) -> FeatureResult {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FeatureState(Mutex<Vec<Box<dyn Feature>>>);
|
||||||
|
|
||||||
|
impl FeatureState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(Mutex::new(vec![Box::new(InstanceSync::default())]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureState {
|
||||||
|
pub async fn launch(
|
||||||
|
&self,
|
||||||
|
state: &State<'_, SharedState>,
|
||||||
|
version: &GameVersion,
|
||||||
|
account: &MinecraftAccount,
|
||||||
|
) -> FeatureResult {
|
||||||
|
for feature in self.0.lock().await.iter_mut() {
|
||||||
|
feature.launch(state, version, account)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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(state, version, account)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
@@ -50,31 +50,34 @@ pub fn auth_create_link(app: AppHandle) -> Result<String, String> {
|
|||||||
|
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
|
|
||||||
tauri::WebviewWindowBuilder::new(&app, "auth", tauri::WebviewUrl::External(url))
|
std::thread::spawn(move || {
|
||||||
.title("Microsoft Login")
|
tauri::WebviewWindowBuilder::new(&app, "auth", tauri::WebviewUrl::External(url))
|
||||||
.inner_size(500.0, 700.0)
|
.title("Microsoft Login")
|
||||||
.resizable(false)
|
.inner_size(500.0, 700.0)
|
||||||
.on_navigation(move |nav_url| {
|
.resizable(false)
|
||||||
let url_str = nav_url.to_string();
|
.on_navigation(move |nav_url| {
|
||||||
|
let url_str = nav_url.to_string();
|
||||||
|
|
||||||
// Check for auth code
|
// Check for auth code
|
||||||
if url_str.contains("code=") {
|
if url_str.contains("code=") {
|
||||||
if let Some(code) = extract_code(&url_str) {
|
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);
|
let _ = app_handle.emit("auth-code", code);
|
||||||
|
|
||||||
if let Some(win) = app_handle.get_webview_window("auth") {
|
if let Some(win) = app_handle.get_webview_window("auth") {
|
||||||
let _ = win.close();
|
let _ = win.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Always allow navigation
|
// Always allow navigation
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
Ok(link)
|
Ok(link)
|
||||||
}
|
}
|
||||||
@@ -122,8 +125,11 @@ pub async fn auth_refresh(uuid: String) -> Result<MinecraftAccount, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub fn auth_list() -> Result<Vec<MinecraftAccount>, String> {
|
pub fn auth_list() -> Result<Vec<(String, String)>, String> {
|
||||||
Ok(load_accounts())
|
Ok(load_accounts()
|
||||||
|
.iter()
|
||||||
|
.map(|account| (account.uuid.clone(), account.username.clone()))
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
|
|||||||
@@ -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,20 +13,41 @@ 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::{features::FeatureState, 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(
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
state: State<'_, SharedState>,
|
state: State<'_, SharedState>,
|
||||||
|
feature_state: State<'_, FeatureState>,
|
||||||
version: GameVersion,
|
version: GameVersion,
|
||||||
account: MinecraftAccount,
|
account: MinecraftAccount,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -70,20 +92,18 @@ 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.clone(),
|
||||||
xuid: account.xuid,
|
xuid: account.xuid.clone(),
|
||||||
uuid: account.uuid,
|
uuid: account.uuid.clone(),
|
||||||
access_token: account.access_token,
|
access_token: account.access_token.clone(),
|
||||||
refresh_token: account.refresh_token,
|
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(
|
.memory(lyceris::minecraft::config::Memory::Megabyte(
|
||||||
state.lock().unwrap().settings.max_memory as u64,
|
state.lock().unwrap().settings.max_memory as u64,
|
||||||
))
|
))
|
||||||
@@ -105,6 +125,11 @@ pub async fn launch_game(
|
|||||||
|
|
||||||
app.emit("launch-status", "Launching").unwrap();
|
app.emit("launch-status", "Launching").unwrap();
|
||||||
|
|
||||||
|
feature_state
|
||||||
|
.inner()
|
||||||
|
.launch(&state, &version, &account)
|
||||||
|
.await?;
|
||||||
|
|
||||||
launch(&config, Some(&emitter))
|
launch(&config, Some(&emitter))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
@@ -112,6 +137,11 @@ pub async fn launch_game(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
feature_state
|
||||||
|
.inner()
|
||||||
|
.after_launch(&state, &version, &account)
|
||||||
|
.await?;
|
||||||
|
|
||||||
app.emit("is-launching", "false").unwrap();
|
app.emit("is-launching", "false").unwrap();
|
||||||
app.emit("launch-status", "").unwrap();
|
app.emit("launch-status", "").unwrap();
|
||||||
emit_progress(&app, 0.0);
|
emit_progress(&app, 0.0);
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod features;
|
||||||
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::{features::FeatureSettings, launcher::launcher_dir};
|
||||||
|
|
||||||
type SharedState = Mutex<AppState>;
|
pub type SharedState = Arc<Mutex<AppState>>;
|
||||||
|
|
||||||
// --- Types ---
|
// --- Types ---
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
@@ -31,6 +36,7 @@ pub struct GameVersion {
|
|||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct LauncherSettings {
|
pub struct LauncherSettings {
|
||||||
pub max_memory: u32,
|
pub max_memory: u32,
|
||||||
|
pub features: FeatureSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- App State ---
|
// --- App State ---
|
||||||
@@ -43,7 +49,10 @@ pub struct AppState {
|
|||||||
impl Default for AppState {
|
impl Default for AppState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
settings: LauncherSettings { max_memory: 4096 },
|
settings: LauncherSettings {
|
||||||
|
max_memory: 2048,
|
||||||
|
features: FeatureSettings::default(),
|
||||||
|
},
|
||||||
version: String::from("1.21.6"),
|
version: String::from("1.21.6"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +60,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 +70,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 +82,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
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![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::{features::FeatureState, 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() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.manage(load_state())
|
.manage(load_state())
|
||||||
|
.manage(FeatureState::new())
|
||||||
.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 +33,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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 +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",
|
||||||
|
"visible": false,
|
||||||
|
"dragDropEnabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import ModsSection from "./components/ModsSection";
|
|||||||
import SettingsSection from "./components/SettingsSection";
|
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 { getVersion, installPaths, updateVersion } from "./lib/saturn";
|
||||||
import { getVersion, updateVersion } from "./lib/saturn";
|
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";
|
||||||
|
|
||||||
@@ -23,8 +25,8 @@ export default function App() {
|
|||||||
const versionRef = useRef<HTMLDivElement | null>(null);
|
const versionRef = useRef<HTMLDivElement | null>(null);
|
||||||
const accountRef = useRef<HTMLDivElement | null>(null);
|
const accountRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const [accounts, setAccounts] = useState<MinecraftAccount[]>([]);
|
const [accounts, setAccounts] = useState<[string, string][]>([]);
|
||||||
const [activeAccount, setActiveAccount] = useState<MinecraftAccount | null>(
|
const [activeAccount, setActiveAccount] = useState<[string, string] | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -61,37 +63,88 @@ export default function App() {
|
|||||||
if (selectedVersion?.id) updateVersion(selectedVersion?.id);
|
if (selectedVersion?.id) updateVersion(selectedVersion?.id);
|
||||||
}, [selectedVersion]);
|
}, [selectedVersion]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const window = getCurrentWindow();
|
||||||
|
|
||||||
|
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 (
|
||||||
<div className="flex h-screen">
|
<>
|
||||||
<Sidebar active={active} setActive={setActive} />
|
<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)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
|
||||||
<main className="flex-1 flex flex-col">
|
<div className="flex h-screen">
|
||||||
<TopBar
|
<Sidebar active={active} setActive={setActive} />
|
||||||
versions={versions}
|
|
||||||
selectedVersion={selectedVersion}
|
|
||||||
setSelectedVersion={setSelectedVersion}
|
|
||||||
isVersionDropdownOpen={isVersionDropdownOpen}
|
|
||||||
setIsVersionDropdownOpen={setIsVersionDropdownOpen}
|
|
||||||
isAccountDropdownOpen={isAccountDropdownOpen}
|
|
||||||
setIsAccountDropdownOpen={setIsAccountDropdownOpen}
|
|
||||||
versionRef={versionRef}
|
|
||||||
accountRef={accountRef}
|
|
||||||
accounts={accounts}
|
|
||||||
setAccounts={setAccounts}
|
|
||||||
activeAccount={activeAccount}
|
|
||||||
setActiveAccount={setActiveAccount}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex-1 p-8 overflow-y-scroll">
|
<main className="flex-1 flex flex-col">
|
||||||
{active === "play" && (
|
<TopBar
|
||||||
<PlaySection version={selectedVersion} account={activeAccount} />
|
versions={versions}
|
||||||
)}
|
selectedVersion={selectedVersion}
|
||||||
{active === "mods" && selectedVersion && (
|
setSelectedVersion={setSelectedVersion}
|
||||||
<ModsSection version={selectedVersion} />
|
isVersionDropdownOpen={isVersionDropdownOpen}
|
||||||
)}
|
setIsVersionDropdownOpen={setIsVersionDropdownOpen}
|
||||||
{active === "settings" && <SettingsSection />}
|
isAccountDropdownOpen={isAccountDropdownOpen}
|
||||||
</div>
|
setIsAccountDropdownOpen={setIsAccountDropdownOpen}
|
||||||
</main>
|
versionRef={versionRef}
|
||||||
</div>
|
accountRef={accountRef}
|
||||||
|
accounts={accounts}
|
||||||
|
setAccounts={setAccounts}
|
||||||
|
activeAccount={activeAccount}
|
||||||
|
setActiveAccount={setActiveAccount}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 p-8 overflow-y-scroll">
|
||||||
|
{active === "play" && (
|
||||||
|
<PlaySection version={selectedVersion} activeAccount={activeAccount} setActiveAccount={setActiveAccount} accounts={accounts} setAccounts={setAccounts} />
|
||||||
|
)}
|
||||||
|
{active === "mods" && selectedVersion && (
|
||||||
|
<ModsSection version={selectedVersion} />
|
||||||
|
)}
|
||||||
|
{active === "settings" && <SettingsSection />}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 612 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -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);
|
||||||
if (activeTab === "installed") {
|
toast.promise(installMod(selectedModForInstall, selectedVersionsForInstall), {
|
||||||
const m = await getInstalledMods(version.id);
|
loading: `Installing mod: ${selectedModForInstall.name}`,
|
||||||
setMods(m);
|
success: `Mod sucessfully installed: ${selectedModForInstall.name}`,
|
||||||
}
|
error: (e) => `${selectedModForInstall.name} Failed: ${e}`
|
||||||
|
}).then(async () => {
|
||||||
|
if (activeTab === "installed") {
|
||||||
|
const m = await getInstalledMods(version.id);
|
||||||
|
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>
|
||||||
</>
|
</>
|
||||||
@@ -271,7 +287,7 @@ const ModsSection: React.FC<ModsSectionProps> = ({ version }) => {
|
|||||||
? "bg-saturn-accent/10 border-saturn-accent/50"
|
? "bg-saturn-accent/10 border-saturn-accent/50"
|
||||||
: "bg-white/5 border-transparent hover:border-white/10",
|
: "bg-white/5 border-transparent hover:border-white/10",
|
||||||
!isSupported &&
|
!isSupported &&
|
||||||
"opacity-40 cursor-not-allowed grayscale",
|
"opacity-40 cursor-not-allowed grayscale",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Play, Loader2 } from "lucide-react";
|
import { Play, Loader2 } from "lucide-react";
|
||||||
// import { launchGame } from "../services/SaturnApi";
|
|
||||||
import Bg from "@/assets/bg.png";
|
import Bg from "@/assets/bg.png";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { GameVersion } from "@/lib/types";
|
import { GameVersion } from "@/lib/types";
|
||||||
import { launch } from "@/lib/launcher";
|
import { launch } from "@/lib/launcher";
|
||||||
import { MinecraftAccount } from "@/lib/auth";
|
import { authGetValid } from "@/lib/auth";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { SetState } from "./layout/props";
|
||||||
|
|
||||||
interface PlaySectionProps {
|
interface PlaySectionProps {
|
||||||
version: GameVersion | null;
|
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<PlaySectionProps> = ({ version, account }) => {
|
const PlaySection: React.FC<PlaySectionProps> = ({ version, activeAccount: account }) => {
|
||||||
const [isLaunching, setIsLaunching] = useState(false);
|
const [isLaunching, setIsLaunching] = useState(false);
|
||||||
const [launchStatus, setLaunchStatus] = useState("");
|
const [launchStatus, setLaunchStatus] = useState("");
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
@@ -39,6 +42,13 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleLaunch = async () => {
|
||||||
|
if (version && account) {
|
||||||
|
const validAccount = await authGetValid(account[0]);
|
||||||
|
launch(version, validAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
@@ -54,12 +64,12 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
|
|||||||
<div className="absolute -inset-1 bg-linear-to-r from-blue-600 to-cyan-500 rounded-lg blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
<div className="absolute -inset-1 bg-linear-to-r from-blue-600 to-cyan-500 rounded-lg blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => version && account && launch(version, account)}
|
onClick={handleLaunch}
|
||||||
disabled={isLaunching || !version || !account}
|
disabled={isLaunching || !version || !account}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative btn-primary px-16 py-4 text-xl flex items-center gap-3 min-w-60 justify-center",
|
"relative btn-primary px-16 py-4 text-xl flex items-center gap-3 min-w-60 justify-center",
|
||||||
(isLaunching || !version || !account) &&
|
(isLaunching || !version || !account) &&
|
||||||
"cursor-not-allowed opacity-70",
|
"cursor-not-allowed opacity-70",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLaunching ? (
|
{isLaunching ? (
|
||||||
|
|||||||
@@ -1,9 +1,210 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { MemoryStick, Save, RefreshCcw, Info } from "lucide-react";
|
import { Save, RefreshCcw } from "lucide-react";
|
||||||
import { getSettings, updateSettings } from "../lib/saturn";
|
import { getSettings, updateSettings } from "../lib/saturn";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { LauncherSettings } from "@/lib/types";
|
import { LauncherSettings } from "@/lib/types";
|
||||||
|
|
||||||
|
export type SettingSchemaItem =
|
||||||
|
| SettingSchemaLeaf
|
||||||
|
| SettingSchemaGroup;
|
||||||
|
|
||||||
|
export interface SettingSchemaBase {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingSchemaLeaf extends SettingSchemaBase {
|
||||||
|
type: "slider" | "toggle" | "input";
|
||||||
|
|
||||||
|
key: string; // leaf key (resolved in context)
|
||||||
|
default: any;
|
||||||
|
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
format?: (value: any) => string;
|
||||||
|
ticks?: number[];
|
||||||
|
info?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SettingSchemaGroup extends SettingSchemaBase {
|
||||||
|
type: "group";
|
||||||
|
children: SettingSchemaItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsSchema: SettingSchemaItem[] = [
|
||||||
|
{
|
||||||
|
type: "group",
|
||||||
|
label: "Performance",
|
||||||
|
|
||||||
|
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`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: "group",
|
||||||
|
label: "Features",
|
||||||
|
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: "features.sync_options",
|
||||||
|
label: "Sync options.txt",
|
||||||
|
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<LauncherSettings | null>
|
||||||
|
>;
|
||||||
|
}> = ({ item, settings, setSettings }) => {
|
||||||
|
if (item.type === "group") {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 border-l border-saturn-border pl-4">
|
||||||
|
<div className="text-sm font-semibold text-saturn-text-secondary">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.children.map((child, i) => (
|
||||||
|
<SettingItem
|
||||||
|
key={i}
|
||||||
|
item={child}
|
||||||
|
settings={settings}
|
||||||
|
setSettings={setSettings}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = getValue(settings, item.key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{item.type === "slider" && (
|
||||||
|
<div key={item.key}>
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<span className="text-md font-medium">{item.label}</span>
|
||||||
|
<span className="text-md font-medium text-saturn-accent">{(item.format && item.format(value)) || value}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={item.min}
|
||||||
|
max={item.max}
|
||||||
|
step={item.step}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((prev) =>
|
||||||
|
prev
|
||||||
|
? setValue(prev, item.key, parseInt(e.target.value))
|
||||||
|
: prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full h-2 bg-saturn-border rounded-lg appearance-none cursor-pointer accent-saturn-accent"
|
||||||
|
/>
|
||||||
|
{item.ticks && (
|
||||||
|
<div className="relative w-full mt-2 h-6">
|
||||||
|
{item.ticks.map((tick) => {
|
||||||
|
const percent =
|
||||||
|
(((tick - item.min!) / (item.max! - item.min!)) * 100 - 50) *
|
||||||
|
0.97 +
|
||||||
|
50;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={tick}
|
||||||
|
className="absolute text-[10px] text-saturn-text-secondary font-bold uppercase tracking-widest -translate-x-1/2 whitespace-nowrap"
|
||||||
|
style={{ left: `${percent}%` }}
|
||||||
|
>
|
||||||
|
{tick / 1024} GB
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.type === "toggle" && (
|
||||||
|
<div className="flex gap-2 items-center" key={item.key}>
|
||||||
|
<input
|
||||||
|
className="appearance-none w-4 h-4 bg-saturn-panel border border-saturn-border rounded checked:bg-saturn-accent"
|
||||||
|
type="checkbox"
|
||||||
|
checked={value}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings((prev) =>
|
||||||
|
prev ? setValue(prev, item.key, e.target.checked) : prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function setNestedValue(obj: any, path: string, value: any) {
|
||||||
|
const keys = path.split(".");
|
||||||
|
let current = obj;
|
||||||
|
|
||||||
|
keys.forEach((key, index) => {
|
||||||
|
if (index === keys.length - 1) {
|
||||||
|
current[key] = value;
|
||||||
|
} else {
|
||||||
|
if (!current[key] || typeof current[key] !== "object") {
|
||||||
|
current[key] = {};
|
||||||
|
}
|
||||||
|
current = current[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildDefaultSettings = (schema: SettingSchemaItem[]): any => {
|
||||||
|
const result: any = {};
|
||||||
|
|
||||||
|
for (const item of schema) {
|
||||||
|
if (item.type === "group") {
|
||||||
|
Object.assign(result, buildDefaultSettings(item.children));
|
||||||
|
} else {
|
||||||
|
setNestedValue(result, item.key, item.default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const SettingsSection: React.FC = () => {
|
const SettingsSection: React.FC = () => {
|
||||||
const [settings, setSettings] = useState<LauncherSettings | null>(null);
|
const [settings, setSettings] = useState<LauncherSettings | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
@@ -32,11 +233,7 @@ const SettingsSection: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = async () => {
|
const handleReset = async () => {
|
||||||
// In a real app, this would reset to defaults in the service
|
setSettings(buildDefaultSettings(settingsSchema) as LauncherSettings);
|
||||||
const defaultSettings: LauncherSettings = {
|
|
||||||
max_memory: 4096,
|
|
||||||
};
|
|
||||||
setSettings(defaultSettings);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
@@ -45,9 +242,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">
|
||||||
@@ -70,69 +267,7 @@ const SettingsSection: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-8">
|
<div className="grid grid-cols-1 gap-8">
|
||||||
{/* Memory Allocation */}
|
{settingsSchema.map((item) => (<SettingItem item={item} settings={settings} setSettings={setSettings} />))}
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: 10 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="panel p-6 space-y-6"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-8 h-8 rounded-lg bg-saturn-accent/10 flex items-center justify-center text-saturn-accent">
|
|
||||||
<MemoryStick size={18} />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-lg font-bold">Memory Allocation</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm font-medium">Maximum Memory (RAM)</span>
|
|
||||||
<span className="text-lg font-bold text-saturn-accent">
|
|
||||||
{(settings.max_memory / 1024).toFixed(1)} GB
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1024"
|
|
||||||
max="16384"
|
|
||||||
step="512"
|
|
||||||
value={settings.max_memory}
|
|
||||||
onChange={(e) =>
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative w-full mt-2 h-6">
|
|
||||||
{[1024, 4096, 8192, 12288, 16384].map((value) => {
|
|
||||||
const percent =
|
|
||||||
(((value - 1024) / (16384 - 1024)) * 100 - 50) * 0.97 + 50;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={value}
|
|
||||||
className="absolute text-[10px] text-saturn-text-secondary font-bold uppercase tracking-widest -translate-x-1/2 whitespace-nowrap"
|
|
||||||
style={{ left: `${percent}%` }}
|
|
||||||
>
|
|
||||||
{value / 1024} GB
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-3 bg-blue-500/5 border border-blue-500/10 rounded-lg flex gap-3">
|
|
||||||
<Info size={16} className="text-blue-400 shrink-0 mt-0.5" />
|
|
||||||
<p className="text-xs text-blue-300/80 leading-relaxed">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
authRemove,
|
authRemove,
|
||||||
authCreateLink,
|
authCreateLink,
|
||||||
authLogin,
|
authLogin,
|
||||||
MinecraftAccount,
|
|
||||||
} from "@/lib/auth";
|
} from "@/lib/auth";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ export default function AccountDropdown({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Switch account
|
// Switch account
|
||||||
function switchAccount(acc: MinecraftAccount) {
|
function switchAccount(acc: [string, string]) {
|
||||||
setActiveAccount(acc);
|
setActiveAccount(acc);
|
||||||
setIsAccountDropdownOpen(false);
|
setIsAccountDropdownOpen(false);
|
||||||
}
|
}
|
||||||
@@ -73,7 +72,7 @@ export default function AccountDropdown({
|
|||||||
async function logout() {
|
async function logout() {
|
||||||
if (!activeAccount) return;
|
if (!activeAccount) return;
|
||||||
|
|
||||||
await authRemove(activeAccount.uuid);
|
await authRemove(activeAccount[0]);
|
||||||
|
|
||||||
const updated = await authList();
|
const updated = await authList();
|
||||||
setAccounts(updated);
|
setAccounts(updated);
|
||||||
@@ -91,7 +90,7 @@ export default function AccountDropdown({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const otherAccounts = accounts.filter((a) => a.uuid !== activeAccount?.uuid);
|
const otherAccounts = accounts.filter((a) => a[0] !== activeAccount?.[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={accountRef} className="relative group overflow-visible">
|
<div ref={accountRef} className="relative group overflow-visible">
|
||||||
@@ -106,7 +105,7 @@ export default function AccountDropdown({
|
|||||||
<div className="absolute -top-2 left-4 group-hover:scale-110 transition-transform duration-700 w-12 overflow-hidden z-20">
|
<div className="absolute -top-2 left-4 group-hover:scale-110 transition-transform duration-700 w-12 overflow-hidden z-20">
|
||||||
{activeAccount && (
|
{activeAccount && (
|
||||||
<img
|
<img
|
||||||
src={`https://render.crafty.gg/3d/bust/${activeAccount.uuid}`}
|
src={`https://render.crafty.gg/3d/bust/${activeAccount[0]}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -123,7 +122,7 @@ export default function AccountDropdown({
|
|||||||
Playing as
|
Playing as
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm font-bold text-white">
|
<p className="text-sm font-bold text-white">
|
||||||
{activeAccount?.username || "No Account"}
|
{activeAccount?.[1] || "No Account"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -140,17 +139,17 @@ export default function AccountDropdown({
|
|||||||
{/* Current account */}
|
{/* Current account */}
|
||||||
{activeAccount && (
|
{activeAccount && (
|
||||||
<UserComponent
|
<UserComponent
|
||||||
username={activeAccount.username}
|
username={activeAccount[1]}
|
||||||
uuid={activeAccount.uuid}
|
uuid={activeAccount[0]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Other accounts */}
|
{/* Other accounts */}
|
||||||
{otherAccounts.map((acc) => (
|
{otherAccounts.map((acc) => (
|
||||||
<UserComponent
|
<UserComponent
|
||||||
key={acc.uuid}
|
key={acc[0]}
|
||||||
username={acc.username}
|
username={acc[1]}
|
||||||
uuid={acc.uuid}
|
uuid={acc[0]}
|
||||||
onClick={() => switchAccount(acc)}
|
onClick={() => switchAccount(acc)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { MinecraftAccount } from "@/lib/auth";
|
|
||||||
import { GameVersion } from "@/lib/types";
|
import { GameVersion } from "@/lib/types";
|
||||||
|
|
||||||
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
|
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
|
||||||
@@ -17,9 +16,9 @@ export interface Props {
|
|||||||
versionRef: React.RefObject<HTMLDivElement | null>;
|
versionRef: React.RefObject<HTMLDivElement | null>;
|
||||||
accountRef: React.RefObject<HTMLDivElement | null>;
|
accountRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
|
||||||
accounts: MinecraftAccount[];
|
accounts: [string, string][];
|
||||||
setAccounts: SetState<MinecraftAccount[]>;
|
setAccounts: SetState<[string, string][]>;
|
||||||
|
|
||||||
activeAccount: MinecraftAccount | null;
|
activeAccount: [string, string] | null;
|
||||||
setActiveAccount: SetState<MinecraftAccount | null>;
|
setActiveAccount: SetState<[string, string] | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ export async function authRefresh(uuid: string): Promise<MinecraftAccount> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all stored accounts
|
* Get all stored accounts
|
||||||
|
* Format [UUID, Username]
|
||||||
*/
|
*/
|
||||||
export async function authList(): Promise<MinecraftAccount[]> {
|
export async function authList(): Promise<[string, string][]> {
|
||||||
return await invoke("auth_list");
|
return await invoke("auth_list");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,4 +17,7 @@ export interface GameVersion {
|
|||||||
|
|
||||||
export interface LauncherSettings {
|
export interface LauncherSettings {
|
||||||
max_memory: number;
|
max_memory: number;
|
||||||
|
features: {
|
||||||
|
sync_options: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||