16 Commits
Author SHA1 Message Date
selimaj-dev f1830f63f5 Merge pull request #17 from saturnclientmc/added-versions
Tauri Release Build / Build (ubuntu-latest) (release) Failing after 1m53s
Tauri Release Build / Build (macos-latest) (release) Canceled after 0s
Tauri Release Build / Build (windows-latest) (release) Canceled after 0s
Tauri Release Build / Upload to GitHub Release (release) Canceled after 0s
Added versions
2026-04-07 22:17:25 +02:00
Kr4ight f9752b3ea6 Update: version to 0.1.3 2026-04-07 22:11:59 +02:00
Kr4ight 955890dabf Fix: Mods tab 2026-04-07 22:10:57 +02:00
Kr4ight a7842e2c12 Added 1.21.7-1.21.11 2026-04-07 22:07:09 +02:00
Kr4ight 615ac0c530 Fixed reset
Tauri Release Build / Build (ubuntu-latest) (release) Failing after 34s
Tauri Release Build / Build (macos-latest) (release) Canceled after 0s
Tauri Release Build / Build (windows-latest) (release) Canceled after 0s
Tauri Release Build / Upload to GitHub Release (release) Canceled after 0s
2026-04-06 22:19:09 +02:00
Kr4ight 6d253aaf4f Merge pull request #16 from saturnclientmc/refactor
Refactor
2026-04-06 22:12:36 +02:00
Kr4ight ac9108f507 Small changes 2026-04-06 22:11:24 +02:00
Kr4ight 19b19ba08e Improved slider 2026-04-06 22:09:20 +02:00
Kr4ight 2bb686baaf Implemented optional sync 2026-04-06 22:05:27 +02:00
Kr4ight 38207a1f30 Improved settings 2026-04-06 21:52:33 +02:00
Kr4ight 43b9d5910e Updated schema settings 2026-04-06 21:31:09 +02:00
Kr4ight fba51d78b8 Working Instance Sync 2026-04-06 21:13:02 +02:00
Kr4ight 950d7f0a1d Obfuscated auth code log for security 2026-04-06 20:39:59 +02:00
Kr4ight 450ade4b53 Fix: auth not refreshing on launch #15 2026-04-06 20:36:41 +02:00
Kr4ight 9769a2d6c1 Implemented state 2026-04-06 20:02:56 +02:00
Kr4ight 725ac2187f Implementing features 2026-04-06 19:50:56 +02:00
19 changed files with 520 additions and 124 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "saturn-launcher",
"private": true,
"version": "0.1.1",
"version": "0.1.3",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -3887,7 +3887,7 @@ dependencies = [
[[package]]
name = "saturn-launcher"
version = "0.1.1"
version = "0.1.3"
dependencies = [
"futures",
"lyceris",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "saturn-launcher"
version = "0.1.1"
version = "0.1.3"
description = "A minecraft launcher for Saturn Client"
authors = ["SaturnClient"]
edition = "2021"
+123
View File
@@ -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")
}
+78
View File
@@ -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(())
}
}
+6 -3
View File
@@ -61,7 +61,7 @@ pub fn auth_create_link(app: AppHandle) -> Result<String, String> {
// Check for auth code
if url_str.contains("code=") {
if let Some(code) = extract_code(&url_str) {
println!("Auth code received: {}", code);
println!("Auth code received: {} ({})", &code[0..5], code.len());
let _ = app_handle.emit("auth-code", code);
@@ -125,8 +125,11 @@ pub async fn auth_refresh(uuid: String) -> Result<MinecraftAccount, String> {
}
#[command]
pub fn auth_list() -> Result<Vec<MinecraftAccount>, String> {
Ok(load_accounts())
pub fn auth_list() -> Result<Vec<(String, String)>, String> {
Ok(load_accounts()
.iter()
.map(|account| (account.uuid.clone(), account.username.clone()))
.collect())
}
#[command]
+18 -7
View File
@@ -16,7 +16,7 @@ use lyceris::{
use tauri::{Emitter as _, State};
use crate::{GameVersion, SharedState};
use crate::{features::FeatureState, GameVersion, SharedState};
pub fn launcher_dir() -> PathBuf {
if cfg!(target_os = "macos") {
@@ -47,6 +47,7 @@ pub fn launcher_dir() -> PathBuf {
pub async fn launch_game(
app: tauri::AppHandle,
state: State<'_, SharedState>,
feature_state: State<'_, FeatureState>,
version: GameVersion,
account: MinecraftAccount,
) -> Result<(), String> {
@@ -95,14 +96,14 @@ pub async fn launch_game(
launcher_dir().join(&version.id),
version.id.clone(),
lyceris::auth::AuthMethod::Microsoft {
username: account.username,
xuid: account.xuid,
uuid: account.uuid,
access_token: account.access_token,
refresh_token: account.refresh_token,
username: account.username.clone(),
xuid: account.xuid.clone(),
uuid: account.uuid.clone(),
access_token: account.access_token.clone(),
refresh_token: account.refresh_token.clone(),
},
)
.loader(Fabric(version.loader_version).into())
.loader(Fabric(version.loader_version.clone()).into())
.memory(lyceris::minecraft::config::Memory::Megabyte(
state.lock().unwrap().settings.max_memory as u64,
))
@@ -124,6 +125,11 @@ pub async fn launch_game(
app.emit("launch-status", "Launching").unwrap();
feature_state
.inner()
.launch(&state, &version, &account)
.await?;
launch(&config, Some(&emitter))
.await
.map_err(|e| e.to_string())?
@@ -131,6 +137,11 @@ pub async fn launch_game(
.await
.map_err(|e| e.to_string())?;
feature_state
.inner()
.after_launch(&state, &version, &account)
.await?;
app.emit("is-launching", "false").unwrap();
app.emit("launch-status", "").unwrap();
emit_progress(&app, 0.0);
+8 -3
View File
@@ -1,4 +1,5 @@
pub mod commands;
pub mod features;
pub mod launcher;
use serde::{Deserialize, Serialize};
@@ -8,7 +9,7 @@ use std::{
sync::{Arc, Mutex},
};
use crate::launcher::launcher_dir;
use crate::{features::FeatureSettings, launcher::launcher_dir};
pub type SharedState = Arc<Mutex<AppState>>;
@@ -35,6 +36,7 @@ pub struct GameVersion {
#[derive(Clone, Serialize, Deserialize)]
pub struct LauncherSettings {
pub max_memory: u32,
pub features: FeatureSettings,
}
// --- App State ---
@@ -47,8 +49,11 @@ pub struct AppState {
impl Default for AppState {
fn default() -> Self {
Self {
settings: LauncherSettings { max_memory: 4096 },
version: String::from("1.21.6"),
settings: LauncherSettings {
max_memory: 2048,
features: FeatureSettings::default(),
},
version: String::from("1.21.11"),
}
}
}
+2 -1
View File
@@ -1,7 +1,7 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use saturn_launcher_lib::load_state;
use saturn_launcher_lib::{features::FeatureState, load_state};
use tauri::{Emitter, Manager, WindowEvent};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -9,6 +9,7 @@ fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.manage(load_state())
.manage(FeatureState::new())
.invoke_handler(tauri::generate_handler![
// Saturn Launcher Commands
saturn_launcher_lib::commands::get_installed_mods,
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "saturn-launcher",
"version": "0.1.1",
"version": "0.1.3",
"identifier": "org.saturnclient.saturn-launcher",
"build": {
"beforeDevCommand": "npm run dev",
@@ -12,7 +12,7 @@
"app": {
"windows": [
{
"title": "Saturn Launcher v0.1.1",
"title": "Saturn Launcher v0.1.3",
"width": 1000,
"height": 650,
"backgroundColor": "#0a0a0a",
+3 -4
View File
@@ -6,7 +6,6 @@ import ModsSection from "./components/ModsSection";
import SettingsSection from "./components/SettingsSection";
import { versions } from "./lib/launcher";
import { GameVersion } from "./lib/types";
import { MinecraftAccount } from "./lib/auth";
import { getVersion, installPaths, updateVersion } from "./lib/saturn";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { listen } from "@tauri-apps/api/event";
@@ -26,8 +25,8 @@ export default function App() {
const versionRef = useRef<HTMLDivElement | null>(null);
const accountRef = useRef<HTMLDivElement | null>(null);
const [accounts, setAccounts] = useState<MinecraftAccount[]>([]);
const [activeAccount, setActiveAccount] = useState<MinecraftAccount | null>(
const [accounts, setAccounts] = useState<[string, string][]>([]);
const [activeAccount, setActiveAccount] = useState<[string, string] | null>(
null,
);
@@ -137,7 +136,7 @@ export default function App() {
<div className="flex-1 p-8 overflow-y-scroll">
{active === "play" && (
<PlaySection version={selectedVersion} account={activeAccount} />
<PlaySection version={selectedVersion} activeAccount={activeAccount} setActiveAccount={setActiveAccount} accounts={accounts} setAccounts={setAccounts} />
)}
{active === "mods" && selectedVersion && (
<ModsSection version={selectedVersion} />
+9 -5
View File
@@ -22,6 +22,7 @@ import { motion, AnimatePresence } from "framer-motion";
import { GameVersion, Mod } from "@/lib/types";
import { versions } from "@/lib/launcher";
import toast from "react-hot-toast";
import { useRef } from "react";
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -44,18 +45,21 @@ const ModsSection: React.FC<ModsSectionProps> = ({ version }) => {
string[]
>([]);
const requestIdRef = useRef(0);
const loadMods = async () => {
const requestId = ++requestIdRef.current;
if (activeTab === "installed") {
const m = await getInstalledMods(version.id);
setMods(m);
if (activeTab === "installed") {
// Fix: If discover mods takes too long and the user switches tabs it glitches
if (requestId === requestIdRef.current) {
setMods(m);
}
} else {
const m = await discoverMods(searchQuery);
if (activeTab === "discover") {
// Fix: If discover mods takes too long and the user switches tabs it glitches
if (requestId === requestIdRef.current) {
setMods(m);
}
}
+15 -5
View File
@@ -1,19 +1,22 @@
import React, { useEffect, useState } from "react";
import { Play, Loader2 } from "lucide-react";
// import { launchGame } from "../services/SaturnApi";
import Bg from "@/assets/bg.png";
import { listen } from "@tauri-apps/api/event";
import { GameVersion } from "@/lib/types";
import { launch } from "@/lib/launcher";
import { MinecraftAccount } from "@/lib/auth";
import { authGetValid } from "@/lib/auth";
import { cn } from "@/lib/utils";
import { SetState } from "./layout/props";
interface PlaySectionProps {
version: GameVersion | null;
account: MinecraftAccount | null;
activeAccount: [string, string] | null;
setActiveAccount: SetState<[string, string] | null>,
accounts: [string, string][],
setAccounts: SetState<[string, string][]>,
}
const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
const PlaySection: React.FC<PlaySectionProps> = ({ version, activeAccount: account }) => {
const [isLaunching, setIsLaunching] = useState(false);
const [launchStatus, setLaunchStatus] = useState("");
const [progress, setProgress] = useState(0);
@@ -39,6 +42,13 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
};
}, []);
const handleLaunch = async () => {
if (version && account) {
const validAccount = await authGetValid(account[0]);
launch(version, validAccount);
}
}
return (
<div className="h-full relative">
{/* Background Image */}
@@ -54,7 +64,7 @@ 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>
<button
onClick={() => version && account && launch(version, account)}
onClick={handleLaunch}
disabled={isLaunching || !version || !account}
className={cn(
"relative btn-primary px-16 py-4 text-xl flex items-center gap-3 min-w-60 justify-center",
+205 -70
View File
@@ -1,9 +1,210 @@
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 { motion } from "framer-motion";
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 [settings, setSettings] = useState<LauncherSettings | null>(null);
const [isSaving, setIsSaving] = useState(false);
@@ -32,11 +233,7 @@ const SettingsSection: React.FC = () => {
};
const handleReset = async () => {
// In a real app, this would reset to defaults in the service
const defaultSettings: LauncherSettings = {
max_memory: 4096,
};
setSettings(defaultSettings);
setSettings(buildDefaultSettings(settingsSchema) as LauncherSettings);
};
if (!settings) return null;
@@ -70,69 +267,7 @@ const SettingsSection: React.FC = () => {
</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>
{settingsSchema.map((item) => (<SettingItem item={item} settings={settings} setSettings={setSettings} />))}
</div>
</div>
);
+10 -11
View File
@@ -7,7 +7,6 @@ import {
authRemove,
authCreateLink,
authLogin,
MinecraftAccount,
} from "@/lib/auth";
import { listen } from "@tauri-apps/api/event";
@@ -64,7 +63,7 @@ export default function AccountDropdown({
}
// Switch account
function switchAccount(acc: MinecraftAccount) {
function switchAccount(acc: [string, string]) {
setActiveAccount(acc);
setIsAccountDropdownOpen(false);
}
@@ -73,7 +72,7 @@ export default function AccountDropdown({
async function logout() {
if (!activeAccount) return;
await authRemove(activeAccount.uuid);
await authRemove(activeAccount[0]);
const updated = await authList();
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 (
<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">
{activeAccount && (
<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"
/>
)}
@@ -123,7 +122,7 @@ export default function AccountDropdown({
Playing as
</p>
<p className="text-sm font-bold text-white">
{activeAccount?.username || "No Account"}
{activeAccount?.[1] || "No Account"}
</p>
</div>
</button>
@@ -140,17 +139,17 @@ export default function AccountDropdown({
{/* Current account */}
{activeAccount && (
<UserComponent
username={activeAccount.username}
uuid={activeAccount.uuid}
username={activeAccount[1]}
uuid={activeAccount[0]}
/>
)}
{/* Other accounts */}
{otherAccounts.map((acc) => (
<UserComponent
key={acc.uuid}
username={acc.username}
uuid={acc.uuid}
key={acc[0]}
username={acc[1]}
uuid={acc[0]}
onClick={() => switchAccount(acc)}
/>
))}
+4 -5
View File
@@ -1,4 +1,3 @@
import { MinecraftAccount } from "@/lib/auth";
import { GameVersion } from "@/lib/types";
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
@@ -17,9 +16,9 @@ export interface Props {
versionRef: React.RefObject<HTMLDivElement | null>;
accountRef: React.RefObject<HTMLDivElement | null>;
accounts: MinecraftAccount[];
setAccounts: SetState<MinecraftAccount[]>;
accounts: [string, string][];
setAccounts: SetState<[string, string][]>;
activeAccount: MinecraftAccount | null;
setActiveAccount: SetState<MinecraftAccount | null>;
activeAccount: [string, string] | null;
setActiveAccount: SetState<[string, string] | null>;
}
+2 -1
View File
@@ -33,8 +33,9 @@ export async function authRefresh(uuid: string): Promise<MinecraftAccount> {
/**
* 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");
}
+28 -3
View File
@@ -6,17 +6,42 @@ export const versions: GameVersion[] = [
{
id: "1.21.4",
name: "Fabric 1.21.4",
loader_version: "0.18.6",
loader_version: "0.19.0",
},
{
id: "1.21.5",
name: "Fabric 1.21.5",
loader_version: "0.18.6",
loader_version: "0.19.0",
},
{
id: "1.21.6",
name: "Fabric 1.21.6",
loader_version: "0.18.6",
loader_version: "0.19.0",
},
{
id: "1.21.7",
name: "Fabric 1.21.7",
loader_version: "0.19.0",
},
{
id: "1.21.8",
name: "Fabric 1.21.8",
loader_version: "0.19.0",
},
{
id: "1.21.9",
name: "Fabric 1.21.9",
loader_version: "0.19.0",
},
{
id: "1.21.10",
name: "Fabric 1.21.10",
loader_version: "0.19.0",
},
{
id: "1.21.11",
name: "Fabric 1.21.11",
loader_version: "0.19.0",
},
];
+3
View File
@@ -17,4 +17,7 @@ export interface GameVersion {
export interface LauncherSettings {
max_memory: number;
features: {
sync_options: boolean
}
}