Files
encounterflow/battleflow/static/js/admin.js
Peter van Arkel 644b207997 pre-push commit
2025-11-20 14:40:42 +01:00

267 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 `<span class="ico_wrap"><img class="icoimg" src="/icons/${encodeURIComponent(name)}.svg" alt="${name}"></span>`; }
function typeChip(t){ return `<span class="tagType ${t}">${t}</span>`; }
function row(a, idx){
const main = `<tr class="${idx===state.turn_idx? 'active':''}">
<td>${idx+1}</td>
<td>${a.name}</td>
<td>${a.init}</td>
<td>${a.hp}</td>
<td>${a.ac}${a.reveal_ac?'':' (hidden)'} </td>
<td>${typeChip(a.type)}</td>
<td>
<button class="btn" title="Concentration" onclick=toggle('${a.id}','conc')>✦</button>
<button class="btn" title="Dead" onclick=toggle('${a.id}','dead')>☠</button>
<button class="btn" title="Visible to players" onclick=toggle('${a.id}','visible')>👁</button>
<button class="btn" title="Reveal AC" onclick=toggle('${a.id}','reveal_ac')>🛡</button>
</td>
<td>
<button class="btn btn-danger" onclick=removeA('${a.id}')>🗑 Remove</button>
</td>
</tr>`;
const active = (a.effects||[]);
const KNOWN_CONDITIONS = ['poisoned','prone','grappled','restrained','stunned','blinded','deafened','charmed','frightened','paralyzed','petrified','unconscious'];
const cond = `<tr class="condrow"><td colspan="8"><div class="condbar">${
KNOWN_CONDITIONS.map(c=>`<button class="${active.includes(c)?'on':''}" onclick=toggleEffect('${a.id}','${c}')>${iconImg(c)} ${c}</button>`).join(' ')
} <button class="btn" style="margin-left:8px" onclick=clearEffects('${a.id}')>Clear conditions</button></div></td></tr>`;
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 = '<div class="muted">No presets yet.</div>'; return; }
presets.forEach(p=>{
const id = 'chk_'+p.id; const w=document.createElement('div');
w.innerHTML = `<label><input type="checkbox" id="${id}" data-id="${p.id}"/> ${p.name} <span class="muted">(${p.type}${p.ac? ' · AC '+p.ac:''}${p.hp? ' · HP '+p.hp:''})</span></label>`;
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 = `<strong>${g.name}</strong> <span class="muted">(${g.member_ids.length} members)</span> — ${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();
}