const socket = io(); // --- Fetch-hjälpare med felhantering --- async function apiFetch(url, options = {}) { const res = await fetch(url, options); if (res.redirected && res.url.includes('/login')) { window.location.href = '/login'; return null; } if (!res.ok) throw new Error(`HTTP ${res.status}`); return res; } // --- Visa nu --- function visaAdress() { let src = document.getElementById('url-input').value.trim(); if (!src) return; if (!/^https?:\/\//i.test(src)) src = 'https://' + src; const embed = youtubeEmbedUrl(src); if (embed) { socket.emit('admin_switch', { type: 'url', source: embed }); } else { socket.emit('admin_switch', { type: 'url', source: src }); } } function extractYouTubeId(url) { const patterns = [ /[?&]v=([a-zA-Z0-9_-]{11})/, /youtu\.be\/([a-zA-Z0-9_-]{11})/, /embed\/([a-zA-Z0-9_-]{11})/, ]; for (const p of patterns) { const m = url.match(p); if (m) return m[1]; } return null; } function youtubeEmbedUrl(url) { const id = extractYouTubeId(url); if (!id) return null; return `https://www.youtube.com/embed/${id}?autoplay=1`; } function extractYouTubePlaylistId(url) { const m = url.match(/[?&]list=([a-zA-Z0-9_-]+)/); return m ? m[1] : null; } function youtubePlaylistEmbedUrl(url) { const listId = extractYouTubePlaylistId(url); if (!listId) return null; return `https://www.youtube.com/embed?listType=playlist&list=${listId}&autoplay=1&loop=1`; } function normalizeMusicYouTubeUrl(url) { // Convert music.youtube.com or any YouTube playlist URL to an embeddable URL const listId = extractYouTubePlaylistId(url); if (!listId) return url; return `https://www.youtube.com/embed?listType=playlist&list=${listId}&autoplay=1&loop=1&enablejsapi=1`; } function visaLokalFil(path, type) { const src = '/files?path=' + encodeURIComponent(path); socket.emit('admin_switch', { type, source: src }); } async function setSchemaCalla(path, type) { const form = document.getElementById('job-form'); const url = '/files?path=' + encodeURIComponent(path); form.source.value = url; form.type.value = type; if (type === 'video') { const mins = await getVideoDuration(url); if (mins !== null) form.duration.value = mins; } } function getVideoDuration(url) { return new Promise((resolve) => { const v = document.createElement('video'); v.preload = 'metadata'; v.onloadedmetadata = () => { const mins = Math.ceil(v.duration / 60 * 100) / 100; v.src = ''; resolve(mins); }; v.onerror = () => resolve(null); v.src = url; }); } function laddaUpp() { const input = document.getElementById('upload-input'); const file = input.files[0]; if (!file) return; const fill = document.getElementById('progress-fill'); const status = document.getElementById('upload-status'); const bar = document.getElementById('upload-progress'); fill.style.width = '0%'; status.textContent = 'Laddar upp...'; bar.style.display = 'flex'; const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { fill.style.width = Math.round(e.loaded / e.total * 100) + '%'; } }; xhr.onload = () => { input.value = ''; if (xhr.status === 200) { const data = JSON.parse(xhr.responseText); fill.style.width = '100%'; status.textContent = '✓ ' + data.path.split('/').pop(); socket.emit('admin_switch', { type: extType(data.path), source: data.url }); setTimeout(() => { bar.style.display = 'none'; }, 3000); } else { status.textContent = 'Fel ' + xhr.status; } }; xhr.onerror = () => { status.textContent = 'Nätverksfel'; }; const fd = new FormData(); fd.append('file', file); xhr.open('POST', '/api/upload'); xhr.send(fd); } async function laddaOmDisplay() { const res = await apiFetch('/api/sysinfo'); if (!res) return; const d = await res.json(); if (d.active_type && d.active_type !== '—') { socket.emit('admin_switch', { type: d.active_type, source: d.active_source }); } } function tomSkarm() { socket.emit('admin_switch', { type: 'local', source: '', html: '' }); } // --- System --- async function startaOmProgram() { if (!confirm('Starta om kiosken?')) return; await apiFetch('/api/restart', { method: 'POST' }); } async function startaOmSystemet() { if (!confirm('Starta om hela datorn?')) return; await apiFetch('/api/reboot', { method: 'POST' }); } async function stangAvDatorn() { if (!confirm('Stäng av datorn?')) return; await apiFetch('/api/shutdown', { method: 'POST' }); } // --- Schema --- async function laddaJobb() { const res = await apiFetch('/api/jobs'); if (!res) return; const jobs = await res.json(); const tbody = document.getElementById('jobs-body'); if (jobs.length === 0) { tbody.innerHTML = 'Inga schemalagda jobb'; return; } tbody.innerHTML = jobs.map(j => ` ${escHtml(j.label)} ${fmtDuration(j.duration)} ${escHtml(j.type)} ${escHtml(j.source)} ${j.show_chat ? '✓' : '—'} `).join(''); // enable drag-and-drop ordering makeRowsDraggable(tbody); } function makeRowsDraggable(tbody) { let dragSrc = null; const rows = Array.from(tbody.querySelectorAll('tr[data-id]')); rows.forEach(row => { row.draggable = true; row.addEventListener('dragstart', (e) => { dragSrc = row; e.dataTransfer.effectAllowed = 'move'; }); row.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }); row.addEventListener('drop', (e) => { e.preventDefault(); if (!dragSrc || dragSrc === row) return; const nodes = Array.from(tbody.querySelectorAll('tr[data-id]')); const srcIdx = nodes.indexOf(dragSrc); const dstIdx = nodes.indexOf(row); if (srcIdx < dstIdx) tbody.insertBefore(dragSrc, row.nextSibling); else tbody.insertBefore(dragSrc, row); sendReorder(tbody); }); row.addEventListener('dragend', () => { dragSrc = null; }); }); } function sendReorder(tbody) { const newOrder = Array.from(tbody.querySelectorAll('tr[data-id]')).map(r => parseInt(r.dataset.id, 10)); fetch('/api/jobs/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order: newOrder }) }).then(r => { if (r.ok) laddaJobb(); }); } function _jobTypeChanged(formId, wrapId, statusId, emuWrapId) { const form = document.getElementById(formId); const wrap = document.getElementById(wrapId); const emuWrap = emuWrapId ? document.getElementById(emuWrapId) : null; if (!form) return; const type = form.type.value; const isPlaylist = type === 'youtube_playlist'; const isEmulator = type === 'emulator'; // Show/hide shuffle row for playlist type const shuffleWrapId = formId === 'job-form' ? 'job-shuffle-wrap' : 'edit-shuffle-wrap'; const shuffleWrap = document.getElementById(shuffleWrapId); if (shuffleWrap) shuffleWrap.style.display = isPlaylist ? 'block' : 'none'; if (wrap) wrap.style.display = isPlaylist ? 'flex' : 'none'; if (emuWrap) emuWrap.style.display = isEmulator ? 'flex' : 'none'; if (!isPlaylist && statusId) { const s = document.getElementById(statusId); if (s) s.textContent = ''; } } // Öppnar ROM-bläddraren och fyller source-fältet i det angivna formuläret function openEmuBrowser(formId) { const form = document.getElementById(formId); const nameEl = document.getElementById( formId === 'job-form' ? 'job-emu-name' : 'edit-emu-name' ); openBrowser(function (path, _type) { form.source.value = path; if (nameEl) nameEl.textContent = path.split('/').pop(); }); } async function extractJobPlaylist(formId) { const form = document.getElementById(formId); const isAdd = formId === 'job-form'; const statusId = isAdd ? 'job-extract-status' : 'edit-extract-status'; const status = document.getElementById(statusId); const url = form.source.value.trim(); if (!url) { status.textContent = 'Ange URL först.'; return; } status.style.color = '#444'; status.textContent = 'Hämtar spellista…'; try { const res = await apiFetch('/api/playlist/extract', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); const d = await res.json(); if (!res.ok) { status.textContent = '⚠ ' + (d.error || 'Okänt fel'); } else { const totalMin = d.total_seconds ? (d.total_seconds / 60) : null; // Auto-fyll duration om fältet är tomt if (totalMin && !form.duration.value) { form.duration.value = Math.round(totalMin * 100) / 100; } const timeStr = totalMin ? _fmtDuration(d.total_seconds) : ''; status.style.color = '#166534'; status.textContent = `✓ ${d.count} videos hämtade${timeStr ? ' · total speltid ' + timeStr : ''}.`; } } catch (err) { status.textContent = '⚠ ' + err.message; } } function _fmtDuration(secs) { const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); const s = secs % 60; if (h > 0) return `${h}h ${m}m`; if (m > 0) return `${m}m ${s}s`; return `${s}s`; } async function laggTillJobb(e) { e.preventDefault(); const form = e.target; let type = form.type.value; let source = form.source.value; if (type === 'youtube') { const embed = youtubeEmbedUrl(source); if (!embed) { alert('Ogiltig YouTube-URL'); return; } type = 'url'; source = embed; } else if (type === 'youtube_playlist') { if (!extractYouTubePlaylistId(source)) { alert('Ogiltig YouTube-spellisteURL (måste innehålla ?list=...)'); return; } // Behåll type=youtube_playlist och rå URL — display cyclar via IFrame API } else if (type === 'kiosk_chat') { source = '/chat-display'; } const txDur = form.transition_duration ? parseInt(form.transition_duration.value, 10) : 0; const data = { label: form.label.value, duration: form.duration.value, type, source, show_chat: form.show_chat ? form.show_chat.checked : false, shuffle: form.shuffle ? form.shuffle.checked : false, transition_type: form.transition_type ? form.transition_type.value : '', transition_duration: isNaN(txDur) ? 0 : txDur, }; const res = await apiFetch('/api/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res) return; form.reset(); laddaJobb(); } // --- Edit job --- function openEdit(id) { const row = document.getElementById('job-' + id); if (!row) return; // fetch job data from server (or parse from row) fetch('/api/jobs').then(r => r.json()).then(jobs => { const job = jobs.find(x => x.id === id); if (!job) return; const modal = document.getElementById('edit-modal'); const form = document.getElementById('edit-form'); form.elements.namedItem('id').value = job.id; form.label.value = job.label; form.duration.value = job.duration; form.type.value = job.type; form.source.value = job.source; if (form.shuffle) form.shuffle.checked = !!job.shuffle; if (form.transition_type) form.transition_type.value = job.transition_type || ''; if (form.transition_duration) form.transition_duration.value = job.transition_duration || ''; _jobTypeChanged('edit-form', 'edit-extract-wrap', 'edit-extract-status'); modal.style.display = 'flex'; }); } function closeEditModal() { document.getElementById('edit-modal').style.display = 'none'; } function editModalClick(e) { if (e.target === document.getElementById('edit-modal')) closeEditModal(); } async function saveEdit(e) { e.preventDefault(); const form = e.target; const id = parseInt(form.id.value, 10); const txDur = form.transition_duration ? parseInt(form.transition_duration.value, 10) : 0; const payload = { label: form.label.value, duration: parseFloat(form.duration.value), type: form.type.value, source: form.source.value, shuffle: form.shuffle ? form.shuffle.checked : false, transition_type: form.transition_type ? form.transition_type.value : '', transition_duration: isNaN(txDur) ? 0 : txDur, }; const res = await apiFetch('/api/jobs/' + id, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res) return; closeEditModal(); laddaJobb(); } // --- Reorder --- function moveJob(id, dir) { const tbody = document.getElementById('jobs-body'); const rows = Array.from(tbody.querySelectorAll('tr[data-id]')); const idx = rows.findIndex(r => String(r.dataset.id) === String(id)); if (idx === -1) return; let newIdx = dir === 'up' ? idx - 1 : idx + 1; if (newIdx < 0 || newIdx >= rows.length) return; // swap nodes const row = rows[idx]; const ref = rows[newIdx]; if (dir === 'up') tbody.insertBefore(row, ref); else tbody.insertBefore(ref, row); // send new order to server const newOrder = Array.from(tbody.querySelectorAll('tr[data-id]')).map(r => parseInt(r.dataset.id, 10)); fetch('/api/jobs/reorder', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order: newOrder }) }).then(r => { if (r.ok) laddaJobb(); }); } async function taBortJobb(id) { const res = await apiFetch('/api/jobs/' + id, { method: 'DELETE' }); if (!res) return; laddaJobb(); } async function toggleJobb(id) { const res = await apiFetch('/api/jobs/' + id, { method: 'PATCH' }); if (!res) return; laddaJobb(); } // --- Inställningar --- async function laddaVolym() { const res = await apiFetch('/api/volume'); if (!res) return; const d = await res.json(); const slider = document.getElementById('setting-volume'); slider.value = d.volume; document.getElementById('volume-val').textContent = d.volume + '%'; } async function setVolym() { const vol = parseInt(document.getElementById('setting-volume').value, 10); await apiFetch('/api/volume', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ volume: vol }), }); } async function laddaInstallningar() { const [setRes, audioRes, dispRes] = await Promise.all([ apiFetch('/api/settings'), apiFetch('/api/audio/profiles'), apiFetch('/api/displays'), ]); if (!setRes || !audioRes || !dispRes) return; const settings = await setRes.json(); const profiles = await audioRes.json(); const displays = await dispRes.json(); const audioSel = document.getElementById('setting-audio'); audioSel.innerHTML = profiles.map(p => { const label = escHtml(p.description || p.name) + (p.available === false ? ' (frånkopplad)' : ''); return ``; }).join(''); document.getElementById('setting-internal-display').checked = settings.internal_display !== 'off'; // populate display target select with available outputs + special choices const dispSel = document.getElementById('setting-display'); const outOptions = [{val:'last',label:'Sista anslutna (standard)'},{val:'first',label:'Första anslutna'}]; displays.forEach(d => outOptions.push({val:d.name,label:d.name + (d.connected ? ' (ansluten)' : ' (ej ansluten)')})); dispSel.innerHTML = outOptions.map(o => ``).join(''); // build a mapping name -> modes/current const modesByOutput = {}; displays.forEach(d => { modesByOutput[d.name] = {modes: Array.isArray(d.modes)?d.modes:[], current: d.current_mode || ''}; }); const modeSel = document.getElementById('setting-display-mode'); const customInput = document.getElementById('setting-display-mode-custom'); function formatLabel(token) { // token like '1920x1080@60.000000' -> '1920x1080 @ 60 Hz' if (!token) return ''; if (token === '__custom__') return 'Ange manuellt...'; const parts = token.split('@'); const hz = parts[1] ? String(Math.round(parseFloat(parts[1])*1000)/1000).replace(/\.0+$/,'') : ''; return parts[0] + (hz ? (' @ ' + hz + ' Hz') : ''); } function populateModesForOutput(outName) { const info = modesByOutput[outName] || {modes:[], current:''}; const modes = info.modes.slice(); const currentMode = settings.display_mode || info.current || ''; if (currentMode && !modes.includes(currentMode)) modes.unshift(currentMode); // sort by resolution descending (compare token prefix) modes.sort((a,b)=>{const A=a.split('@')[0].split('x').map(Number),B=b.split('@')[0].split('x').map(Number);return B[0]-A[0]||B[1]-A[1];}); modeSel.innerHTML = '' + modes.map(m => ``).join('') + ``; // set selected if (currentMode) { if (modes.includes(currentMode)) { modeSel.value = currentMode; customInput.style.display='none'; } else { modeSel.value='__custom__'; customInput.style.display=''; customInput.value = currentMode; } } else { modeSel.value = ''; customInput.style.display='none'; } } // initial populate for selected display const initialOut = dispSel.value && dispSel.value !== 'last' && dispSel.value !== 'first' ? dispSel.value : (displays.find(d=>d.connected)?.name || ''); if (initialOut) populateModesForOutput(initialOut); dispSel.addEventListener('change', () => { const v = dispSel.value; if (v === 'last' || v === 'first') { // choose connected first/last const candidate = v === 'last' ? (displays.slice().reverse().find(d=>d.connected) || displays.find(d=>d.connected)) : (displays.find(d=>d.connected) || null); populateModesForOutput(candidate ? candidate.name : ''); } else { populateModesForOutput(v); } }); modeSel.addEventListener('change', () => { if (modeSel.value === '__custom__') { customInput.style.display=''; customInput.focus(); } else { customInput.style.display='none'; } }); const connected = displays.filter(d => d.connected).map(d => d.name).join(', '); const disconnected = displays.filter(d => !d.connected).map(d => d.name).join(', '); const parts = []; if (connected) parts.push('Anslutna: ' + connected); if (disconnected) parts.push('Ej anslutna: ' + disconnected); document.getElementById('displays-info').textContent = parts.join(' | '); } async function setInternDisplay(enabled) { const status = document.getElementById('settings-status'); status.textContent = enabled ? 'Aktiverar intern display...' : 'Inaktiverar intern display...'; const res = await apiFetch('/api/internal_display', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }), }); if (!res) return; status.textContent = enabled ? '✓ Intern display aktiverad' : '✓ Intern display inaktiverad'; setTimeout(() => { status.textContent = ''; }, 3000); } async function sparaInstallningar() { const profileIndex = parseInt(document.getElementById('setting-audio').value, 10); const display = document.getElementById('setting-display').value; const modeSelEl = document.getElementById('setting-display-mode'); let displayMode = ''; if (modeSelEl.value === '__custom__') { displayMode = document.getElementById('setting-display-mode-custom').value.trim(); } else { displayMode = modeSelEl.value.trim(); } const status = document.getElementById('settings-status'); status.textContent = 'Tillämpar...'; const res = await apiFetch('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ audio_profile_index: profileIndex, display_output: display, display_mode: displayMode, internal_display: document.getElementById('setting-internal-display').checked }), }); if (!res) return; status.textContent = '✓ Tillämpat'; setTimeout(() => { status.textContent = ''; }, 3000); } async function bytaLösenord() { const current = document.getElementById('cred-current').value; const user = document.getElementById('cred-user').value.trim(); const pass = document.getElementById('cred-pass').value; const status = document.getElementById('cred-status'); if (!current || !user || !pass) { status.textContent = 'Fyll i alla fält'; return; } const res = await apiFetch('/api/settings/credentials', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_password: current, new_username: user, new_password: pass }), }).catch(() => null); if (!res) { status.textContent = '✗ Fel'; return; } const d = await res.json(); if (d.ok) { status.textContent = '✓ Sparat — logga in igen'; document.getElementById('cred-current').value = ''; document.getElementById('cred-pass').value = ''; } else { status.textContent = '✗ ' + (d.error || 'Fel'); } } async function applyDisplayNow() { const modeSelEl = document.getElementById('setting-display-mode'); let displayMode = ''; if (modeSelEl.value === '__custom__') { displayMode = document.getElementById('setting-display-mode-custom').value.trim(); } else { displayMode = modeSelEl.value.trim(); } if (!displayMode) { alert('Ange en upplösning först.'); return; } const res = await apiFetch('/api/settings/apply_display', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ display_mode: displayMode, display_output: document.getElementById('setting-display').value }) }).catch(() => null); if (!res) { alert('Misslyckades att kontakta servern'); return; } const j = await res.json(); if (j.ok) alert('Upplösning tillämpad (försök att ändra skärm).'); else alert('Kunde inte tillämpa upplösning: ' + (j.error || 'okänt fel')); } // --- Kiosk Livechat overlay --- async function laddaChat() { const res = await apiFetch('/api/chat'); if (!res) return; const d = await res.json(); const kw = d.kiosk_width_pct != null ? d.kiosk_width_pct : 30; const kh = d.kiosk_height != null ? d.kiosk_height : 80; if (document.getElementById('kiosk-chat-enabled')) document.getElementById('kiosk-chat-enabled').checked = !!d.kiosk_enabled; if (document.getElementById('kiosk-chat-position')) document.getElementById('kiosk-chat-position').value = d.kiosk_position || 'right'; if (document.getElementById('kiosk-chat-width')) { document.getElementById('kiosk-chat-width').value = kw; document.getElementById('kiosk-chat-width-val').textContent = kw + '%'; } if (document.getElementById('kiosk-chat-height')) { document.getElementById('kiosk-chat-height').value = kh; document.getElementById('kiosk-chat-height-val').textContent = kh + '%'; } if (document.getElementById('kiosk-chat-status')) document.getElementById('kiosk-chat-status').textContent = d.kiosk_enabled ? '● Aktiv overlay' : ''; } async function _sendKioskChat(kioskEnabled) { const res = await apiFetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kiosk_enabled: kioskEnabled, kiosk_position: document.getElementById('kiosk-chat-position').value, kiosk_width_pct: parseInt(document.getElementById('kiosk-chat-width').value, 10), kiosk_height: parseInt(document.getElementById('kiosk-chat-height').value, 10), }), }); if (!res) return; if (document.getElementById('kiosk-chat-status')) document.getElementById('kiosk-chat-status').textContent = kioskEnabled ? '● Aktiv overlay' : ''; if (document.getElementById('kiosk-chat-enabled')) document.getElementById('kiosk-chat-enabled').checked = kioskEnabled; } // --- YouTube Live Chat overlay --- async function laddaYtChat() { const res = await apiFetch('/api/yt_chat'); if (!res) return; const d = await res.json(); if (document.getElementById('yt-chat-position')) document.getElementById('yt-chat-position').value = d.position || 'right'; if (document.getElementById('yt-chat-bg-color')) document.getElementById('yt-chat-bg-color').value = d.bg_color || '#000000'; const wp = d.width_pct != null ? d.width_pct : 25; const h = d.height != null ? d.height : 100; const bo = d.bg_opacity != null ? d.bg_opacity : 70; if (document.getElementById('yt-chat-width')) { document.getElementById('yt-chat-width').value = wp; document.getElementById('yt-chat-width-val').textContent = wp + '%'; } if (document.getElementById('yt-chat-height')) { document.getElementById('yt-chat-height').value = h; document.getElementById('yt-chat-height-val').textContent = h + '%'; } if (document.getElementById('yt-chat-bg-opacity')) { document.getElementById('yt-chat-bg-opacity').value = bo; document.getElementById('yt-chat-bg-opacity-val').textContent = bo + '%'; } if (document.getElementById('yt-chat-status')) document.getElementById('yt-chat-status').textContent = d.enabled ? '● Aktiv' : ''; // Chat theme button try { const KEY = 'kiosk_chat_theme'; const btn = document.getElementById('yt-chat-theme-btn'); if (btn) { const getTheme = () => localStorage.getItem(KEY) || 'auto'; const order = ['auto','dark','light']; const updateBtn = () => { const t = getTheme(); btn.textContent = t === 'auto' ? 'Chatt: Auto' : (t === 'dark' ? 'Chatt: Mörk' : 'Chatt: Ljus'); }; btn.onclick = () => { const next = order[(order.indexOf(getTheme()) + 1) % order.length]; localStorage.setItem(KEY, next); updateBtn(); window.dispatchEvent(new Event('kiosk-chat-theme-changed')); }; updateBtn(); window.addEventListener('storage', (e) => { if (e.key === KEY) updateBtn(); }); } } catch (e) {} } async function _sendYtChat(enabled) { const res = await apiFetch('/api/yt_chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, position: document.getElementById('yt-chat-position') ? document.getElementById('yt-chat-position').value : 'right', width_pct: document.getElementById('yt-chat-width') ? parseInt(document.getElementById('yt-chat-width').value, 10) : 25, height: document.getElementById('yt-chat-height') ? parseInt(document.getElementById('yt-chat-height').value, 10) : 100, bg_color: document.getElementById('yt-chat-bg-color') ? document.getElementById('yt-chat-bg-color').value : '#000000', bg_opacity: document.getElementById('yt-chat-bg-opacity') ? parseInt(document.getElementById('yt-chat-bg-opacity').value, 10) : 70, }), }); if (!res) return; if (document.getElementById('yt-chat-status')) document.getElementById('yt-chat-status').textContent = enabled ? '● Aktiv' : ''; } function aktiveraChatOverlay(which) { if (which === 'yt') _sendYtChat(true); if (which === 'kiosk') _sendKioskChat(true); } function avaktiveraChatOverlay(which) { if (which === 'yt') _sendYtChat(false); if (which === 'kiosk') _sendKioskChat(false); } // --- Ticker --- function tickerModeChanged() { const continuous = document.getElementById('ticker-mode').value === 'continuous'; document.getElementById('ticker-sep-label').style.display = continuous ? '' : 'none'; document.getElementById('ticker-sep-wrap').style.display = continuous ? '' : 'none'; } function tickerSourceChanged() { const rss = document.getElementById('ticker-source').value === 'rss'; document.getElementById('ticker-text-label').style.display = rss ? 'none' : ''; document.getElementById('ticker-text-wrap').style.display = rss ? 'none' : ''; document.getElementById('ticker-rss-label').style.display = rss ? '' : 'none'; document.getElementById('ticker-rss-wrap').style.display = rss ? '' : 'none'; document.getElementById('ticker-rss-interval-label').style.display = rss ? '' : 'none'; document.getElementById('ticker-rss-interval').style.display = rss ? '' : 'none'; } async function laddaTicker() { const res = await apiFetch('/api/ticker'); if (!res) return; const d = await res.json(); document.getElementById('ticker-source').value = d.source || 'text'; document.getElementById('ticker-text').value = d.text || ''; document.getElementById('ticker-rss-url').value = d.rss_url || ''; document.getElementById('ticker-rss-interval').value = String(d.rss_interval || 10); document.getElementById('ticker-mode').value = d.mode || 'sequential'; document.getElementById('ticker-separator').value = d.separator || '◆'; document.getElementById('ticker-position').value = d.position || 'bottom'; document.getElementById('ticker-text-color').value = d.text_color || '#ffffff'; document.getElementById('ticker-font-family').value = d.font_family || 'sans-serif'; document.getElementById('ticker-bar-color').value = d.bar_color || '#000000'; tickerSourceChanged(); tickerModeChanged(); const pps = d.pps != null ? d.pps : 150; const size = d.font_size != null ? d.font_size : 2; const opacity = d.bar_opacity != null ? d.bar_opacity : 72; const padding = d.bar_padding != null ? d.bar_padding : 0.45; document.getElementById('ticker-pps').value = pps; document.getElementById('ticker-pps-val').textContent = pps + ' px/s'; document.getElementById('ticker-font-size').value = size; document.getElementById('ticker-font-size-val').textContent = parseFloat(size).toFixed(1) + 'rem'; document.getElementById('ticker-bar-opacity').value = opacity; document.getElementById('ticker-bar-opacity-val').textContent = opacity + '%'; document.getElementById('ticker-bar-padding').value = padding; document.getElementById('ticker-bar-padding-val').textContent = parseFloat(padding).toFixed(2) + 'rem'; document.getElementById('ticker-status').textContent = d.enabled ? '● Aktiv' : ''; } async function _sendTicker(enabled) { const res = await apiFetch('/api/ticker', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, source: document.getElementById('ticker-source').value, text: document.getElementById('ticker-text').value, rss_url: document.getElementById('ticker-rss-url').value, rss_interval: parseInt(document.getElementById('ticker-rss-interval').value, 10), mode: document.getElementById('ticker-mode').value, separator: document.getElementById('ticker-separator').value, position: document.getElementById('ticker-position').value, pps: parseInt(document.getElementById('ticker-pps').value, 10), text_color: document.getElementById('ticker-text-color').value, font_size: parseFloat(document.getElementById('ticker-font-size').value), font_family: document.getElementById('ticker-font-family').value, bar_color: document.getElementById('ticker-bar-color').value, bar_opacity: parseInt(document.getElementById('ticker-bar-opacity').value, 10), bar_padding: parseFloat(document.getElementById('ticker-bar-padding').value), }), }); if (!res) return; document.getElementById('ticker-status').textContent = enabled ? '● Aktiv' : ''; } function aktiveraTicker() { _sendTicker(true); } function avaktriveraTicker() { _sendTicker(false); } // BBCode insert helpers function insertBB(tag, openVal) { const input = document.getElementById('ticker-text'); const start = input.selectionStart; const end = input.selectionEnd; const sel = input.value.slice(start, end); const open = openVal ? `[${tag}=${openVal}]` : `[${tag}]`; const close = `[/${tag}]`; input.value = input.value.slice(0, start) + open + sel + close + input.value.slice(end); input.focus(); const cur = start + open.length; input.setSelectionRange(cur, cur + sel.length); } function insertBBColor() { const col = document.getElementById('bb-color-val').value; insertBB('color', col); } function insertBBSize() { const px = parseInt(document.getElementById('bb-size-val').value, 10) || 48; insertBB('size', px); } function insertBBGlow() { const col = document.getElementById('bb-color-val').value; insertBB('glow', col); } // --- Wi-Fi --- let _wifiConnectSsid = ''; let _wifiConnectOpen = false; function _signalColor(pct) { if (pct >= 70) return '#22c55e'; if (pct >= 40) return '#f59e0b'; return '#ef4444'; } function _signalBarsHtml(pct) { const c = _signalColor(pct); const lit = pct >= 70 ? 4 : pct >= 50 ? 3 : pct >= 30 ? 2 : 1; const bars = [4, 7, 11, 14].map((h, i) => `` ).join(''); return `
${bars}
`; } async function laddaWifiStatus() { try { const res = await apiFetch('/api/wifi/status'); if (!res) return; const d = await res.json(); const dot = document.getElementById('wifi-status-dot'); const txt = document.getElementById('wifi-status-text'); const ip = document.getElementById('wifi-ip'); const sig = document.getElementById('wifi-signal-bar'); const sigv = document.getElementById('wifi-signal-val'); if (d.connected) { dot.style.background = '#22c55e'; txt.textContent = d.ssid || 'Ansluten'; } else { dot.style.background = '#ef4444'; txt.textContent = 'Ej ansluten'; } ip.textContent = d.ip || '—'; const pct = d.connected ? Math.max(0, Math.min(100, 2 * (d.signal + 100))) : 0; sig.innerHTML = _signalBarsHtml(pct); sigv.textContent = d.connected ? pct + '%' : '—'; if (d.sudo_ok === false) { document.getElementById('wifi-scan-status').textContent = '⚠ sudo saknas — se installation nedan'; } } catch {} } async function laddaWifiSaved() { try { const res = await apiFetch('/api/wifi/saved'); if (!res) return; const d = await res.json(); const list = document.getElementById('wifi-saved-list'); if (!d.saved || !d.saved.length) { list.innerHTML = 'Inga sparade nätverk'; return; } list.innerHTML = d.saved.map(n => `
${escHtml(n.ssid)}${n.current ? ' ● aktiv' : ''}
`).join(''); } catch {} } async function wifiScan() { const btn = document.getElementById('wifi-scan-btn'); const status = document.getElementById('wifi-scan-status'); const wrap = document.getElementById('wifi-networks'); const list = document.getElementById('wifi-network-list'); btn.disabled = true; status.textContent = 'Skannar...'; list.innerHTML = 'Söker...'; wrap.style.display = 'block'; try { const res = await apiFetch('/api/wifi/networks'); if (!res) { btn.disabled = false; status.textContent = ''; return; } const d = await res.json(); if (!d.networks || !d.networks.length) { list.innerHTML = 'Inga nätverk hittades'; } else { list.innerHTML = d.networks.map(n => { const lock = n.security !== 'open' ? `${escHtml(n.security)}` : ''; return `
${_signalBarsHtml(n.signal)} ${escHtml(n.ssid)}${n.known ? ' ' : ''} ${lock}
`; }).join(''); } status.textContent = d.networks.length + ' nätverk'; } catch (e) { status.textContent = 'Fel: ' + e.message; list.innerHTML = ''; } btn.disabled = false; } function wifiOpenModal(ssid, isOpen) { _wifiConnectSsid = ssid; _wifiConnectOpen = isOpen; document.getElementById('wifi-pw-title').textContent = 'Anslut till ' + ssid; document.getElementById('wifi-pw-input').value = ''; document.getElementById('wifi-pw-input').type = isOpen ? 'text' : 'password'; document.getElementById('wifi-pw-input').placeholder = isOpen ? 'Öppet nätverk — inget lösenord behövs' : 'Wi-Fi lösenord'; document.getElementById('wifi-connect-status').textContent = ''; document.getElementById('wifi-connect-btn').disabled = false; document.getElementById('wifi-pw-modal').style.display = 'flex'; if (!isOpen) document.getElementById('wifi-pw-input').focus(); } function closeWifiModal() { document.getElementById('wifi-pw-modal').style.display = 'none'; } function wifiModalClick(e) { if (e.target === document.getElementById('wifi-pw-modal')) closeWifiModal(); } async function wifiDoConnect() { const btn = document.getElementById('wifi-connect-btn'); const status = document.getElementById('wifi-connect-status'); const pw = document.getElementById('wifi-pw-input').value; btn.disabled = true; status.textContent = 'Ansluter... (upp till 20 s)'; try { const res = await apiFetch('/api/wifi/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ssid: _wifiConnectSsid, password: pw }), }); if (!res) { btn.disabled = false; return; } const d = await res.json(); if (d.ok) { status.textContent = '✓ Ansluten — IP: ' + (d.ip || '—'); setTimeout(() => { closeWifiModal(); laddaWifiStatus(); laddaWifiSaved(); }, 1500); } else { status.textContent = '✗ ' + (d.message || 'Misslyckades'); btn.disabled = false; } } catch (e) { status.textContent = 'Fel: ' + e.message; btn.disabled = false; } } async function wifiDisconnect() { const status = document.getElementById('wifi-scan-status'); status.textContent = 'Kopplar från...'; try { await apiFetch('/api/wifi/disconnect', { method: 'POST' }); status.textContent = ''; setTimeout(laddaWifiStatus, 1000); } catch {} } async function wifiRemove(id, ssid) { if (!confirm(`Ta bort sparat nätverk "${ssid}"?`)) return; try { await apiFetch('/api/wifi/saved/' + encodeURIComponent(id), { method: 'DELETE' }); laddaWifiSaved(); } catch {} } // --- Bluetooth --- let _btPowered = true; let _btScanning = false; let _btScanTimer = null; function _btDeviceIcon(type) { if (type === 'audio') return '🔊'; if (type === 'phone') return '📱'; if (type === 'input') return '⌨'; return '📶'; } function _btDeviceRow(dev, section) { const icon = _btDeviceIcon(dev.type); const label = escHtml(dev.name || dev.address); const addrSpan = dev.name ? `${escHtml(dev.address)}` : ''; const nameHtml = `${icon} ${label}${addrSpan}`; if (section === 'found') { return `
${nameHtml}
`; } const connBtn = dev.connected ? `` : ``; const connDot = dev.connected ? `● ansluten` : `○ ej ansluten`; return `
${nameHtml} ${connDot} ${connBtn}
`; } async function laddaBluetooth() { try { const [stRes, devRes] = await Promise.all([ apiFetch('/api/bluetooth/status'), apiFetch('/api/bluetooth/devices'), ]); if (!stRes || !devRes) return; const st = await stRes.json(); const dev = await devRes.json(); _btPowered = st.powered; _btScanning = dev.scanning; const dot = document.getElementById('bt-status-dot'); const nameEl = document.getElementById('bt-adapter-name'); const addrEl = document.getElementById('bt-adapter-addr'); const powerBtn = document.getElementById('bt-power-btn'); const scanBtn = document.getElementById('bt-scan-btn'); if (!st.available) { dot.style.background = '#ef4444'; nameEl.textContent = 'Bluetooth ej tillgängligt'; return; } dot.style.background = st.powered ? '#22c55e' : '#555'; nameEl.textContent = st.name || 'Bluetooth'; addrEl.textContent = st.address || ''; powerBtn.textContent = st.powered ? 'Stäng av adapter' : 'Slå på adapter'; const known = dev.devices || []; const paired = known.filter(d => d.paired); const knownList = document.getElementById('bt-known-list'); if (paired.length) { knownList.innerHTML = paired.map(d => _btDeviceRow(d, 'known')).join(''); } else { knownList.innerHTML = 'Inga kopplade enheter'; } if (_btScanning) { scanBtn.disabled = true; document.getElementById('bt-scan-status').textContent = 'Skannar... (15 s)'; _btPollScan(); } } catch (e) { document.getElementById('bt-adapter-name').textContent = 'Fel: ' + e.message; } } async function btScan() { if (_btScanning) return; const btn = document.getElementById('bt-scan-btn'); const status = document.getElementById('bt-scan-status'); const wrap = document.getElementById('bt-found-wrap'); const list = document.getElementById('bt-found-list'); btn.disabled = true; status.textContent = 'Skannar... (15 s)'; list.innerHTML = 'Söker...'; wrap.style.display = 'block'; try { const res = await apiFetch('/api/bluetooth/scan', { method: 'POST' }); if (!res) { btn.disabled = false; return; } _btScanning = true; _btPollScan(); } catch (e) { status.textContent = 'Fel: ' + e.message; btn.disabled = false; } } function _btPollScan() { if (_btScanTimer) clearInterval(_btScanTimer); const knownAddrs = new Set(); document.querySelectorAll('#bt-known-list [data-btaddr]').forEach(el => knownAddrs.add(el.dataset.btaddr)); _btScanTimer = setInterval(async () => { try { const res = await apiFetch('/api/bluetooth/devices'); if (!res) return; const d = await res.json(); // Uppdatera kopplade enheter (parade) const known = (d.devices || []); const paired = known.filter(k => k.paired); const knownList = document.getElementById('bt-known-list'); knownList.innerHTML = paired.length ? paired.map(dev => _btDeviceRow(dev, 'known')).join('') : 'Inga kopplade enheter'; // Visa hittade men ej parade enheter under "Hittade enheter" const foundList = document.getElementById('bt-found-list'); const found = known.filter(k => !k.paired); if (found.length) { foundList.innerHTML = found.map(dev => _btDeviceRow(dev, 'found')).join(''); } if (!d.scanning) { clearInterval(_btScanTimer); _btScanTimer = null; _btScanning = false; document.getElementById('bt-scan-btn').disabled = false; document.getElementById('bt-scan-status').textContent = found.length + ' enhet(er) hittades'; if (!found.length) { document.getElementById('bt-found-list').innerHTML = 'Inga nya enheter hittades'; } } } catch {} }, 2500); } async function btPair(addr, name) { const status = document.getElementById('bt-scan-status'); status.textContent = `Kopplar ${name}...`; try { const res = await apiFetch('/api/bluetooth/pair', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr }), }); if (!res) return; const d = await res.json(); if (d.ok) { status.textContent = `✓ ${name} kopplad`; await laddaBluetooth(); } else { status.textContent = `✗ ${d.message || 'Misslyckades'}`; } } catch (e) { status.textContent = 'Fel: ' + e.message; } } async function btConnect(addr) { const status = document.getElementById('bt-scan-status'); status.textContent = 'Ansluter...'; try { const res = await apiFetch('/api/bluetooth/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr }), }); if (!res) return; const d = await res.json(); status.textContent = d.ok ? '✓ Ansluten' : '✗ ' + (d.message || 'Misslyckades'); await laddaBluetooth(); } catch (e) { status.textContent = 'Fel: ' + e.message; } } async function btDisconnect(addr) { await apiFetch('/api/bluetooth/disconnect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: addr }), }); await laddaBluetooth(); } async function btRemove(addr, name) { if (!confirm(`Ta bort "${name}" från kopplade enheter?`)) return; await apiFetch('/api/bluetooth/device/' + encodeURIComponent(addr), { method: 'DELETE' }); await laddaBluetooth(); } async function btTogglePower() { const btn = document.getElementById('bt-power-btn'); btn.disabled = true; try { const res = await apiFetch('/api/bluetooth/power', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ on: !_btPowered }), }); if (!res) { btn.disabled = false; return; } const st = await res.json(); _btPowered = st.powered; document.getElementById('bt-status-dot').style.background = st.powered ? '#22c55e' : '#555'; btn.textContent = st.powered ? 'Stäng av adapter' : 'Slå på adapter'; } catch {} btn.disabled = false; } // --- Klocka --- let _clockPos = 'top-right'; function setClockPos(pos) { _clockPos = pos; document.querySelectorAll('.clock-pos-btn').forEach(b => b.classList.remove('active')); const btn = document.getElementById('clkpos-' + pos); if (btn) btn.classList.add('active'); } async function laddaClock() { const res = await apiFetch('/api/clock'); if (!res) return; const d = await res.json(); document.getElementById('clock-style').value = d.style || 'digital'; document.getElementById('clock-show-date').checked = !!d.show_date; document.getElementById('clock-size').value = d.size != null ? d.size : 2; document.getElementById('clock-size-val').textContent = d.size != null ? d.size : 2; document.getElementById('clock-color').value = d.color || '#ffffff'; document.getElementById('clock-opacity').value = d.opacity != null ? d.opacity : 90; document.getElementById('clock-opacity-val').textContent = (d.opacity != null ? d.opacity : 90) + '%'; document.getElementById('clock-schedule-mode').value = d.schedule_mode || 'always'; setClockPos(d.position || 'top-right'); document.getElementById('clock-status').textContent = d.enabled ? '● Aktiv' : ''; } async function _sendClock(enabled) { const res = await apiFetch('/api/clock', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, style: document.getElementById('clock-style').value, show_date: document.getElementById('clock-show-date').checked, position: _clockPos, size: parseFloat(document.getElementById('clock-size').value), color: document.getElementById('clock-color').value, opacity: parseInt(document.getElementById('clock-opacity').value, 10), schedule_mode: document.getElementById('clock-schedule-mode').value, }), }); if (!res) return; document.getElementById('clock-status').textContent = enabled ? '● Aktiv' : ''; } function aktiveraClock() { _sendClock(true); } function avaktiveraClock() { _sendClock(false); } // --- Musik --- function musicSourceTypeChanged() { const isYt = document.getElementById('music-source-type').value === 'youtube'; document.getElementById('music-local-label').style.display = isYt ? 'none' : ''; document.getElementById('music-local-wrap').style.display = isYt ? 'none' : ''; document.getElementById('music-yt-label').style.display = isYt ? '' : 'none'; document.getElementById('music-yt-wrap').style.display = isYt ? '' : 'none'; document.getElementById('music-track-list-wrap').style.display = 'none'; } function openMusicFolderBrowser() { // Öppna filbläddraren men tillåt val av mapp _browserCallback = (path) => { // Användaren valde en fil — ta mappens sökväg const folder = path.includes('/') ? path.substring(0, path.lastIndexOf('/')) : path; document.getElementById('music-source-local').value = folder; laddaMusicTracks(folder); }; document.getElementById('browser-modal').style.display = 'flex'; browseTo('/home/mrfox'); } async function laddaMusicTracks(folder) { if (!folder) return; const wrap = document.getElementById('music-track-list-wrap'); const list = document.getElementById('music-track-list'); list.textContent = 'Laddar...'; wrap.style.display = 'block'; try { const res = await apiFetch('/api/music/tracks?path=' + encodeURIComponent(folder)); if (!res) return; const tracks = await res.json(); if (!tracks.length) { list.textContent = 'Inga musikfiler hittades i mappen.'; } else { list.innerHTML = tracks.map((t, i) => `
${i + 1}. ${escHtml(t.name)}
` ).join(''); } } catch { list.textContent = 'Kunde inte lista spår.'; } } async function laddaMusik() { const res = await apiFetch('/api/music'); if (!res) return; const d = await res.json(); document.getElementById('music-source-type').value = d.source_type || 'local'; document.getElementById('music-source-local').value = d.source_type === 'local' ? d.source : ''; document.getElementById('music-source-youtube').value = d.source_type === 'youtube' ? d.source : ''; document.getElementById('music-volume').value = d.volume != null ? d.volume : 50; document.getElementById('music-volume-val').textContent = (d.volume != null ? d.volume : 50) + '%'; document.getElementById('music-shuffle').checked = !!d.shuffle; document.getElementById('music-show-overlay').checked = d.show_overlay !== false; document.getElementById('music-overlay-position').value = d.overlay_position || 'bottom-left'; document.getElementById('music-status').textContent = d.enabled ? '● Aktiv' : ''; musicSourceTypeChanged(); // Visa spårlista om lokal mapp är satt if (d.source_type === 'local' && d.source) { await laddaMusicTracks(d.source); } // Visa "Spelar nu"-banner om musik spelas if (d.now_playing && (d.now_playing.title || d.now_playing.artist)) { _updateNowPlayingBanner(d.now_playing.title, d.now_playing.artist); } } async function extractMusicPlaylist() { const url = document.getElementById('music-source-youtube').value.trim(); const status = document.getElementById('music-extract-status'); const btn = document.getElementById('music-extract-btn'); if (!url) { status.textContent = 'Ange en URL först.'; return; } btn.disabled = true; status.textContent = 'Hämtar spellista…'; try { const res = await apiFetch('/api/music/extract', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); const d = await res.json(); if (!res.ok) { status.textContent = '⚠ ' + (d.error || 'Okänt fel'); } else { status.style.color = '#166534'; status.textContent = `✓ ${d.count} låtar hämtade och redo att spelas.`; } } catch (e) { status.textContent = '⚠ Nätverksfel: ' + e.message; } btn.disabled = false; } async function setMusik(enabled) { const sourceType = document.getElementById('music-source-type').value; const rawSource = sourceType === 'youtube' ? document.getElementById('music-source-youtube').value.trim() : document.getElementById('music-source-local').value.trim(); const source = sourceType === 'youtube' ? normalizeMusicYouTubeUrl(rawSource) : rawSource; const res = await apiFetch('/api/music', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled, source_type: sourceType, source, volume: parseInt(document.getElementById('music-volume').value, 10), shuffle: document.getElementById('music-shuffle').checked, show_overlay: document.getElementById('music-show-overlay').checked, overlay_position: document.getElementById('music-overlay-position').value, }), }); if (!res) return; document.getElementById('music-status').textContent = enabled ? '● Aktiv' : ''; } function _updateNowPlayingBanner(title, artist) { const banner = document.getElementById('now-playing-banner'); const text = document.getElementById('np-banner-text'); if (!banner) return; if (title || artist) { const label = artist ? `${artist} — ${title}` : title; text.textContent = 'Spelar nu: ' + label; banner.style.display = 'flex'; } else { banner.style.display = 'none'; } } // Lyssna på now_playing-händelser från servern socket.on('now_playing', (data) => { _updateNowPlayingBanner(data.title || '', data.artist || ''); }); // --- Föreslagna uppspelningar från chatten --- socket.on('play_suggestion', (data) => { const area = document.getElementById('play-suggestions'); if (!area) return; const url = data.url || ''; const short = url.length > 55 ? url.slice(0, 52) + '…' : url; const item = document.createElement('div'); item.style.cssText = 'display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;background:rgba(129,140,248,.1);border:1px solid rgba(129,140,248,.25);border-radius:8px;font-size:.88rem;'; item.innerHTML = ` 🎬 ${escHtml(data.name)} vill visa: ${escHtml(short)} `; area.prepend(item); area.style.display = 'flex'; }); function _updateSuggestionsVisibility() { const area = document.getElementById('play-suggestions'); if (area && !area.children.length) area.style.display = 'none'; } function visaSuggestion(url, itemEl) { const embed = youtubeEmbedUrl(url) || youtubePlaylistEmbedUrl(url); socket.emit('admin_switch', { type: 'url', source: embed || url }); if (itemEl) { itemEl.remove(); _updateSuggestionsVisibility(); } } // --- Sovläge --- async function laddaSovlage() { const res = await apiFetch('/api/sleep'); if (!res) return; const d = await res.json(); document.getElementById('sleep-enabled').checked = !!d.enabled; document.getElementById('sleep-start').value = d.start || '22:00'; document.getElementById('sleep-end').value = d.end || '07:00'; } async function sparaSovlage() { const status = document.getElementById('sleep-status'); status.textContent = 'Sparar...'; const res = await apiFetch('/api/sleep', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: document.getElementById('sleep-enabled').checked, start: document.getElementById('sleep-start').value, end: document.getElementById('sleep-end').value, }), }); if (!res) return; status.textContent = '✓ Sparat'; setTimeout(() => { status.textContent = ''; }, 3000); } // --- Systeminfo --- async function laddaSysinfo() { const res = await apiFetch('/api/sysinfo'); if (!res) return; const d = await res.json(); document.getElementById('cpu').textContent = d.cpu + '%'; document.getElementById('ram').textContent = d.ram + '%'; document.getElementById('disk').textContent = d.disk + '%'; document.getElementById('uptime').textContent = d.uptime; document.getElementById('active-source').textContent = d.active_type !== '—' ? `[${d.active_type}] ${d.active_source}` : '—'; } // --- Filbläddrare --- let _browserCallback = null; const FILE_TYPES = { video: ['mp4','webm','avi','mkv','mov'], image: ['jpg','jpeg','png','gif','webp','bmp'], pdf: ['pdf','ppt','pptx','odp'], url: ['html','htm'], }; function extType(filename) { const ext = filename.split('.').pop().toLowerCase(); for (const [type, exts] of Object.entries(FILE_TYPES)) { if (exts.includes(ext)) return type; } return 'url'; } function extClass(filename) { const t = extType(filename); return t === 'url' ? 'file-html' : 'file-' + t; } function openBrowser(callback) { _browserCallback = callback; document.getElementById('browser-modal').style.display = 'flex'; browseTo('/home/mrfox'); } function closeBrowser() { document.getElementById('browser-modal').style.display = 'none'; _browserCallback = null; } function browserOverlayClick(e) { if (e.target === document.getElementById('browser-modal')) closeBrowser(); } async function browseTo(path) { const res = await apiFetch('/api/browse?path=' + encodeURIComponent(path)); if (!res) return; const data = await res.json(); document.getElementById('browser-path').textContent = data.path; let html = ''; if (data.parent) { html += `
.. (upp)
`; } for (const e of data.entries) { if (e.type === 'dir') { html += `
${escHtml(e.name)}
`; } else { html += `
${escHtml(e.name)}
`; } } if (!html) html = '
Inga filer hittades
'; const list = document.getElementById('browser-list'); list.innerHTML = html; list.querySelectorAll('.browser-entry.dir').forEach(el => el.addEventListener('click', () => browseTo(el.dataset.path)) ); list.querySelectorAll('.browser-entry:not(.dir)').forEach(el => el.addEventListener('click', () => { if (_browserCallback) _browserCallback(el.dataset.path, extType(el.dataset.path)); closeBrowser(); }) ); } // --- Helpers --- function fmtDuration(mins) { const totalSec = Math.round(mins * 60); const m = Math.floor(totalSec / 60); const s = totalSec % 60; return m > 0 ? `${m} min ${s > 0 ? s + ' sek' : ''}`.trim() : `${s} sek`; } function escHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // --- Övergångar --- async function laddaTransition() { try { const res = await apiFetch('/api/transition'); if (!res) return; const d = await res.json(); const sel = document.getElementById('tx-type'); const dur = document.getElementById('tx-duration'); if (sel) sel.value = d.type || 'fade'; if (dur) dur.value = d.duration || 600; } catch {} } async function saveTransition() { const type = document.getElementById('tx-type').value; const duration = parseInt(document.getElementById('tx-duration').value, 10) || 600; const status = document.getElementById('tx-status'); try { const res = await apiFetch('/api/transition', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, duration }), }); if (!res) return; status.textContent = 'Sparat!'; setTimeout(() => { status.textContent = ''; }, 2000); } catch (e) { status.textContent = 'Fel: ' + e.message; } } // --- Init --- async function init() { await laddaSysinfo(); await laddaJobb(); await laddaInstallningar(); await laddaVolym(); await laddaTicker(); await laddaChat(); await laddaYtChat(); await laddaClock(); await laddaMusik(); await laddaWifiStatus(); await laddaBluetooth(); await laddaWifiSaved(); await laddaSovlage(); await laddaTransition(); setInterval(laddaSysinfo, 10000); const res = await apiFetch('/api/sysinfo'); if (!res) return; const d = await res.json(); if (d.active_type === 'url') { document.getElementById('url-input').value = d.active_source; } } init(); // Theme toggle control for admin UI (function(){ const KEY = 'kiosk_theme'; function getTheme(){ return localStorage.getItem(KEY) || 'auto'; } function applyTheme(pref){ if (pref === 'auto') { delete document.documentElement.dataset.theme; } else { document.documentElement.dataset.theme = pref; } } function cycleTheme(){ const order = ['auto','dark','light']; const cur = getTheme(); const next = order[(order.indexOf(cur)+1) % order.length]; localStorage.setItem(KEY, next); applyTheme(next); updateBtn(); // notify other windows/tabs window.dispatchEvent(new Event('kiosk-theme-changed')); } function updateBtn(){ const t = getTheme(); btn.textContent = t === 'auto' ? 'Tema: Auto' : (t === 'dark' ? 'Tema: Mörkt' : 'Tema: Ljust'); } const container = document.querySelector('.container'); if (!container) return; const btn = document.createElement('button'); btn.className = 'secondary'; btn.style.marginLeft = 'auto'; btn.style.marginBottom = '1rem'; btn.addEventListener('click', cycleTheme); // Insert button under the H1 const h1 = container.querySelector('h1'); if (h1) h1.insertAdjacentElement('afterend', btn); applyTheme(getTheme()); updateBtn(); // React to storage changes from other tabs window.addEventListener('storage', (e) => { if (e.key === KEY) { applyTheme(e.newValue || 'auto'); updateBtn(); } }); // Also react to programmatic events window.addEventListener('kiosk-theme-changed', () => { applyTheme(getTheme()); updateBtn(); }); })();