foxygit / infodisplay Log in
commits tags

/kiosk/static/admin.js · 63.57 KB

raw
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 = '<tr><td colspan="7" class="empty">Inga schemalagda jobb</td></tr>';
    return;
  }

  tbody.innerHTML = jobs.map(j => `
    <tr id="job-${j.id}" data-id="${j.id}">
      <td>${escHtml(j.label)}</td>
      <td>${fmtDuration(j.duration)}</td>
      <td>${escHtml(j.type)}</td>
      <td class="source-cell">${escHtml(j.source)}</td>
      <td>${j.show_chat ? '✓' : '—'}</td>
      <td>
        <button class="toggle ${j.enabled ? 'on' : 'off'}" onclick="toggleJobb(${j.id})">
          ${j.enabled ? 'Ja' : 'Nej'}
        </button>
      </td>
      <td>
        <button class="secondary" onclick="openEdit(${j.id})">Redigera</button>
        <button class="secondary" onclick="moveJob(${j.id}, 'up')">▲</button>
        <button class="secondary" onclick="moveJob(${j.id}, 'down')">▼</button>
        <button class="danger" onclick="taBortJobb(${j.id})">X</button>
      </td>
    </tr>
  `).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 `<option value="${p.index}"${p.index === settings.audio_profile_index ? ' selected' : ''}>${label}</option>`;
  }).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 => `<option value="${o.val}"${o.val===settings.display_output? ' selected':''}>${escHtml(o.label)}</option>`).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 = '<option value="">(Ingen / lämna tomt)</option>' + modes.map(m => `<option value="${m}">${formatLabel(m)}${m===info.current? ' (aktuell)':''}</option>`).join('') + `<option value="__custom__">${formatLabel('__custom__')}</option>`;
    // 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) =>
    `<span style="width:4px;border-radius:1px;height:${h}px;background:${i < lit ? c : '#333'}"></span>`
  ).join('');
  return `<div style="display:flex;align-items:flex-end;gap:2px;height:14px">${bars}</div>`;
}

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 = '<span style="color:#555;font-style:italic">Inga sparade nätverk</span>';
      return;
    }
    list.innerHTML = d.saved.map(n => `
      <div style="display:flex;align-items:center;gap:.5rem;padding:.35rem .5rem;background:var(--surface);border-radius:6px">
        <span style="flex:1">${escHtml(n.ssid)}${n.current ? ' <span style="color:#22c55e;font-size:.75rem">● aktiv</span>' : ''}</span>
        <button class="secondary" style="padding:.2rem .6rem;font-size:.8rem" onclick="wifiRemove('${escHtml(n.id)}','${escHtml(n.ssid)}')">Ta bort</button>
      </div>`).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 = '<span style="color:#555;font-style:italic">Söker...</span>';
  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 = '<span style="color:#555;font-style:italic">Inga nätverk hittades</span>';
    } else {
      list.innerHTML = d.networks.map(n => {
        const lock = n.security !== 'open'
          ? `<span style="font-size:.75rem;color:#aaa">${escHtml(n.security)}</span>` : '';
        return `
          <div style="display:flex;align-items:center;gap:.5rem;padding:.35rem .5rem;background:var(--surface);border-radius:6px">
            ${_signalBarsHtml(n.signal)}
            <span style="flex:1">${escHtml(n.ssid)}${n.known ? ' <span style="color:#22c55e;font-size:.75rem">✓</span>' : ''}</span>
            ${lock}
            <button class="secondary" style="padding:.2rem .6rem;font-size:.8rem"
              onclick="wifiOpenModal('${escHtml(n.ssid)}',${n.security === 'open'})">
              Anslut
            </button>
          </div>`;
      }).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
    ? `<span style="font-size:.75rem;color:#555;margin-left:.4rem;font-family:monospace">${escHtml(dev.address)}</span>`
    : '';
  const nameHtml = `<span style="flex:1">${icon} ${label}${addrSpan}</span>`;

  if (section === 'found') {
    return `
      <div style="display:flex;align-items:center;gap:.5rem;padding:.35rem .5rem;background:var(--surface);border-radius:6px">
        ${nameHtml}
        <button class="secondary" style="padding:.2rem .6rem;font-size:.8rem" onclick="btPair('${escHtml(dev.address)}','${escHtml(dev.name)}')">Koppla</button>
      </div>`;
  }

  const connBtn = dev.connected
    ? `<button class="secondary" style="padding:.2rem .6rem;font-size:.8rem" onclick="btDisconnect('${escHtml(dev.address)}')">Koppla från</button>`
    : `<button class="secondary" style="padding:.2rem .6rem;font-size:.8rem" onclick="btConnect('${escHtml(dev.address)}')">Anslut</button>`;
  const connDot = dev.connected
    ? `<span style="color:#22c55e;font-size:.75rem">● ansluten</span>`
    : `<span style="color:#555;font-size:.75rem">○ ej ansluten</span>`;

  return `
    <div style="display:flex;align-items:center;gap:.5rem;padding:.35rem .5rem;background:var(--surface);border-radius:6px">
      ${nameHtml}
      ${connDot}
      ${connBtn}
      <button class="danger" style="padding:.2rem .6rem;font-size:.8rem" onclick="btRemove('${escHtml(dev.address)}','${escHtml(dev.name || dev.address)}')">Ta bort</button>
    </div>`;
}

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 = '<span style="color:#555;font-style:italic">Inga kopplade enheter</span>';
    }

    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 = '<span style="color:#555;font-style:italic">Söker...</span>';
  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('')
        : '<span style="color:#555;font-style:italic">Inga kopplade enheter</span>';

      // 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 = '<span style="color:#555;font-style:italic">Inga nya enheter hittades</span>';
        }
      }
    } 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) =>
        `<div>${i + 1}. ${escHtml(t.name)}</div>`
      ).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 = `
    <span style="font-size:1rem;flex-shrink:0">🎬</span>
    <span style="flex:1;min-width:0"><b style="color:#c4b5fd">${escHtml(data.name)}</b> vill visa: <span style="color:#818cf8;word-break:break-all">${escHtml(short)}</span></span>
    <button class="secondary" style="padding:.25rem .65rem;font-size:.82rem;flex-shrink:0" onclick="visaSuggestion('${escHtml(url)}',this.closest('div'))">Spela nu</button>
    <button style="background:none;border:none;color:#555;cursor:pointer;font-size:1.1rem;line-height:1;padding:.1rem .3rem;flex-shrink:0" onclick="this.closest('div').remove();_updateSuggestionsVisibility()">✕</button>`;
  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 += `<div class="browser-entry dir" data-path="${escHtml(data.parent)}">.. (upp)</div>`;
  }
  for (const e of data.entries) {
    if (e.type === 'dir') {
      html += `<div class="browser-entry dir" data-path="${escHtml(e.path)}">${escHtml(e.name)}</div>`;
    } else {
      html += `<div class="browser-entry ${escHtml(extClass(e.name))}" data-path="${escHtml(e.path)}">${escHtml(e.name)}</div>`;
    }
  }
  if (!html) html = '<div style="padding:.75rem 1rem;color:#555;font-style:italic">Inga filer hittades</div>';

  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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

// --- Ö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(); });
})();