38 lines
8.5 KiB
HTML
38 lines
8.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Gauntlet Overlay Editor</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}body{min-height:100vh;background:#110d09;color:#f8ead2;font-family:Segoe UI,Arial,sans-serif;padding:26px}.shell{max-width:1120px;margin:0 auto}.header{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;margin-bottom:18px}.eyebrow{text-transform:uppercase;letter-spacing:.18em;color:#e1b15f;font-size:12px;font-weight:800}.header h1{font-family:Georgia,'Times New Roman',serif;font-size:42px;line-height:1;margin-top:6px}.header p{color:#d9bd88;margin-top:8px;max-width:650px}.actions{display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end}.btn{border:1px solid rgba(255,226,170,.25);background:#2b1b12;color:#fff3d8;border-radius:10px;padding:11px 13px;font-weight:800;cursor:pointer;box-shadow:0 8px 20px rgba(0,0,0,.22)}.btn.primary{background:#e1b15f;color:#1b110b;border-color:#ffe0a0}.btn.danger{background:#452020}.btn:hover{filter:brightness(1.08)}.panel{background:linear-gradient(120deg,#3d2819,#1f1711);border:1px solid rgba(255,226,170,.22);border-radius:16px;padding:16px;box-shadow:0 20px 60px rgba(0,0,0,.42)}.grid{display:grid;gap:10px}.row{display:grid;grid-template-columns:42px 2fr 2fr 70px 126px 72px;gap:8px;align-items:center;padding:10px;border-radius:12px;background:rgba(0,0,0,.20);border:1px solid rgba(255,226,170,.12)}.row.active{border-color:#e1b15f;box-shadow:0 0 20px rgba(218,153,61,.15)}label{font-size:11px;color:#d9bd88;text-transform:uppercase;letter-spacing:.1em;display:block;margin-bottom:5px}input,select{width:100%;background:#17110d;color:#f8ead2;border:1px solid rgba(255,226,170,.22);border-radius:9px;padding:10px;font:inherit}input::placeholder{color:#806d50}.slot{width:34px;height:34px;border-radius:9px;background:#2f2117;display:grid;place-items:center;color:#e1b15f;font-weight:900}.small{font-size:13px;color:#d9bd88;margin:12px 2px}.status{margin-top:12px;color:#f0c36d;min-height:22px}.fallbacks{display:flex;gap:10px;flex-wrap:wrap;margin-top:14px}textarea{width:100%;height:220px;background:#0b0806;color:#f8ead2;border:1px solid rgba(255,226,170,.22);border-radius:12px;padding:12px;font-family:Consolas,monospace;margin-top:10px;display:none}.preview-link{color:#f0c36d;text-decoration:none;font-weight:800}.preview-link:hover{text-decoration:underline}@media(max-width:920px){.header{flex-direction:column}.actions{justify-content:flex-start}.row{grid-template-columns:38px 1fr}.field{grid-column:auto}.row .wide{grid-column:2}.row .btn{grid-column:2;width:max-content}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<div class="header"><div><div class="eyebrow">OBS Browser Source Tool</div><h1>The Gauntlet Editor</h1><p>Add up to 10 games. Blank title rows are hidden from the overlay. Set one game as Active so the overlay highlights your current run.</p></div><div class="actions"><button class="btn primary" onclick="saveDirect()">Save gauntlet.json directly</button><button class="btn" onclick="advanceRun()">Clear current + next</button><button class="btn danger" onclick="resetRun()">Reset to beginning</button></div></div>
|
|
<div class="panel"><div class="grid" id="rows"></div><div class="small">Overlay: <a class="preview-link" href="overlay.html" target="_blank">open overlay.html</a>. Batch controls use the same advance/reset actions as this editor.</div><div class="fallbacks"><button class="btn" onclick="copyJson()">Copy JSON</button><button class="btn" onclick="downloadJson()">Download gauntlet.json</button><button class="btn" onclick="toggleJson()">Show/hide JSON</button></div><textarea id="jsonBox" spellcheck="false"></textarea><div class="status" id="status"></div></div>
|
|
</div>
|
|
<script>
|
|
const rows = document.getElementById('rows'); const statusEl = document.getElementById('status'); const jsonBox = document.getElementById('jsonBox');
|
|
const MAX = 10; const statuses = ['active','queued','cleared','paused','failed'];
|
|
function setStatus(msg, bad=false){statusEl.textContent = msg; statusEl.style.color = bad ? '#ffb2a8' : '#f0c36d';}
|
|
function rowTemplate(i, game={}){const row=document.createElement('div'); row.className='row'; row.dataset.index=i; row.innerHTML=`<div class="slot">${i+1}</div><div class="field wide"><label>Game title</label><input data-field="title" placeholder="Game ${i+1}" value="${esc(game.title||'')}"></div><div class="field wide"><label>Subtitle / rules</label><input data-field="subtitle" placeholder="Any%, Nuzlocke, casual clear..." value="${esc(game.subtitle||'')}"></div><div class="field"><label>Icon</label><input data-field="icon" placeholder="◆" maxlength="8" value="${esc(game.icon||'')}"></div><div class="field"><label>Status</label><select data-field="status">${statuses.map(s=>`<option value="${s}" ${s===(game.status||'queued')?'selected':''}>${s}</option>`).join('')}</select></div><button class="btn" type="button" onclick="setActive(${i})">Active</button>`; rows.appendChild(row);}
|
|
function esc(s){return String(s).replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]));}
|
|
function buildRows(games=[]){rows.innerHTML=''; for(let i=0;i<MAX;i++) rowTemplate(i, games[i]||{}); refreshActiveStyle();}
|
|
function getData(){const games=[]; for(const row of rows.children){const title=row.querySelector('[data-field="title"]').value.trim(); if(!title) continue; const game={title, subtitle:row.querySelector('[data-field="subtitle"]').value.trim(), icon:row.querySelector('[data-field="icon"]').value.trim()||'◆', status:row.querySelector('[data-field="status"]').value}; games.push(game);} let activeSeen=false; for(const g of games){if(g.status==='active'){if(activeSeen)g.status='queued'; activeSeen=true;}} if(games.length && !activeSeen){const first=games.find(g=>g.status!=='cleared')||games[0]; first.status='active';} return {games};}
|
|
function applyData(data){buildRows((data&&Array.isArray(data.games))?data.games:[]);}
|
|
function refreshActiveStyle(){for(const row of rows.children){row.classList.toggle('active', row.querySelector('[data-field="status"]').value==='active'); row.querySelector('[data-field="status"]').onchange=refreshActiveStyle;}}
|
|
function setActive(index){for(const row of rows.children){row.querySelector('[data-field="status"]').value = Number(row.dataset.index)===index ? 'active' : (row.querySelector('[data-field="status"]').value==='active'?'queued':row.querySelector('[data-field="status"]').value);} refreshActiveStyle();}
|
|
async function load(){try{const res=await fetch('gauntlet.json?cache='+Date.now(),{cache:'no-store'}); applyData(await res.json()); setStatus('Loaded gauntlet.json');}catch(e){buildRows(); setStatus('Could not load gauntlet.json; starting blank.', true);}}
|
|
async function saveDirect(){try{const data=getData(); const res=await fetch('/api/gauntlet',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}); const out=await res.json(); if(!out.ok) throw new Error(out.error||'Save failed'); applyData(out); setStatus('Saved gauntlet.json directly. OBS will update within about 2 seconds.');}catch(e){setStatus('Direct save failed. Start with py server.py, not python -m http.server. ' + e.message, true);}}
|
|
async function advanceRun(){try{const res=await fetch('/api/advance',{method:'POST'}); const out=await res.json(); if(!out.ok) throw new Error(out.error||'Advance failed'); applyData(out); setStatus('Current game cleared; next game is active.');}catch(e){setStatus('Advance failed: '+e.message, true);}}
|
|
async function resetRun(){if(!confirm('Reset the run to the beginning? First game becomes active and all others become queued.'))return; try{const res=await fetch('/api/reset',{method:'POST'}); const out=await res.json(); if(!out.ok) throw new Error(out.error||'Reset failed'); applyData(out); setStatus('Run reset to the beginning.');}catch(e){setStatus('Reset failed: '+e.message, true);}}
|
|
function updateJsonBox(){jsonBox.value=JSON.stringify(getData(), null, 2);} function toggleJson(){updateJsonBox(); jsonBox.style.display=jsonBox.style.display==='block'?'none':'block';}
|
|
async function copyJson(){updateJsonBox(); await navigator.clipboard.writeText(jsonBox.value); setStatus('Copied JSON to clipboard.');}
|
|
function downloadJson(){updateJsonBox(); const blob=new Blob([jsonBox.value+'\n'],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='gauntlet.json'; a.click(); URL.revokeObjectURL(a.href); setStatus('Downloaded gauntlet.json.');}
|
|
load();
|
|
</script>
|
|
</body>
|
|
</html>
|