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 install;
pub mod auth;
use directories::ProjectDirs;
use lyceris::minecraft::{
config::ConfigBuilder,
emitter::{Emitter, Event},
launch::launch,
loader::fabric::Fabric,
use lyceris::{
auth::microsoft::MinecraftAccount,
minecraft::{
config::ConfigBuilder,
emitter::{Emitter, Event},
launch::launch,
loader::fabric::Fabric,
},
};
use once_cell::sync::Lazy;
use crate::GameVersion;
pub static LAUNCHER_DIR: Lazy<ProjectDirs> = Lazy::new(|| {
ProjectDirs::from("org", "SaturnLauncher", "SaturnLauncher")
.expect("Failed to create project directories")
});
#[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
// and it uses tokio::Mutex for locking.
// 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 config = ConfigBuilder::new(
current_dir.join("1.21.4"),
"1.21.4".into(),
lyceris::auth::AuthMethod::Offline {
username: "Lyceris".into(),
uuid: None,
current_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,
},
)
.loader(Fabric("0.18.6".to_string()).into())
.loader(Fabric(version.loader_version).into())
.build();
println!("Starting installation");
+1 -23
View File
@@ -21,8 +21,7 @@ pub struct Mod {
pub struct GameVersion {
pub id: String,
pub name: String,
pub release_date: String,
pub r#type: String, // "release" | "snapshot"
pub loader_version: String,
}
#[derive(Clone, Serialize, Deserialize)]
@@ -33,7 +32,6 @@ pub struct LauncherSettings {
// --- App State ---
pub struct AppState {
pub versions: Vec<GameVersion>,
pub mods: Vec<Mod>,
pub settings: LauncherSettings,
}
@@ -41,26 +39,6 @@ pub struct AppState {
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(),
+12 -2
View File
@@ -6,6 +6,7 @@ import ModsSection from "./components/ModsSection";
import SettingsSection from "./components/SettingsSection";
import { versions } from "./lib/saturn";
import { GameVersion } from "./lib/types";
import { MinecraftAccount } from "./lib/auth";
type Section = "play" | "mods" | "settings";
@@ -21,6 +22,11 @@ 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>(
null,
);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
const target = event.target as Node;
@@ -62,11 +68,15 @@ export default function App() {
setIsAccountDropdownOpen={setIsAccountDropdownOpen}
versionRef={versionRef}
accountRef={accountRef}
accounts={accounts}
setAccounts={setAccounts}
activeAccount={activeAccount}
setActiveAccount={setActiveAccount}
/>
<div className="flex-1 p-8 overflow-y-scroll">
{active === "play" && selectedVersion && (
<PlaySection version={selectedVersion} />
{active === "play" && (
<PlaySection version={selectedVersion} account={activeAccount} />
)}
{active === "mods" && 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 { GameVersion } from "@/lib/types";
import { launch } from "@/lib/launcher";
import { MinecraftAccount } from "@/lib/auth";
import { cn } from "@/lib/utils";
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 [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>
<button
onClick={() => launch()}
disabled={isLaunching}
className="relative btn-primary px-16 py-4 text-xl flex items-center gap-3 min-w-60 justify-center"
onClick={() => version && account && launch(version, account)}
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",
(isLaunching || !version || !account) &&
"cursor-not-allowed opacity-70",
)}
>
{isLaunching ? (
<>
@@ -56,7 +63,7 @@ const PlaySection: React.FC<PlaySectionProps> = ({ version }) => {
) : (
<>
<Play size={24} fill="currentColor" />
<span>LAUNCH {version.id}</span>
<span>LAUNCH {version?.id}</span>
</>
)}
</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 { cn } from "@/lib/utils";
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";
interface Account {
uuid: string;
username: string;
}
function UserComponent({
username,
uuid,
@@ -43,10 +44,11 @@ export default function AccountDropdown({
isAccountDropdownOpen,
setIsAccountDropdownOpen,
accountRef,
activeAccount,
setActiveAccount,
accounts,
setAccounts,
}: Props) {
const [accounts, setAccounts] = useState<Account[]>([]);
const [active, setActive] = useState<Account | null>(null);
// Load accounts
useEffect(() => {
loadAccounts();
@@ -56,26 +58,26 @@ export default function AccountDropdown({
const accs = await authList();
setAccounts(accs);
if (accs.length && !active) {
setActive(accs[0]); // default
if (accs.length && !activeAccount) {
setActiveAccount(accs[0]); // default
}
}
// Switch account
function switchAccount(acc: Account) {
setActive(acc);
function switchAccount(acc: MinecraftAccount) {
setActiveAccount(acc);
setIsAccountDropdownOpen(false);
}
// Logout (remove account)
async function logout() {
if (!active) return;
if (!activeAccount) return;
await authRemove(active.uuid);
await authRemove(activeAccount.uuid);
const updated = await authList();
setAccounts(updated);
setActive(updated[0] || null);
setActiveAccount(updated[0] || null);
}
// 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 (
<div ref={accountRef} className="relative group overflow-visible">
@@ -102,13 +104,13 @@ export default function AccountDropdown({
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">
{active && (
{activeAccount && (
<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"
/>
)}
{!active && (
{!activeAccount && (
<img
src={`https://render.crafty.gg/3d/bust/MHF_Steve`}
className="w-full h-full object-cover"
@@ -121,7 +123,7 @@ export default function AccountDropdown({
Playing as
</p>
<p className="text-sm font-bold text-white">
{active?.username || "No Account"}
{activeAccount?.username || "No Account"}
</p>
</div>
</button>
@@ -136,8 +138,11 @@ export default function AccountDropdown({
)}
>
{/* Current account */}
{active && (
<UserComponent username={active.username} uuid={active.uuid} />
{activeAccount && (
<UserComponent
username={activeAccount.username}
uuid={activeAccount.uuid}
/>
)}
{/* Other accounts */}
+7
View File
@@ -1,3 +1,4 @@
import { MinecraftAccount } from "@/lib/auth";
import { GameVersion } from "@/lib/types";
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
@@ -15,4 +16,10 @@ export interface Props {
versionRef: 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 { GameVersion } from "./types";
import { MinecraftAccount } from "./auth";
/**
* Launch the game with the selected account and version
*/
export async function launch() {
return await invoke("launch_game");
export async function launch(version: GameVersion, account: MinecraftAccount) {
return await invoke("launch_game", { version, account });
}
+1
View File
@@ -6,6 +6,7 @@ export const versions: GameVersion[] = [
{
id: "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 {
id: string;
name: string;
loader_version: string;
}
export interface LauncherSettings {