const $ = (s) => document.querySelector(s); let config = { supportedHint: '', maxUploadMb: 500, mailAvailable: false }; let me = null; function toast(msg) { const t = $('#toast'); t.textContent = msg; t.classList.add('show'); clearTimeout(toast._t); toast._t = setTimeout(() => t.classList.remove('show'), 2200); } async function api(path, opts = {}) { const res = await fetch(path, opts); if (res.status === 401) { location.href = '/login'; throw new Error('Not signed in'); } const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`); return data; } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } // --- session guard + user menu -------------------------------------------- async function boot() { me = await api('/api/me'); if (!me.authed) { location.href = '/login'; return; } try { config = await api('/config.json'); } catch {} $('#usermenu .who').innerHTML = `Signed in as ${escapeHtml(me.user.username)} ${me.user.role}`; if (me.user.role === 'admin') $('#manageBtn').style.display = ''; $('#dropSub').textContent = `or click to browse — ${config.supportedHint}`; hostWarning(); load(); } $('#logoutBtn').addEventListener('click', async () => { await fetch('/api/logout', { method: 'POST' }); location.href = '/login'; }); function hostWarning() { const h = location.hostname; if (h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0') { $('#hostWarn').innerHTML = 'Heads up: you\'re on localhost, so QR codes will point at this machine only. ' + 'Open the app via this computer\'s LAN address (or set PUBLIC_BASE_URL) so phones can reach it.'; } } // --- upload ---------------------------------------------------------------- const drop = $('#drop'); const fileInput = $('#file'); drop.addEventListener('click', () => fileInput.click()); ['dragenter', 'dragover'].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.add('drag'); })); ['dragleave', 'drop'].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.remove('drag'); })); drop.addEventListener('drop', (e) => { if (e.dataTransfer.files[0]) startUpload(e.dataTransfer.files[0]); }); fileInput.addEventListener('change', () => { if (fileInput.files[0]) startUpload(fileInput.files[0]); }); function startUpload(file) { const maxBytes = config.maxUploadMb * 1024 * 1024; if (file.size > maxBytes) { toast(`That file is over the ${config.maxUploadMb} MB limit.`); return; } const form = new FormData(); form.append('file', file); form.append('title', $('#title').value); const bar = $('#progress'); const fill = bar.querySelector('i'); bar.style.display = 'block'; fill.style.width = '0%'; $('#uploadNote').textContent = `Uploading ${file.name}…`; const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/sessions'); xhr.upload.onprogress = (e) => { if (e.lengthComputable) fill.style.width = `${Math.round((e.loaded / e.total) * 100)}%`; }; xhr.onload = async () => { bar.style.display = 'none'; if (xhr.status === 401) { location.href = '/login'; return; } let data = {}; try { data = JSON.parse(xhr.responseText); } catch {} if (xhr.status >= 400) { $('#uploadNote').textContent = data.error || 'Upload failed.'; return; } $('#uploadNote').textContent = 'Converting… this can take a moment for large files.'; $('#title').value = ''; await pollUntilReady(data.id); load(); }; xhr.onerror = () => { bar.style.display = 'none'; $('#uploadNote').textContent = 'Upload failed.'; }; xhr.send(form); } async function pollUntilReady(id) { for (let i = 0; i < 240; i++) { try { const m = await api(`/api/sessions/${id}`); if (m.status === 'ready') { $('#uploadNote').textContent = 'Ready.'; return; } if (m.status === 'error') { $('#uploadNote').textContent = `Conversion failed: ${m.error}`; return; } } catch {} load(); await new Promise((r) => setTimeout(r, 1500)); } } // --- session list ---------------------------------------------------------- $('#refresh').addEventListener('click', async () => { // Rotate every presentation's QR code so links from finished sessions stop working. try { await api('/api/sessions/rotate-codes', { method: 'POST' }); toast('New QR codes generated'); } catch {} load(); }); async function load() { let items; try { items = await api('/api/sessions'); } catch { return; } const list = $('#list'); if (!items.length) { list.innerHTML = '