first commit

This commit is contained in:
lucasdpt
2026-06-14 12:32:29 +02:00
commit cc4f90e840
32 changed files with 9729 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'"
/>
<title>OFLauncher</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+236
View File
@@ -0,0 +1,236 @@
import { useEffect, useRef, useState } from 'react'
import type {
PlayerProfile,
ProgressEvent,
GameLogLine,
DeviceCodeInfo,
UpdateStatus
} from '../../shared/ipc'
type Status = 'loading' | 'logged-out' | 'logged-in' | 'working' | 'running'
export default function App(): JSX.Element {
const [status, setStatus] = useState<Status>('loading')
const [profile, setProfile] = useState<PlayerProfile | null>(null)
const [progress, setProgress] = useState<ProgressEvent | null>(null)
const [logs, setLogs] = useState<GameLogLine[]>([])
const [error, setError] = useState<string | null>(null)
const [authCode, setAuthCode] = useState<DeviceCodeInfo | null>(null)
const [maxMemoryMb, setMaxMemoryMb] = useState(8192)
const [appVersion, setAppVersion] = useState('')
const [update, setUpdate] = useState<UpdateStatus | null>(null)
const consoleRef = useRef<HTMLDivElement>(null)
// Restaure la session + réglages au démarrage.
useEffect(() => {
;(async () => {
const [p, s, v] = await Promise.all([
window.api.getProfile(),
window.api.getSettings(),
window.api.getAppVersion()
])
setMaxMemoryMb(s.maxMemoryMb)
setAppVersion(v)
setProfile(p)
setStatus(p ? 'logged-in' : 'logged-out')
})()
}, [])
// Abonnements aux événements main -> renderer.
useEffect(() => {
const offProgress = window.api.onProgress((e) => {
setProgress(e)
if (e.phase === 'running') setStatus('running')
if (e.phase === 'error') {
setError(e.message)
setStatus(profile ? 'logged-in' : 'logged-out')
}
})
const offLog = window.api.onGameLog((l) =>
setLogs((prev) => [...prev.slice(-800), l])
)
const offClosed = window.api.onGameClosed((code) => {
setStatus('logged-in')
setProgress(null)
setLogs((prev) => [
...prev,
{ stream: 'stdout', line: `\n— Jeu fermé (code ${code ?? '?'}) —` }
])
})
const offAuthCode = window.api.onAuthCode((info) => setAuthCode(info))
const offUpdate = window.api.onUpdateStatus((s) => setUpdate(s))
return () => {
offProgress()
offLog()
offClosed()
offAuthCode()
offUpdate()
}
}, [profile])
// Auto-scroll de la console.
useEffect(() => {
consoleRef.current?.scrollTo(0, consoleRef.current.scrollHeight)
}, [logs])
async function handleLogin(): Promise<void> {
setError(null)
setAuthCode(null)
try {
const p = await window.api.login()
setProfile(p)
setStatus('logged-in')
} catch (e) {
setError(`Échec de connexion : ${(e as Error).message}`)
} finally {
setAuthCode(null)
}
}
async function handleLogout(): Promise<void> {
await window.api.logout()
setProfile(null)
setStatus('logged-out')
}
function handleOpenInstance(): void {
void window.api.openInstanceDir()
}
async function handlePlay(): Promise<void> {
setError(null)
setLogs([])
setStatus('working')
await window.api.setSettings({ maxMemoryMb, extraJvmArgs: [] })
try {
await window.api.play()
} catch (e) {
setError(`Échec du lancement : ${(e as Error).message}`)
setStatus('logged-in')
}
}
if (status === 'loading') {
return (
<div className="app">
<div className="center muted">Chargement</div>
</div>
)
}
if (status === 'logged-out') {
return (
<div className="app">
<div className="center">
<h1>OFLauncher</h1>
<p className="muted">Connecte-toi avec ton compte Microsoft pour jouer.</p>
<button className="play" onClick={handleLogin} disabled={!!authCode}>
{authCode ? 'En attente…' : 'Se connecter'}
</button>
{authCode && (
<div className="authcode">
<p>
Va sur{' '}
<a href={authCode.verificationUri} target="_blank" rel="noreferrer">
{authCode.verificationUri}
</a>{' '}
et entre le code :
</p>
<div className="code">{authCode.userCode}</div>
<p className="muted" style={{ fontSize: 12 }}>
La page sest ouverte dans ton navigateur. Reviens ici une fois connecté.
</p>
</div>
)}
{error && <div className="error">{error}</div>}
<div className="muted" style={{ fontSize: 12 }}>
v{appVersion}
</div>
</div>
</div>
)
}
const busy = status === 'working' || status === 'running'
const pct = progress?.progress
const indeterminate = busy && (pct === undefined || pct === null)
const updateBanner =
update?.state === 'downloading' ? (
<div className="updatebar">
<span>
Téléchargement de la mise à jour
{update.version ? ` ${update.version}` : ''}{' '}
{update.progress != null ? `${Math.round(update.progress * 100)} %` : ''}
</span>
</div>
) : update?.state === 'downloaded' ? (
<div className="updatebar ready">
<span>Mise à jour {update.version ?? ''} prête.</span>
<button className="linkbtn" onClick={() => void window.api.installUpdate()}>
Redémarrer pour installer
</button>
</div>
) : null
return (
<div className="app">
<div className="topbar">
<div className="brand">
OFLauncher
<span className="pack">All The Mods 10 · 1.21.1</span>
</div>
<div className="profile">
<img src={`https://mc-heads.net/avatar/${profile?.uuid}`} alt="" />
<span>{profile?.name}</span>
<button className="linkbtn" onClick={handleLogout} disabled={busy}>
Déconnexion
</button>
</div>
</div>
{updateBanner}
<div className="main">
<div className="console" ref={consoleRef}>
{logs.map((l, i) => (
<div key={i} className={l.stream === 'stderr' ? 'stderr' : undefined}>
{l.line}
</div>
))}
</div>
<div className="footer">
<div className="progress">
<div className="label">{busy ? progress?.message ?? 'Préparation…' : ''}</div>
<div className={`bar${indeterminate ? ' indeterminate' : ''}`}>
<div style={{ width: indeterminate ? undefined : `${Math.round((pct ?? 0) * 100)}%` }} />
</div>
</div>
<div className="settings">
RAM
<input
type="number"
min={2048}
max={32768}
step={1024}
value={maxMemoryMb}
disabled={busy}
onChange={(e) => setMaxMemoryMb(Number(e.target.value))}
/>
Mo
<button className="linkbtn" onClick={handleOpenInstance}>
Ouvrir le dossier de l'instance
</button>
</div>
<button className="play" onClick={handlePlay} disabled={busy}>
{status === 'running' ? 'En jeu' : busy ? 'Patiente' : 'Jouer'}
</button>
</div>
{error && <div className="error">{error}</div>}
</div>
</div>
)
}
+257
View File
@@ -0,0 +1,257 @@
:root {
--bg: #0e1116;
--bg-soft: #161b22;
--border: #283040;
--text: #e6edf3;
--text-dim: #8b97a7;
--accent: #3fb950;
--accent-hover: #46c95a;
--danger: #f85149;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
height: 100vh;
overflow: hidden;
}
#root {
height: 100vh;
}
.app {
display: flex;
flex-direction: column;
height: 100vh;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg-soft);
}
.brand {
font-weight: 700;
letter-spacing: 0.5px;
}
.brand .pack {
color: var(--text-dim);
font-weight: 400;
margin-left: 8px;
font-size: 0.85em;
}
.profile {
display: flex;
align-items: center;
gap: 10px;
}
.updatebar {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 8px 20px;
font-size: 0.85em;
background: var(--bg-soft);
border-bottom: 1px solid var(--border);
color: var(--text-dim);
}
.updatebar.ready {
color: var(--text);
border-bottom-color: var(--accent);
}
.profile img {
width: 28px;
height: 28px;
border-radius: 4px;
image-rendering: pixelated;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
padding: 24px;
gap: 16px;
min-height: 0;
}
.console {
flex: 1;
background: #07090d;
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
font-family: 'Cascadia Code', 'Consolas', monospace;
font-size: 12px;
line-height: 1.45;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
.console .stderr {
color: #f0883e;
}
.console:empty::before {
content: 'La console du jeu saffichera ici…';
color: var(--text-dim);
}
.footer {
display: flex;
align-items: center;
gap: 16px;
}
.progress {
flex: 1;
}
.progress .label {
font-size: 13px;
color: var(--text-dim);
margin-bottom: 6px;
min-height: 18px;
}
.bar {
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.bar > div {
height: 100%;
background: var(--accent);
transition: width 0.2s ease;
}
.bar.indeterminate > div {
width: 35% !important;
animation: slide 1.1s ease-in-out infinite;
}
@keyframes slide {
0% {
margin-left: -35%;
}
100% {
margin-left: 100%;
}
}
button.play {
background: var(--accent);
color: #04250c;
border: none;
padding: 14px 34px;
font-size: 16px;
font-weight: 700;
border-radius: 8px;
cursor: pointer;
min-width: 160px;
}
button.play:hover:not(:disabled) {
background: var(--accent-hover);
}
button.play:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.center {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 18px;
}
.center h1 {
margin: 0;
}
.muted {
color: var(--text-dim);
}
.linkbtn {
background: none;
border: 1px solid var(--border);
color: var(--text-dim);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
}
.linkbtn:hover {
color: var(--text);
}
.settings {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-dim);
}
.settings input {
width: 80px;
background: var(--bg-soft);
border: 1px solid var(--border);
color: var(--text);
border-radius: 4px;
padding: 4px 6px;
}
.error {
color: var(--danger);
font-size: 13px;
}
.authcode {
text-align: center;
max-width: 420px;
}
.authcode a {
color: var(--accent);
}
.authcode .code {
font-family: 'Cascadia Code', 'Consolas', monospace;
font-size: 28px;
letter-spacing: 4px;
font-weight: 700;
background: var(--bg-soft);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 18px;
margin: 10px auto;
display: inline-block;
user-select: all;
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
)