Working launch with auth and version

This commit is contained in:
2026-04-04 19:55:19 +02:00
parent f822674bff
commit 9535b920fa
9 changed files with 89 additions and 70 deletions
+21 -13
View File
@@ -1,23 +1,28 @@
pub mod auth;
pub mod downloader; pub mod downloader;
pub mod install; pub mod install;
pub mod auth;
use directories::ProjectDirs; use directories::ProjectDirs;
use lyceris::minecraft::{ use lyceris::{
config::ConfigBuilder, auth::microsoft::MinecraftAccount,
emitter::{Emitter, Event}, minecraft::{
launch::launch, config::ConfigBuilder,
loader::fabric::Fabric, emitter::{Emitter, Event},
launch::launch,
loader::fabric::Fabric,
},
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::GameVersion;
pub static LAUNCHER_DIR: Lazy<ProjectDirs> = Lazy::new(|| { pub static LAUNCHER_DIR: Lazy<ProjectDirs> = Lazy::new(|| {
ProjectDirs::from("org", "SaturnLauncher", "SaturnLauncher") ProjectDirs::from("org", "SaturnLauncher", "SaturnLauncher")
.expect("Failed to create project directories") .expect("Failed to create project directories")
}); });
#[tauri::command] #[tauri::command]
pub async fn launch_game() -> Result<(), String> { pub async fn launch_game(version: GameVersion, account: MinecraftAccount) -> Result<(), String> {
// Emitter uses `EventEmitter` inside of it // Emitter uses `EventEmitter` inside of it
// and it uses tokio::Mutex for locking. // and it uses tokio::Mutex for locking.
// That causes emitter methods to be async. // That causes emitter methods to be async.
@@ -58,14 +63,17 @@ pub async fn launch_game() -> Result<(), String> {
let current_dir = LAUNCHER_DIR.data_dir(); let current_dir = LAUNCHER_DIR.data_dir();
let config = ConfigBuilder::new( let config = ConfigBuilder::new(
current_dir.join("1.21.4"), current_dir.join(&version.id),
"1.21.4".into(), version.id.clone(),
lyceris::auth::AuthMethod::Offline { lyceris::auth::AuthMethod::Microsoft {
username: "Lyceris".into(), username: account.username,
uuid: None, xuid: account.xuid,
uuid: account.uuid,
access_token: account.access_token,
refresh_token: account.refresh_token,
}, },
) )
.loader(Fabric("0.18.6".to_string()).into()) .loader(Fabric(version.loader_version).into())
.build(); .build();
println!("Starting installation"); println!("Starting installation");
+1 -23
View File
@@ -21,8 +21,7 @@ pub struct Mod {
pub struct GameVersion { pub struct GameVersion {
pub id: String, pub id: String,
pub name: String, pub name: String,
pub release_date: String, pub loader_version: String,
pub r#type: String, // "release" | "snapshot"
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
@@ -33,7 +32,6 @@ pub struct LauncherSettings {
// --- App State --- // --- App State ---
pub struct AppState { pub struct AppState {
pub versions: Vec<GameVersion>,
pub mods: Vec<Mod>, pub mods: Vec<Mod>,
pub settings: LauncherSettings, pub settings: LauncherSettings,
} }
@@ -41,26 +39,6 @@ pub struct AppState {
impl Default for AppState { impl Default for AppState {
fn default() -> Self { fn default() -> Self {
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![ mods: vec![
Mod { Mod {
id: "sodium".into(), id: "sodium".into(),
+12 -2
View File
@@ -6,6 +6,7 @@ import ModsSection from "./components/ModsSection";
import SettingsSection from "./components/SettingsSection"; import SettingsSection from "./components/SettingsSection";
import { versions } from "./lib/saturn"; import { versions } from "./lib/saturn";
import { GameVersion } from "./lib/types"; import { GameVersion } from "./lib/types";
import { MinecraftAccount } from "./lib/auth";
type Section = "play" | "mods" | "settings"; type Section = "play" | "mods" | "settings";
@@ -21,6 +22,11 @@ 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 [activeAccount, setActiveAccount] = useState<MinecraftAccount | null>(
null,
);
useEffect(() => { useEffect(() => {
function handleClickOutside(event: MouseEvent) { function handleClickOutside(event: MouseEvent) {
const target = event.target as Node; const target = event.target as Node;
@@ -62,11 +68,15 @@ export default function App() {
setIsAccountDropdownOpen={setIsAccountDropdownOpen} setIsAccountDropdownOpen={setIsAccountDropdownOpen}
versionRef={versionRef} versionRef={versionRef}
accountRef={accountRef} accountRef={accountRef}
accounts={accounts}
setAccounts={setAccounts}
activeAccount={activeAccount}
setActiveAccount={setActiveAccount}
/> />
<div className="flex-1 p-8 overflow-y-scroll"> <div className="flex-1 p-8 overflow-y-scroll">
{active === "play" && selectedVersion && ( {active === "play" && (
<PlaySection version={selectedVersion} /> <PlaySection version={selectedVersion} account={activeAccount} />
)} )}
{active === "mods" && selectedVersion && ( {active === "mods" && selectedVersion && (
<ModsSection version={selectedVersion} /> <ModsSection version={selectedVersion} />
+13 -6
View File
@@ -5,12 +5,15 @@ 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 { cn } from "@/lib/utils";
interface PlaySectionProps { interface PlaySectionProps {
version: GameVersion; version: GameVersion | null;
account: MinecraftAccount | null;
} }
const PlaySection: React.FC<PlaySectionProps> = ({ version }) => { const PlaySection: React.FC<PlaySectionProps> = ({ version, account }) => {
const [isLaunching, setIsLaunching] = useState(false); const [isLaunching, setIsLaunching] = useState(false);
const [launchStatus, setLaunchStatus] = useState(""); const [launchStatus, setLaunchStatus] = useState("");
@@ -44,9 +47,13 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version }) => {
<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={() => launch()} onClick={() => version && account && launch(version, account)}
disabled={isLaunching} disabled={isLaunching || !version || !account}
className="relative btn-primary px-16 py-4 text-xl flex items-center gap-3 min-w-60 justify-center" className={cn(
"relative btn-primary px-16 py-4 text-xl flex items-center gap-3 min-w-60 justify-center",
(isLaunching || !version || !account) &&
"cursor-not-allowed opacity-70",
)}
> >
{isLaunching ? ( {isLaunching ? (
<> <>
@@ -56,7 +63,7 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version }) => {
) : ( ) : (
<> <>
<Play size={24} fill="currentColor" /> <Play size={24} fill="currentColor" />
<span>LAUNCH {version.id}</span> <span>LAUNCH {version?.id}</span>
</> </>
)} )}
</button> </button>
+29 -24
View File
@@ -1,15 +1,16 @@
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { User, LogOut } from "lucide-react"; import { User, LogOut } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Props } from "./props"; import { Props } from "./props";
import { authList, authRemove, authCreateLink, authLogin } from "@/lib/auth"; import {
authList,
authRemove,
authCreateLink,
authLogin,
MinecraftAccount,
} from "@/lib/auth";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
interface Account {
uuid: string;
username: string;
}
function UserComponent({ function UserComponent({
username, username,
uuid, uuid,
@@ -43,10 +44,11 @@ export default function AccountDropdown({
isAccountDropdownOpen, isAccountDropdownOpen,
setIsAccountDropdownOpen, setIsAccountDropdownOpen,
accountRef, accountRef,
activeAccount,
setActiveAccount,
accounts,
setAccounts,
}: Props) { }: Props) {
const [accounts, setAccounts] = useState<Account[]>([]);
const [active, setActive] = useState<Account | null>(null);
// Load accounts // Load accounts
useEffect(() => { useEffect(() => {
loadAccounts(); loadAccounts();
@@ -56,26 +58,26 @@ export default function AccountDropdown({
const accs = await authList(); const accs = await authList();
setAccounts(accs); setAccounts(accs);
if (accs.length && !active) { if (accs.length && !activeAccount) {
setActive(accs[0]); // default setActiveAccount(accs[0]); // default
} }
} }
// Switch account // Switch account
function switchAccount(acc: Account) { function switchAccount(acc: MinecraftAccount) {
setActive(acc); setActiveAccount(acc);
setIsAccountDropdownOpen(false); setIsAccountDropdownOpen(false);
} }
// Logout (remove account) // Logout (remove account)
async function logout() { async function logout() {
if (!active) return; if (!activeAccount) return;
await authRemove(active.uuid); await authRemove(activeAccount.uuid);
const updated = await authList(); const updated = await authList();
setAccounts(updated); setAccounts(updated);
setActive(updated[0] || null); setActiveAccount(updated[0] || null);
} }
// Add account (open Microsoft login) // Add account (open Microsoft login)
@@ -89,7 +91,7 @@ export default function AccountDropdown({
}); });
} }
const otherAccounts = accounts.filter((a) => a.uuid !== active?.uuid); const otherAccounts = accounts.filter((a) => a.uuid !== activeAccount?.uuid);
return ( return (
<div ref={accountRef} className="relative group overflow-visible"> <div ref={accountRef} className="relative group overflow-visible">
@@ -102,13 +104,13 @@ export default function AccountDropdown({
onClick={() => setIsAccountDropdownOpen((prev: boolean) => !prev)} onClick={() => setIsAccountDropdownOpen((prev: boolean) => !prev)}
> >
<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">
{active && ( {activeAccount && (
<img <img
src={`https://render.crafty.gg/3d/bust/${active.uuid}`} src={`https://render.crafty.gg/3d/bust/${activeAccount.uuid}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
)} )}
{!active && ( {!activeAccount && (
<img <img
src={`https://render.crafty.gg/3d/bust/MHF_Steve`} src={`https://render.crafty.gg/3d/bust/MHF_Steve`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
@@ -121,7 +123,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">
{active?.username || "No Account"} {activeAccount?.username || "No Account"}
</p> </p>
</div> </div>
</button> </button>
@@ -136,8 +138,11 @@ export default function AccountDropdown({
)} )}
> >
{/* Current account */} {/* Current account */}
{active && ( {activeAccount && (
<UserComponent username={active.username} uuid={active.uuid} /> <UserComponent
username={activeAccount.username}
uuid={activeAccount.uuid}
/>
)} )}
{/* Other accounts */} {/* Other accounts */}
+7
View File
@@ -1,3 +1,4 @@
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>>;
@@ -15,4 +16,10 @@ 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[];
setAccounts: SetState<MinecraftAccount[]>;
activeAccount: MinecraftAccount | null;
setActiveAccount: SetState<MinecraftAccount | null>;
} }
+4 -2
View File
@@ -1,8 +1,10 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { GameVersion } from "./types";
import { MinecraftAccount } from "./auth";
/** /**
* Launch the game with the selected account and version * Launch the game with the selected account and version
*/ */
export async function launch() { export async function launch(version: GameVersion, account: MinecraftAccount) {
return await invoke("launch_game"); return await invoke("launch_game", { version, account });
} }
+1
View File
@@ -6,6 +6,7 @@ export const versions: GameVersion[] = [
{ {
id: "1.21.4", id: "1.21.4",
name: "Fabric 1.21.4", name: "Fabric 1.21.4",
loader_version: "0.18.6",
}, },
]; ];
+1
View File
@@ -12,6 +12,7 @@ export interface Mod {
export interface GameVersion { export interface GameVersion {
id: string; id: string;
name: string; name: string;
loader_version: string;
} }
export interface LauncherSettings { export interface LauncherSettings {