function initAdmin(token){ const overlay = document.getElementById('authOverlay'); const authMsg = document.getElementById('authMsg'); const tokenInput = document.getElementById('tokenInput'); const setTokenBtn = document.getElementById('setTokenBtn'); const statusEl = document.getElementById('statuspill'); function showOverlay(msg){ if(authMsg && msg){ authMsg.textContent = msg; } if(overlay){ overlay.classList.add('show'); } if(setTokenBtn && tokenInput){ setTokenBtn.onclick = () => { const t = (tokenInput.value||'').trim(); if(!t) return; const url = new URL(location.href); url.searchParams.set('token', t); location.href = url.toString(); }; } } function hideOverlay(){ if(overlay){ overlay.classList.remove('show'); } } function setStatus(txt, cls){ if(!statusEl) return; statusEl.textContent = txt; statusEl.className = 'statuspill ' + (cls||''); } let state=null, since=0, presets=[], groups=[]; const rows = document.getElementById('rows'); const presetlist = document.getElementById('presetlist'); const pg_list = document.getElementById('pg_list'); const g_checks = document.getElementById('g_checks'); function openBoard(){ window.open('/board?token='+encodeURIComponent(token), '_blank'); } window.openBoard = openBoard; function iconImg(name){ return `${name}`; } function typeChip(t){ return `${t}`; } function row(a, idx){ const main = ` ${idx+1} ${a.name} ${a.init} ${a.hp} ${a.ac}${a.reveal_ac?'':' (hidden)'} ${typeChip(a.type)} `; const active = (a.effects||[]); const KNOWN_CONDITIONS = ['poisoned','prone','grappled','restrained','stunned','blinded','deafened','charmed','frightened','paralyzed','petrified','unconscious']; const cond = `
${ KNOWN_CONDITIONS.map(c=>``).join(' ') }
`; return main + cond; } function render(){ document.getElementById('round').textContent = state.round; rows.innerHTML = ''; (state.actors||[]).forEach((a, idx)=>{ rows.insertAdjacentHTML('beforeend', row(a, idx)); }); } function renderPresets(){ presetlist.innerHTML = ''; presets.forEach(p=>{ const div = document.createElement('div'); div.className='preset'; if(p.avatar){ const img=document.createElement('img'); img.className='pav'; img.alt=p.name; img.src=(p.avatar.startsWith('http')||p.avatar.startsWith('/'))?p.avatar:('/avatars/'+encodeURIComponent(p.avatar)); div.appendChild(img); } const span = document.createElement('span'); span.innerHTML = `${typeChip(p.type)} ${p.name}${p.ac? ' · AC '+p.ac: ''}${p.hp? ' · HP '+p.hp: ''}`; div.appendChild(span); const addb = document.createElement('button'); addb.className='btn'; addb.textContent='➕ Add'; addb.onclick=()=>presetApply(p.id); div.appendChild(addb); const delb = document.createElement('button'); delb.className='btn btn-danger'; delb.textContent='🗑'; delb.style.marginLeft='6px'; delb.onclick=()=>presetRemove(p.id); div.appendChild(delb); presetlist.appendChild(div); }); renderGroupChecks(); } function renderGroupChecks(){ g_checks.innerHTML = ''; if(!presets.length){ g_checks.innerHTML = '
No presets yet.
'; return; } presets.forEach(p=>{ const id = 'chk_'+p.id; const w=document.createElement('div'); w.innerHTML = ``; g_checks.appendChild(w); }); } function renderGroups(){ pg_list.innerHTML = ''; groups.forEach(g=>{ const div = document.createElement('div'); div.className='preset'; const names = g.member_ids.map(id=>{ const p=presets.find(x=>x.id===id); return p? p.name : 'unknown'; }).join(', '); const span = document.createElement('span'); span.innerHTML = `${g.name} (${g.member_ids.length} members) — ${names}`; div.appendChild(span); const addb = document.createElement('button'); addb.className='btn'; addb.textContent='➕ Add Group'; addb.onclick=()=>groupApply(g.id); div.appendChild(addb); const delb = document.createElement('button'); delb.className='btn btn-danger'; delb.textContent='🗑'; delb.style.marginLeft='6px'; delb.onclick=()=>groupRemove(g.id); div.appendChild(delb); pg_list.appendChild(div); }); } async function post(p, body){ const r = await fetch(p+`?token=${encodeURIComponent(token)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body: body?JSON.stringify(body):null }); if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid. Fix the token and reload.'); return false; } if(!r.ok){ const t = await r.text(); alert('Error '+r.status+' '+t); setStatus('Reconnecting…','warn'); } return r.ok; } async function get(p){ const r = await fetch(p+`?token=${encodeURIComponent(token)}`); if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid. Fix the token and reload.'); return null; } if(!r.ok){ const t = await r.text(); alert('Error '+r.status+' '+t); setStatus('Reconnecting…','warn'); return null; } return r.json(); } // Upload helpers async function uploadAvatar(fileInputId, targetInputId, previewId){ const fileEl = document.getElementById(fileInputId); if(fileEl && fileEl.files && fileEl.files.length){ await uploadAvatarFile(fileEl.files[0], targetInputId, previewId); fileEl.value = ''; } else { alert('Select or drop a file first.'); } } async function uploadAvatarFile(file, targetInputId, previewId){ const fd = new FormData(); fd.append('file', file); const r = await fetch('/api/upload_avatar?token='+encodeURIComponent(token), { method: 'POST', body: fd }); if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid.'); return; } if(!r.ok){ const t = await r.text(); alert('Upload error '+r.status+' '+t); setStatus('Reconnecting…','warn'); return; } const j = await r.json(); document.getElementById(targetInputId).value = j.filename; if(previewId){ setPreview(j.filename, previewId); } } function setPreview(val, previewId){ const img = document.getElementById(previewId); if(!img) return; if(!val){ img.src=''; img.style.opacity=.3; return; } const src = (val.startsWith('http')||val.startsWith('/')) ? val : ('/avatars/'+encodeURIComponent(val)); img.src = src; img.style.opacity=1; } function bindPreview(inputId, previewId){ const el = document.getElementById(inputId); el.addEventListener('input', ()=> setPreview(el.value.trim(), previewId)); setPreview(el.value.trim(), previewId); } function makeDropzone(zoneId, targetInputId, previewId){ const dz = document.getElementById(zoneId); ['dragenter','dragover'].forEach(ev=> dz.addEventListener(ev, e=>{ e.preventDefault(); dz.classList.add('drag'); })); ['dragleave','drop'].forEach(ev=> dz.addEventListener(ev, e=>{ e.preventDefault(); dz.classList.remove('drag'); })); dz.addEventListener('drop', async (e)=>{ const files = e.dataTransfer.files; if(!files || !files.length) return; await uploadAvatarFile(files[0], targetInputId, previewId); }); } // Mutations async function add(){ const name = document.getElementById('name').value.trim(); const init = parseFloat(document.getElementById('init').value||'0'); const hp = parseInt(document.getElementById('hp').value||'0'); const ac = parseInt(document.getElementById('ac').value||'0'); const type = document.getElementById('type').value; const note = document.getElementById('note').value.trim(); const avatar = document.getElementById('avatar').value.trim(); await post('/api/add', { name, init, hp, ac, type, note, avatar }); } async function clearAll(){ await post('/api/clear'); } async function toggleVisible(){ await post('/api/toggle_visible'); } async function next(){ await post('/api/next'); } async function prev(){ await post('/api/prev'); } async function toggle(id, field){ await post('/api/update', { id, [field]: 'toggle' }); } async function toggleEffect(id, effect){ await post('/api/toggle_effect', { id, effect }); } async function clearEffects(id){ await post('/api/clear_effects', { id }); } async function removeA(id){ await post('/api/remove', { id }); } async function setDeadMode(mode){ await post('/api/dead_mode', { mode }); updateDeadSeg(mode); } function updateDeadSeg(mode){ const seg = document.getElementById('deadseg'); if(!seg) return; seg.querySelectorAll('button[data-mode]').forEach(b=>{ b.classList.toggle('on', b.getAttribute('data-mode')===mode); }); } // Presets async function presetAdd(){ const name = document.getElementById('p_name').value.trim(); const type = document.getElementById('p_type').value; const hp = parseInt(document.getElementById('p_hp').value||'0'); const ac = parseInt(document.getElementById('p_ac').value||'0'); const note = document.getElementById('p_note').value.trim(); const avatar = document.getElementById('p_avatar').value.trim(); await post('/api/preset/add', { name, type, hp, ac, note, avatar }); await loadPresets(); await loadPresetGroups(); } async function presetRemove(id){ await post('/api/preset/remove', { id }); await loadPresets(); await loadPresetGroups(); } async function presetApply(id){ const init = prompt('Initiative roll for this actor?', '10'); const p = parseFloat(init||'0'); await post('/api/preset/apply', { id, init: p }); } // Groups async function groupAdd(){ const name = document.getElementById('g_name').value.trim(); const member_ids = Array.from(g_checks.querySelectorAll('input[type=checkbox]:checked')).map(x=>x.getAttribute('data-id')); if(!name){ alert('Name required'); return; } if(!member_ids.length){ alert('Select at least one preset'); return; } await post('/api/preset/group/add', { name, member_ids }); document.getElementById('g_name').value=''; g_checks.querySelectorAll('input[type=checkbox]').forEach(c=> c.checked=false); await loadPresetGroups(); } async function groupRemove(id){ await post('/api/preset/group/remove', { id }); await loadPresetGroups(); } async function groupApply(id){ const r = await fetch('/api/preset_groups?token='+encodeURIComponent(token)); if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid.'); return; } const all = await r.json(); groups = all.items||[]; const g = groups.find(x=>x.id===id); if(!g){ alert('Group not found'); return; } const mode = (prompt('Group add mode: single | list | per | step','single')||'single').trim().toLowerCase(); let payload = { id }; if(mode==='list'){ const raw = prompt('Comma-separated initiatives in group order','12,11,10'); if(raw && raw.trim().length){ payload.inits = raw.split(',').map(s=>parseFloat((s||'0').trim())); } } else if(mode==='per'){ const arr = []; g.member_ids.forEach((pid, i)=>{ const p = presets.find(x=>x.id===pid); const lbl = p? p.name : ('Member '+(i+1)); const v = prompt('Initiative for '+lbl,'10'); arr.push(parseFloat(v||'0')); }); payload.inits = arr; } else if(mode==='step'){ const base = parseFloat(prompt('Base initiative for first member','12')||'12'); const step = parseFloat(prompt('Step per next member (e.g. -1 for descending)','-1')||'-1'); payload.inits = g.member_ids.map((_,i)=> base + i*step); } else { const v = prompt('Single initiative value for all members','12'); payload.init = parseFloat(v||'12'); } await post('/api/preset/group/apply', payload); } async function loadPresets(){ const r = await get('/api/presets'); if(r) { presets = r.items||[]; renderPresets(); } } async function loadPresetGroups(){ const r = await get('/api/preset_groups'); if(r){ groups = r.items||[]; renderGroups(); } } async function poll(){ try{ const r = await fetch(`/api/state?since=${since}&token=${encodeURIComponent(token)}`); if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid. Fix the token and reload.'); setTimeout(poll, 1500); return; } if(r.ok){ hideOverlay(); setStatus('Connected','ok'); state = await r.json(); since = state.version; render(); updateDeadSeg(state.dead_mode||'normal'); } else { setStatus('Reconnecting…','warn'); } } catch(e){ setStatus('Reconnecting…','warn'); } setTimeout(poll, 300); } // init window.clearAll = clearAll; window.toggleVisible = toggleVisible; window.next = next; window.prev = prev; window.toggle = toggle; window.toggleEffect = toggleEffect; window.clearEffects = clearEffects; window.removeA = removeA; window.setDeadMode = setDeadMode; window.presetAdd = presetAdd; window.presetRemove = presetRemove; window.presetApply = presetApply; window.groupAdd = groupAdd; window.groupRemove = groupRemove; window.groupApply = groupApply; bindPreview('avatar','avatar_preview'); bindPreview('p_avatar','p_avatar_preview'); makeDropzone('avatar_drop','avatar','avatar_preview'); makeDropzone('p_avatar_drop','p_avatar','p_avatar_preview'); loadPresets(); loadPresetGroups(); setStatus('Connecting…','warn'); poll(); }