This commit is contained in:
2026-04-03 14:44:13 +02:00
parent 3c6c6ae749
commit 1fe9197b9b
19 changed files with 2351 additions and 246 deletions
+148 -48
View File
@@ -1,51 +1,151 @@
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import { invoke } from "@tauri-apps/api/core";
import "./App.css";
import React, { useState, useEffect } from 'react'
import { Play, Puzzle, Settings, ChevronDown, User, LogOut, Check } from 'lucide-react'
import { launcherService, type GameVersion } from './services/LauncherService'
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
function App() {
const [greetMsg, setGreetMsg] = useState("");
const [name, setName] = useState("");
async function greet() {
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
setGreetMsg(await invoke("greet", { name }));
}
return (
<main className="container">
<h1>Welcome to Tauri + React</h1>
<div className="row">
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
<form
className="row"
onSubmit={(e) => {
e.preventDefault();
greet();
}}
>
<input
id="greet-input"
onChange={(e) => setName(e.currentTarget.value)}
placeholder="Enter a name..."
/>
<button type="submit">Greet</button>
</form>
<p>{greetMsg}</p>
</main>
);
// Utility for merging tailwind classes
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export default App;
// Components
import PlaySection from './components/PlaySection'
import ModsSection from './components/ModsSection'
import SettingsSection from './components/SettingsSection'
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)
useEffect(() => {
const fetchVersions = async () => {
const v = await launcherService.getVersions()
setVersions(v)
if (v.length > 0) setSelectedVersion(v[0])
}
fetchVersions()
}, [])
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 className="mb-4">
<div className="w-10 h-10 bg-saturn-accent rounded-xl flex items-center justify-center shadow-[0_0_20px_rgba(59,130,246,0.4)]">
<span className="font-bold text-white text-xl">S</span>
</div>
</div>
<nav className="flex flex-col gap-4">
<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">
{/* Version Selector */}
<div className="relative">
<button
onClick={() => setIsVersionDropdownOpen(!isVersionDropdownOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-md hover:bg-white/5 transition-colors text-sm font-medium border border-transparent hover:border-saturn-border"
>
<span className="text-saturn-text-secondary">Version:</span>
<span>{selectedVersion?.name || 'Loading...'}</span>
<ChevronDown size={14} className={cn("transition-transform", isVersionDropdownOpen && "rotate-180")} />
</button>
{isVersionDropdownOpen && (
<div className="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">
{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 Display */}
<div className="relative">
<button
onClick={() => setIsAccountDropdownOpen(!isAccountDropdownOpen)}
className="flex items-center gap-3 px-2 py-1 rounded-full hover:bg-white/5 transition-colors border border-transparent hover:border-saturn-border"
>
<div className="text-right hidden sm:block">
<p className="text-xs font-bold leading-none">SaturnDev</p>
<p className="text-[10px] text-saturn-text-secondary leading-none mt-1">Microsoft Account</p>
</div>
<div className="w-8 h-8 rounded-md bg-zinc-800 border border-saturn-border overflow-hidden flex items-center justify-center">
<User size={18} className="text-saturn-text-secondary" />
</div>
</button>
{isAccountDropdownOpen && (
<div className="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">
<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