@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||
}
|
||||
@@ -1,2 +1,7 @@
|
||||
# launcherv2
|
||||
A minecraft launcher for Saturn Client
|
||||
# Tauri + React + Typescript
|
||||
|
||||
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [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)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + React + Typescript</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "saturn-launcher",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.38.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "saturn-launcher"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "saturn_launcher_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,74 @@
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
|
||||
use crate::{GameVersion, LauncherSettings, Mod, SharedState};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_versions(state: State<SharedState>) -> Vec<GameVersion> {
|
||||
let state = state.lock().unwrap();
|
||||
state.versions.clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_installed_mods(state: State<SharedState>, version_id: String) -> Vec<Mod> {
|
||||
let state = state.lock().unwrap();
|
||||
state
|
||||
.mods
|
||||
.iter()
|
||||
.filter(|m| m.supported_versions.contains(&version_id))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn toggle_mod(state: State<SharedState>, mod_id: String) {
|
||||
let mut state = state.lock().unwrap();
|
||||
if let Some(mod_item) = state.mods.iter_mut().find(|m| m.id == mod_id) {
|
||||
mod_item.enabled = !mod_item.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn install_mod(state: State<SharedState>, mut mod_item: Mod) {
|
||||
let mut state = state.lock().unwrap();
|
||||
|
||||
if !state.mods.iter().any(|m| m.id == mod_item.id) {
|
||||
mod_item.enabled = true;
|
||||
state.mods.push(mod_item);
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_settings(state: State<SharedState>) -> LauncherSettings {
|
||||
state.lock().unwrap().settings.clone()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_settings(state: State<SharedState>, new_settings: LauncherSettings) {
|
||||
let mut state = state.lock().unwrap();
|
||||
state.settings = new_settings;
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn launch_game(app: AppHandle, version_id: String) -> Result<(), String> {
|
||||
std::thread::spawn(move || {
|
||||
app.emit("is-launching", "true").unwrap();
|
||||
app.emit("launch-status", "Preparing...").unwrap();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
app.emit("launch-status", "Verifying assets...").unwrap();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(700));
|
||||
app.emit("launch-status", "Applying Saturn optimizations...")
|
||||
.unwrap();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(700));
|
||||
app.emit("launch-status", "Starting game...").unwrap();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
app.emit("launch-status", "Game launched!").unwrap();
|
||||
|
||||
app.emit("is-launching", "false").unwrap();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
pub mod commands;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Mutex;
|
||||
|
||||
// --- Types ---
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Mod {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
pub enabled: bool,
|
||||
pub icon: Option<String>,
|
||||
pub supported_versions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct GameVersion {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub release_date: String,
|
||||
pub r#type: String, // "release" | "snapshot"
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct LauncherSettings {
|
||||
pub jvm_arguments: String,
|
||||
pub max_memory: u32,
|
||||
}
|
||||
|
||||
// --- App State ---
|
||||
pub struct AppState {
|
||||
pub versions: Vec<GameVersion>,
|
||||
pub mods: Vec<Mod>,
|
||||
pub settings: LauncherSettings,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
versions: vec![
|
||||
GameVersion {
|
||||
id: "1.20.1".into(),
|
||||
name: "Fabric 1.20.1".into(),
|
||||
release_date: "2023-06-07".into(),
|
||||
r#type: "release".into(),
|
||||
},
|
||||
GameVersion {
|
||||
id: "1.20.4".into(),
|
||||
name: "Fabric 1.20.4".into(),
|
||||
release_date: "2023-12-07".into(),
|
||||
r#type: "release".into(),
|
||||
},
|
||||
GameVersion {
|
||||
id: "1.21".into(),
|
||||
name: "Fabric 1.21".into(),
|
||||
release_date: "2024-06-13".into(),
|
||||
r#type: "release".into(),
|
||||
},
|
||||
],
|
||||
mods: vec![
|
||||
Mod {
|
||||
id: "sodium".into(),
|
||||
name: "Sodium".into(),
|
||||
version: "0.5.8".into(),
|
||||
author: "jellysquid3".into(),
|
||||
description: "Modern rendering engine for Minecraft.".into(),
|
||||
enabled: true,
|
||||
icon: None,
|
||||
supported_versions: vec!["1.20.1".into(), "1.20.4".into(), "1.21".into()],
|
||||
},
|
||||
Mod {
|
||||
id: "iris".into(),
|
||||
name: "Iris".into(),
|
||||
version: "1.7.0".into(),
|
||||
author: "coderbot".into(),
|
||||
description: "A modern shaders mod for Minecraft.".into(),
|
||||
enabled: true,
|
||||
icon: None,
|
||||
supported_versions: vec!["1.20.1".into(), "1.20.4".into(), "1.21".into()],
|
||||
},
|
||||
],
|
||||
settings: LauncherSettings {
|
||||
jvm_arguments: "-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200"
|
||||
.into(),
|
||||
max_memory: 4096,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SharedState = Mutex<AppState>;
|
||||
@@ -0,0 +1,23 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use saturn_launcher_lib::{commands::*, AppState};
|
||||
use std::sync::Mutex;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.manage(Mutex::new(AppState::default()))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_versions,
|
||||
get_installed_mods,
|
||||
toggle_mod,
|
||||
install_mod,
|
||||
get_settings,
|
||||
update_settings,
|
||||
launch_game
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "saturn-launcher",
|
||||
"version": "0.1.0",
|
||||
"identifier": "org.saturnclient.saturn-launcher",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "saturn-launcher",
|
||||
"width": 1000,
|
||||
"height": 650
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/[email protected]",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Play,
|
||||
Puzzle,
|
||||
Settings,
|
||||
ChevronDown,
|
||||
Check,
|
||||
User,
|
||||
LogOut,
|
||||
} from "lucide-react";
|
||||
import { getVersions } from "./services/LauncherService";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Logo from "@/assets/logo.png";
|
||||
|
||||
// Utility for merging tailwind classes
|
||||
function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// Components
|
||||
import PlaySection from "./components/PlaySection";
|
||||
import ModsSection from "./components/ModsSection";
|
||||
import SettingsSection from "./components/SettingsSection";
|
||||
import { GameVersion } from "./services/Types";
|
||||
|
||||
type Section = "play" | "mods" | "settings";
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [activeSection, setActiveSection] = useState<Section>("play");
|
||||
const [versions, setVersions] = useState<GameVersion[]>([]);
|
||||
const [selectedVersion, setSelectedVersion] = useState<GameVersion | null>(
|
||||
null,
|
||||
);
|
||||
const [isVersionDropdownOpen, setIsVersionDropdownOpen] = useState(false);
|
||||
const [isAccountDropdownOpen, setIsAccountDropdownOpen] = useState(false);
|
||||
|
||||
const versionRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const accountRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchVersions = async () => {
|
||||
const v = await getVersions();
|
||||
setVersions(v);
|
||||
if (v.length > 0) setSelectedVersion(v[0]);
|
||||
};
|
||||
fetchVersions();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as Node;
|
||||
|
||||
// Close version dropdown if clicked outside
|
||||
if (versionRef.current && !versionRef.current.contains(target)) {
|
||||
setIsVersionDropdownOpen(false);
|
||||
}
|
||||
|
||||
// Close account dropdown if clicked outside
|
||||
if (accountRef.current && !accountRef.current.contains(target)) {
|
||||
setIsAccountDropdownOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen bg-saturn-bg text-saturn-text-primary overflow-hidden font-sans">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-16 border-r border-saturn-border flex flex-col items-center py-6 gap-6 bg-saturn-panel/50">
|
||||
<div>
|
||||
<img src={Logo} alt="Saturn Logo" className="w-10 h-10" />
|
||||
</div>
|
||||
|
||||
<nav className="flex flex-col gap-4 h-full">
|
||||
<button
|
||||
onClick={() => setActiveSection("play")}
|
||||
className={cn("sidebar-icon", activeSection === "play" && "active")}
|
||||
title="Play"
|
||||
>
|
||||
<Play
|
||||
size={22}
|
||||
fill={activeSection === "play" ? "currentColor" : "none"}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveSection("mods")}
|
||||
className={cn("sidebar-icon", activeSection === "mods" && "active")}
|
||||
title="Mods"
|
||||
>
|
||||
<Puzzle
|
||||
size={22}
|
||||
fill={activeSection === "mods" ? "currentColor" : "none"}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveSection("settings")}
|
||||
className={cn(
|
||||
"sidebar-icon",
|
||||
activeSection === "settings" && "active",
|
||||
)}
|
||||
title="Settings"
|
||||
>
|
||||
<Settings
|
||||
size={22}
|
||||
fill={activeSection === "settings" ? "currentColor" : "none"}
|
||||
/>
|
||||
</button>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 flex flex-col min-w-0">
|
||||
{/* Top Bar */}
|
||||
<header className="h-14 border-b border-saturn-border flex items-center justify-between px-6 bg-saturn-panel/30 backdrop-blur-md z-100 py-10">
|
||||
{/* Version Selector */}
|
||||
<div ref={versionRef} className="relative">
|
||||
<div className="relative group overflow-visible inline-block">
|
||||
{/* Glow */}
|
||||
<div className="absolute -inset-1 bg-linear-to-r from-blue-600 to-cyan-500 rounded-md blur opacity-25 group-hover:opacity-50 transition duration-500 pointer-events-none" />
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsVersionDropdownOpen((prev) => !prev);
|
||||
setIsAccountDropdownOpen(false);
|
||||
}}
|
||||
className="relative z-10 flex items-center gap-2 px-3 py-1.5 rounded-md btn-primary text-sm font-medium"
|
||||
>
|
||||
<span>{selectedVersion?.name || "Loading..."}</span>
|
||||
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={cn(
|
||||
"transition-transform",
|
||||
isVersionDropdownOpen && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 mt-2 w-48 bg-saturn-panel border border-saturn-border rounded-lg shadow-2xl z-50 py-1 overflow-hidden transition-all duration-200 origin-top",
|
||||
isVersionDropdownOpen
|
||||
? "opacity-100 scale-100 translate-y-0 pointer-events-auto"
|
||||
: "opacity-0 scale-95 -translate-y-2 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
{versions.map((v) => (
|
||||
<button
|
||||
key={v.id}
|
||||
onClick={() => {
|
||||
setSelectedVersion(v);
|
||||
setIsVersionDropdownOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center justify-between px-4 py-2 text-sm hover:bg-saturn-accent/10 hover:text-saturn-accent transition-colors"
|
||||
>
|
||||
<span>{v.name}</span>
|
||||
{selectedVersion?.id === v.id && <Check size={14} />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account Selector */}
|
||||
<div ref={accountRef} className="relative group overflow-visible">
|
||||
{/* Glow */}
|
||||
<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 pointer-events-none duration-700" />
|
||||
|
||||
<button
|
||||
className="relative z-10 flex items-center gap-3 pl-20 pr-4 py-2 rounded-lg btn-primary"
|
||||
onClick={() => setIsAccountDropdownOpen(!isAccountDropdownOpen)}
|
||||
>
|
||||
{/* Avatar */}
|
||||
<div className="absolute -top-2 left-4 group-hover:scale-110 transition-transform duration-700 w-12 overflow-hidden z-20">
|
||||
<img
|
||||
src="https://render.crafty.gg/3d/bust/Kr4ight"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="leading-tight">
|
||||
<p className="text-[10px] text-white/80 uppercase tracking-wide">
|
||||
Playing as
|
||||
</p>
|
||||
<p className="text-sm font-bold text-white">SaturnDev</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full right-0 mt-2 w-48 bg-saturn-panel border border-saturn-border rounded-lg shadow-2xl z-50 py-1 overflow-hidden transition-all duration-200 origin-top",
|
||||
isAccountDropdownOpen
|
||||
? "opacity-100 scale-100 translate-y-0 pointer-events-auto"
|
||||
: "opacity-0 scale-95 -translate-y-2 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<div className="px-4 py-3 border-b border-saturn-border">
|
||||
<p className="text-sm font-bold">SaturnDev</p>
|
||||
<p className="text-xs text-saturn-text-secondary truncate">
|
||||
dev@saturnclient.com
|
||||
</p>
|
||||
</div>
|
||||
<button className="w-full flex items-center gap-2 px-4 py-2 text-sm hover:bg-white/5 transition-colors">
|
||||
<User size={14} />
|
||||
<span>Switch Account</span>
|
||||
</button>
|
||||
<button className="w-full flex items-center gap-2 px-4 py-2 text-sm hover:bg-red-500/10 text-red-400 transition-colors">
|
||||
<LogOut size={14} />
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Content Sections */}
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden p-8">
|
||||
{activeSection === "play" && selectedVersion && (
|
||||
<PlaySection version={selectedVersion} />
|
||||
)}
|
||||
{activeSection === "mods" && selectedVersion && (
|
||||
<ModsSection version={selectedVersion} />
|
||||
)}
|
||||
{activeSection === "settings" && <SettingsSection />}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,314 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Search,
|
||||
Download,
|
||||
Trash2,
|
||||
Package,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getInstalledMods,
|
||||
discoverMods,
|
||||
getVersions,
|
||||
toggleMod,
|
||||
installMod,
|
||||
} from "../services/LauncherService";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { GameVersion, Mod } from "@/services/Types";
|
||||
|
||||
function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
interface ModsSectionProps {
|
||||
version: GameVersion;
|
||||
}
|
||||
|
||||
const ModsSection: React.FC<ModsSectionProps> = ({ version }) => {
|
||||
const [activeTab, setActiveTab] = useState<"installed" | "discover">(
|
||||
"installed",
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [mods, setMods] = useState<Mod[]>([]);
|
||||
const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false);
|
||||
const [selectedModForInstall, setSelectedModForInstall] =
|
||||
useState<Mod | null>(null);
|
||||
const [selectedVersionsForInstall, setSelectedVersionsForInstall] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [allVersions, setAllVersions] = useState<GameVersion[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadMods = async () => {
|
||||
if (activeTab === "installed") {
|
||||
const m = await getInstalledMods(version.id);
|
||||
setMods(m);
|
||||
} else {
|
||||
const m = await discoverMods(searchQuery);
|
||||
setMods(m);
|
||||
}
|
||||
};
|
||||
loadMods();
|
||||
}, [activeTab, version.id, searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAllVersions = async () => {
|
||||
const v = await getVersions();
|
||||
setAllVersions(v);
|
||||
};
|
||||
loadAllVersions();
|
||||
}, []);
|
||||
|
||||
const handleToggleMod = async (modId: string) => {
|
||||
await toggleMod(modId);
|
||||
const m = await getInstalledMods(version.id);
|
||||
setMods(m);
|
||||
};
|
||||
|
||||
const openInstallDialog = (mod: Mod) => {
|
||||
setSelectedModForInstall(mod);
|
||||
setSelectedVersionsForInstall([version.id]);
|
||||
setIsInstallDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (selectedModForInstall && selectedVersionsForInstall.length > 0) {
|
||||
await installMod(selectedModForInstall, selectedVersionsForInstall);
|
||||
setIsInstallDialogOpen(false);
|
||||
if (activeTab === "installed") {
|
||||
const m = await getInstalledMods(version.id);
|
||||
setMods(m);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleVersionSelection = (versionId: string) => {
|
||||
setSelectedVersionsForInstall((prev) =>
|
||||
prev.includes(versionId)
|
||||
? prev.filter((v) => v !== versionId)
|
||||
: [...prev, versionId],
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto h-full flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 p-1 bg-saturn-panel border border-saturn-border rounded-lg">
|
||||
<button
|
||||
onClick={() => setActiveTab("installed")}
|
||||
className={cn(
|
||||
"px-4 py-1.5 text-sm font-medium rounded-md transition-all",
|
||||
activeTab === "installed"
|
||||
? "bg-white/5 text-white"
|
||||
: "text-saturn-text-secondary hover:text-white",
|
||||
)}
|
||||
>
|
||||
Installed Mods
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("discover")}
|
||||
className={cn(
|
||||
"px-4 py-1.5 text-sm font-medium rounded-md transition-all",
|
||||
activeTab === "discover"
|
||||
? "bg-white/5 text-white"
|
||||
: "text-saturn-text-secondary hover:text-white",
|
||||
)}
|
||||
>
|
||||
Discover
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<Search
|
||||
size={16}
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-saturn-text-secondary group-focus-within:text-saturn-accent transition-colors"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={
|
||||
activeTab === "installed"
|
||||
? "Search installed mods..."
|
||||
: "Search CurseForge/Modrinth..."
|
||||
}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="bg-saturn-panel border border-saturn-border rounded-lg pl-10 pr-4 py-2 text-sm w-64 focus:outline-none focus:border-saturn-accent/50 focus:ring-1 focus:ring-saturn-accent/50 transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-2">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{mods.map((mod) => (
|
||||
<motion.div
|
||||
layout
|
||||
key={mod.id}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="panel p-4 flex items-center gap-4 hover:border-white/10 transition-colors"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-lg bg-zinc-800 border border-saturn-border flex items-center justify-center shrink-0">
|
||||
<Package size={24} className="text-saturn-text-secondary" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<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">
|
||||
v{mod.version}
|
||||
</span>
|
||||
<span className="text-[10px] text-saturn-text-secondary">
|
||||
by {mod.author}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-saturn-text-secondary mt-1 truncate max-w-xl">
|
||||
{mod.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{activeTab === "installed" ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleToggleMod(mod.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-medium transition-all",
|
||||
mod.enabled
|
||||
? "bg-green-500/10 text-green-400 border border-green-500/20"
|
||||
: "bg-red-500/10 text-red-400 border border-red-500/20",
|
||||
)}
|
||||
>
|
||||
{mod.enabled ? (
|
||||
<CheckCircle2 size={14} />
|
||||
) : (
|
||||
<XCircle size={14} />
|
||||
)}
|
||||
{mod.enabled ? "Enabled" : "Disabled"}
|
||||
</button>
|
||||
<button className="p-2 text-saturn-text-secondary hover:text-red-400 transition-colors">
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => openInstallDialog(mod)}
|
||||
className="flex items-center gap-2 px-4 py-1.5 bg-saturn-accent/10 text-saturn-accent hover:bg-saturn-accent hover:text-white rounded-md text-xs font-bold transition-all border border-saturn-accent/20"
|
||||
>
|
||||
<Download size={14} />
|
||||
INSTALL
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{mods.length === 0 && (
|
||||
<div className="h-64 flex flex-col items-center justify-center text-saturn-text-secondary opacity-50">
|
||||
<Package size={48} className="mb-4" strokeWidth={1} />
|
||||
<p>No mods found.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Install Dialog */}
|
||||
<AnimatePresence>
|
||||
{isInstallDialogOpen && selectedModForInstall && (
|
||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-100 flex items-center justify-center p-6">
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0.9, opacity: 0 }}
|
||||
className="bg-saturn-panel border border-saturn-border rounded-xl w-full max-w-md shadow-2xl overflow-hidden"
|
||||
>
|
||||
<div className="p-6 border-b border-saturn-border flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold">
|
||||
Install {selectedModForInstall.name}
|
||||
</h2>
|
||||
<p className="text-xs text-saturn-text-secondary">
|
||||
Select versions to install this mod to.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsInstallDialogOpen(false)}
|
||||
className="text-saturn-text-secondary hover:text-white"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-3 max-h-64 overflow-y-auto">
|
||||
{allVersions.map((v) => {
|
||||
const isSupported =
|
||||
selectedModForInstall.supported_versions.includes(v.id);
|
||||
const isSelected = selectedVersionsForInstall.includes(v.id);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={v.id}
|
||||
onClick={() =>
|
||||
isSupported && toggleVersionSelection(v.id)
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center justify-between p-3 rounded-lg border transition-all cursor-pointer",
|
||||
isSelected
|
||||
? "bg-saturn-accent/10 border-saturn-accent/50"
|
||||
: "bg-white/5 border-transparent hover:border-white/10",
|
||||
!isSupported &&
|
||||
"opacity-40 cursor-not-allowed grayscale",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
"w-5 h-5 rounded border flex items-center justify-center transition-all",
|
||||
isSelected
|
||||
? "bg-saturn-accent border-saturn-accent"
|
||||
: "border-saturn-border",
|
||||
)}
|
||||
>
|
||||
{isSelected && (
|
||||
<CheckCircle2 size={14} className="text-white" />
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-medium">{v.name}</span>
|
||||
</div>
|
||||
{!isSupported && (
|
||||
<span className="text-[10px] text-red-400 font-bold uppercase">
|
||||
Unsupported
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-white/5 flex gap-3">
|
||||
<button
|
||||
onClick={() => setIsInstallDialogOpen(false)}
|
||||
className="flex-1 py-2 text-sm font-medium hover:bg-white/5 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleInstall}
|
||||
disabled={selectedVersionsForInstall.length === 0}
|
||||
className="flex-1 py-2 text-sm font-bold bg-saturn-accent hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-all shadow-lg shadow-saturn-accent/20"
|
||||
>
|
||||
Confirm Install
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModsSection;
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Play, Loader2 } from "lucide-react";
|
||||
import { launchGame } from "../services/LauncherService";
|
||||
import Bg from "@/assets/bg.png";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { GameVersion } from "@/services/Types";
|
||||
|
||||
interface PlaySectionProps {
|
||||
version: GameVersion;
|
||||
}
|
||||
|
||||
const PlaySection: React.FC<PlaySectionProps> = ({ version }) => {
|
||||
const [isLaunching, setIsLaunching] = useState(false);
|
||||
const [launchStatus, setLaunchStatus] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const unlistenLaunchStatus = listen<string>("launch-status", (event) => {
|
||||
setLaunchStatus(event.payload);
|
||||
});
|
||||
|
||||
const unlistenIsLaunching = listen<string>("is-launching", (event) => {
|
||||
setIsLaunching(event.payload === "true");
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlistenLaunchStatus.then((f) => f());
|
||||
unlistenIsLaunching.then((f) => f());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full relative">
|
||||
{/* Background Image */}
|
||||
<img
|
||||
src={Bg}
|
||||
alt="Background"
|
||||
className="w-full h-full object-cover rounded-2xl brightness-50"
|
||||
/>
|
||||
|
||||
{/* Centered Overlay */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="relative group">
|
||||
<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
|
||||
onClick={() => launchGame(version.id)}
|
||||
disabled={isLaunching}
|
||||
className="relative btn-primary px-16 py-4 text-xl flex items-center gap-3 min-w-60 justify-center"
|
||||
>
|
||||
{isLaunching ? (
|
||||
<>
|
||||
<Loader2 size={24} className="animate-spin" />
|
||||
<span>{launchStatus}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play size={24} fill="currentColor" />
|
||||
<span>LAUNCH {version.id}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlaySection;
|
||||
@@ -0,0 +1,193 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
Cpu,
|
||||
MemoryStick,
|
||||
Save,
|
||||
RefreshCcw,
|
||||
Info,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { getSettings, updateSettings } from "../services/LauncherService";
|
||||
import { motion } from "framer-motion";
|
||||
import { LauncherSettings } from "@/services/Types";
|
||||
|
||||
const SettingsSection: React.FC = () => {
|
||||
const [settings, setSettings] = useState<LauncherSettings | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const loadSettings = async () => {
|
||||
const s = await getSettings();
|
||||
setSettings(s);
|
||||
};
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!settings) return;
|
||||
setIsSaving(true);
|
||||
setSaveStatus("Saving...");
|
||||
|
||||
await updateSettings(settings);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsSaving(false);
|
||||
setSaveStatus("Settings saved");
|
||||
setTimeout(() => setSaveStatus(""), 2000);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
// In a real app, this would reset to defaults in the service
|
||||
const defaultSettings: LauncherSettings = {
|
||||
jvm_arguments:
|
||||
"-XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200",
|
||||
max_memory: 4096,
|
||||
};
|
||||
setSettings(defaultSettings);
|
||||
};
|
||||
|
||||
if (!settings) return null;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<h1 className="text-2xl font-bold">Performance Settings</h1>
|
||||
<p className="text-sm text-saturn-text-secondary">
|
||||
Configure how Saturn Client interacts with your hardware.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="p-2 text-saturn-text-secondary hover:text-white transition-colors"
|
||||
title="Reset to Defaults"
|
||||
>
|
||||
<RefreshCcw size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="btn-primary flex items-center gap-2 py-2 px-6 text-sm font-bold"
|
||||
>
|
||||
<Save size={16} />
|
||||
{saveStatus || "SAVE SETTINGS"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8">
|
||||
{/* Memory Allocation */}
|
||||
<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>
|
||||
|
||||
{/* JVM Arguments */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
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">
|
||||
<Cpu size={18} />
|
||||
</div>
|
||||
<h2 className="text-lg font-bold">Custom JVM Arguments</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-xs text-saturn-text-secondary leading-relaxed">
|
||||
Advanced flags for the Java Virtual Machine. Saturn Client
|
||||
includes optimized defaults, but you can override them here.
|
||||
</p>
|
||||
|
||||
<textarea
|
||||
value={settings.jvm_arguments}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, jvm_arguments: e.target.value })
|
||||
}
|
||||
className="w-full h-32 bg-saturn-bg border border-saturn-border rounded-lg p-4 text-sm font-mono text-saturn-text-secondary focus:outline-none focus:border-saturn-accent/50 focus:ring-1 focus:ring-saturn-accent/50 transition-all resize-none"
|
||||
placeholder="-XX:+UseG1GC..."
|
||||
/>
|
||||
|
||||
<div className="p-3 bg-yellow-500/5 border border-yellow-500/10 rounded-lg flex gap-3">
|
||||
<AlertTriangle
|
||||
size={16}
|
||||
className="text-yellow-500 shrink-0 mt-0.5"
|
||||
/>
|
||||
<p className="text-xs text-yellow-500/80 leading-relaxed">
|
||||
Incorrect JVM arguments can prevent the game from starting. Only
|
||||
modify if you know what you're doing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsSection;
|
||||
@@ -0,0 +1,55 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-saturn-bg: #0a0a0a;
|
||||
--color-saturn-panel: #141414;
|
||||
--color-saturn-accent: #3b82f6;
|
||||
--color-saturn-accent-glow: rgba(59, 130, 246, 0.3);
|
||||
--color-saturn-text-primary: #f4f4f5;
|
||||
--color-saturn-text-secondary: #a1a1aa;
|
||||
--color-saturn-border: #27272a;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-saturn-bg text-saturn-text-primary font-sans antialiased;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply bg-saturn-accent hover:bg-blue-600 text-white font-medium py-2 px-6 rounded-md transition-all duration-200 shadow-[0_0_15px_rgba(59,130,246,0.2)] hover:shadow-[0_0_20px_rgba(59,130,246,0.4)];
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
@apply w-10 h-10 flex items-center justify-center rounded-lg transition-all duration-200 text-saturn-text-secondary hover:text-white hover:bg-white/5 cursor-pointer;
|
||||
}
|
||||
|
||||
.sidebar-icon.active {
|
||||
@apply text-saturn-accent bg-saturn-accent/10;
|
||||
}
|
||||
|
||||
.panel {
|
||||
@apply bg-saturn-panel border border-saturn-border rounded-lg overflow-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar for dense UI */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #27272a;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #3f3f46;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { GameVersion, LauncherSettings, Mod } from "./Types";
|
||||
|
||||
// --- Versions ---
|
||||
export async function getVersions(): Promise<GameVersion[]> {
|
||||
return await invoke("get_versions");
|
||||
}
|
||||
|
||||
// --- Mods ---
|
||||
export async function getInstalledMods(versionId: string): Promise<Mod[]> {
|
||||
return await invoke("get_installed_mods", { versionId });
|
||||
}
|
||||
|
||||
export async function toggleMod(modId: string): Promise<void> {
|
||||
await invoke("toggle_mod", { modId });
|
||||
}
|
||||
|
||||
export async function discoverMods(query: string): Promise<Mod[]> {
|
||||
const allDiscoverable: Mod[] = [
|
||||
{
|
||||
id: "modmenu",
|
||||
name: "Mod Menu",
|
||||
version: "7.2.2",
|
||||
author: "TerraformersMC",
|
||||
description: "Adds a mod menu to view the list of installed mods.",
|
||||
enabled: false,
|
||||
supported_versions: ["1.20.1", "1.20.4", "1.21"],
|
||||
},
|
||||
{
|
||||
id: "starlight",
|
||||
name: "Starlight",
|
||||
version: "1.1.2",
|
||||
author: "Spottedleaf",
|
||||
description: "Rewrites the light engine to fix performance issues.",
|
||||
enabled: false,
|
||||
supported_versions: ["1.20.1"],
|
||||
},
|
||||
{
|
||||
id: "ferritecore",
|
||||
name: "FerriteCore",
|
||||
version: "6.0.1",
|
||||
author: "malte0811",
|
||||
description: "Memory usage optimizations.",
|
||||
enabled: false,
|
||||
supported_versions: ["1.20.1", "1.20.4", "1.21"],
|
||||
},
|
||||
];
|
||||
|
||||
if (!query) return allDiscoverable;
|
||||
|
||||
return allDiscoverable.filter((m) =>
|
||||
m.name.toLowerCase().includes(query.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
export async function installMod(
|
||||
mod: Mod,
|
||||
versionsList: string[],
|
||||
): Promise<void> {
|
||||
await invoke("install_mod", { modItem: mod });
|
||||
}
|
||||
|
||||
// --- Settings ---
|
||||
export async function getSettings(): Promise<LauncherSettings> {
|
||||
return await invoke("get_settings");
|
||||
}
|
||||
|
||||
export async function updateSettings(
|
||||
newSettings: Partial<LauncherSettings>,
|
||||
): Promise<void> {
|
||||
await invoke("update_settings", { newSettings });
|
||||
}
|
||||
|
||||
// --- Launch ---
|
||||
export async function launchGame(
|
||||
versionId: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
return await invoke("launch_game", { versionId });
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export interface Mod {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
icon?: string;
|
||||
supported_versions: string[];
|
||||
}
|
||||
|
||||
export interface GameVersion {
|
||||
id: string;
|
||||
name: string;
|
||||
release_date: string;
|
||||
type: "release" | "snapshot";
|
||||
}
|
||||
|
||||
export interface LauncherSettings {
|
||||
jvm_arguments: string;
|
||||
max_memory: number;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [react(), tailwindcss()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
//
|
||||
// 1. prevent Vite from obscuring rust errors
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host,
|
||||
port: 1421,
|
||||
}
|
||||
: undefined,
|
||||
watch: {
|
||||
// 3. tell Vite to ignore watching `src-tauri`
|
||||
ignored: ["**/src-tauri/**"],
|
||||
},
|
||||
},
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": "/src",
|
||||
},
|
||||
},
|
||||
}));
|
||||