first commit
This commit is contained in:
@@ -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>
|
||||
@@ -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 s’est 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>
|
||||
)
|
||||
}
|
||||
@@ -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 s’affichera 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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
Reference in New Issue
Block a user