foxygit / infodisplay Log in
commits tags

/kiosk/templates/chat_display.html · 7.84 KB

raw
<!DOCTYPE html>
<html lang="sv">
<head>
  <meta charset="UTF-8">
  <title>Kiosk Livechat</title>
  <script src="/static/socket.io.min.js"></script>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }

    html, body {
      width: 100%; height: 100%;
      background: #0f0f0f;
      color: #e8e8e8;
      font-family: 'Segoe UI', system-ui, sans-serif;
      overflow: hidden;
    }

    /* Layout — chat left, QR panel right */
    #root {
      display: flex;
      height: 100vh;
      gap: 0;
    }

    /* === Chat column === */
    #chat-col {
      flex: 1;
      display: flex;
      flex-direction: column;
      padding: 1.2rem 1rem 1rem 1.4rem;
      overflow: hidden;
    }

    #chat-header {
      display: flex;
      align-items: center;
      gap: .75rem;
      margin-bottom: .9rem;
      flex-shrink: 0;
    }

    #chat-header h1 {
      font-size: 1.35rem;
      font-weight: 700;
      color: #fff;
      letter-spacing: .02em;
    }

    #viewer-count {
      font-size: .85rem;
      color: #888;
      background: #1a1a1a;
      padding: .2rem .6rem;
      border-radius: 99px;
    }

    /* Message list — fills remaining height, scrolls from bottom */
    #messages {
      flex: 1;
      overflow: hidden;
      position: relative;
    }

    #msg-inner {
      position: absolute;
      bottom: 0; left: 0; right: 0;
      display: flex;
      flex-direction: column;
      gap: .55rem;
      padding-bottom: .2rem;
    }

    .msg {
      display: flex;
      align-items: flex-start;
      gap: .55rem;
      animation: slide-in .25s ease;
    }

    @keyframes slide-in {
      from { opacity: 0; transform: translateY(8px); }
      to   { opacity: 1; transform: translateY(0);   }
    }

    .msg-avatar {
      width: 2rem; height: 2rem;
      border-radius: 50%;
      display: flex; align-items: center; justify-content: center;
      font-size: 1rem;
      flex-shrink: 0;
      margin-top: .1rem;
      overflow: hidden;
    }

    .msg-body { flex: 1; min-width: 0; }

    .msg-name {
      font-size: .78rem;
      font-weight: 700;
      margin-bottom: .15rem;
    }

    .msg-text {
      font-size: 1.05rem;
      line-height: 1.45;
      word-break: break-word;
      color: #e8e8e8;
    }

    .msg.system .msg-text {
      font-size: .82rem;
      color: #666;
      font-style: italic;
    }

    .msg-img { max-width: 240px; max-height: 180px; border-radius: 8px; display: block; margin-top: .3rem; }

    .msg-suggestion {
      border-left: 3px solid #818cf8;
      padding: .4rem .65rem;
      background: #818cf818;
      border-radius: 0 8px 8px 0;
      margin-top: .2rem;
    }
    .msg-suggestion .sug-label { font-size: .72rem; color: #818cf8; margin-bottom: .15rem; }
    .msg-suggestion .sug-url   { font-size: .9rem; color: #e8e8e8; word-break: break-all; }

    /* === QR panel === */
    #qr-col {
      width: 180px;
      flex-shrink: 0;
      background: #181818;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: .9rem;
      padding: 1.2rem .8rem;
      border-left: 1px solid #222;
    }

    #qr-canvas {
      border-radius: 8px;
      background: #fff;
      padding: 6px;
    }

    #qr-label {
      font-size: .72rem;
      color: #666;
      text-align: center;
      word-break: break-all;
    }

    #qr-title {
      font-size: .85rem;
      font-weight: 600;
      color: #aaa;
      text-align: center;
    }

    .dot-live {
      width: .55rem; height: .55rem;
      background: #ef4444;
      border-radius: 50%;
      display: inline-block;
      animation: pulse 1.4s infinite;
    }

    @keyframes pulse {
      0%,100% { opacity: 1; }
      50%      { opacity: .3; }
    }
  </style>
</head>
<body>
<div id="root">

  <!-- Chat column -->
  <div id="chat-col">
    <div id="chat-header">
      <span class="dot-live"></span>
      <h1>Livechat</h1>
      <span id="viewer-count">0 aktiva</span>
    </div>
    <div id="messages">
      <div id="msg-inner"></div>
    </div>
  </div>

  <!-- QR panel -->
  <div id="qr-col">
    <div id="qr-title">Delta i chatten</div>
    <img id="qr-img" src="/api/chat-qr" alt="QR" style="width:140px;height:140px;border-radius:8px;background:#fff;display:block">
    <div id="qr-label">{{ local_ip }}:5000/chat-mobile</div>
  </div>

</div>

<script>
// I overlay-läge (?overlay=1) döljs QR-panelen och bakgrunden görs transparent
const isOverlay = new URLSearchParams(location.search).has('overlay');
if (isOverlay) {
  document.body.style.background = 'transparent';
  document.getElementById('root').style.background = 'transparent';
  document.getElementById('qr-col').style.display  = 'none';
  document.getElementById('chat-col').style.padding = '.5rem .75rem';
}

const MOBILE_URL = 'http://{{ local_ip }}:5000/chat-mobile';
const MAX_MSGS   = 40;

// SocketIO — /chat namespace
const socket = io('/chat');

const msgInner   = document.getElementById('msg-inner');
const countEl    = document.getElementById('viewer-count');

function avatarBg(color) {
  return `background:${color}22; color:${color};`;
}

function avatarContent(emoji, name, avatarUrl) {
  if (avatarUrl) return `<img src="${avatarUrl}" style="width:100%;height:100%;object-fit:cover;border-radius:50%">`;
  return escHtml(emoji || name.charAt(0).toUpperCase());
}

function addMsg(name, text, color, emoji, avatarUrl) {
  const div = document.createElement('div');
  div.className = 'msg';
  div.innerHTML = `
    <div class="msg-avatar" style="${avatarBg(color)}">${avatarContent(emoji, name, avatarUrl)}</div>
    <div class="msg-body">
      <div class="msg-name" style="color:${color}">${escHtml(name)}</div>
      <div class="msg-text">${_renderContent(text)}</div>
    </div>`;
  msgInner.appendChild(div);
  while (msgInner.children.length > MAX_MSGS) msgInner.removeChild(msgInner.firstChild);
}

function addSuggestion(d) {
  const div = document.createElement('div');
  div.className = 'msg';
  div.innerHTML = `
    <div class="msg-avatar" style="${avatarBg(d.color)}">${avatarContent(d.emoji, d.name, d.avatar_url)}</div>
    <div class="msg-body">
      <div class="msg-name" style="color:${d.color}">${escHtml(d.name)}</div>
      ${_suggestionHtml(d.text || d.url)}
    </div>`;
  msgInner.appendChild(div);
  while (msgInner.children.length > MAX_MSGS) msgInner.removeChild(msgInner.firstChild);
}

function addSystem(text) {
  const div = document.createElement('div');
  div.className = 'msg system';
  div.innerHTML = `<div class="msg-body"><div class="msg-text">${escHtml(text)}</div></div>`;
  msgInner.appendChild(div);
  while (msgInner.children.length > MAX_MSGS) msgInner.removeChild(msgInner.firstChild);
}

function escHtml(s) {
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

const _IMG_RE = /^(?:https?:\/\/|\/)[^\s"<>]+\.(gif|jpe?g|png|webp|avif)(\?[^\s"<>]*)?$/i;

function _renderContent(text) {
  const t = text.trim();
  if (_IMG_RE.test(t)) {
    const safe = t.replace(/"/g, '%22');
    return `<img src="${safe}" class="msg-img" loading="lazy" onerror="this.style.display='none'">`;
  }
  return escHtml(text);
}

function _suggestionHtml(url) {
  const short = url.length > 60 ? url.slice(0, 57) + '…' : url;
  return `<div class="msg-suggestion">
    <div class="sug-label">🎬 Förslag till displayen</div>
    <div class="sug-url">${escHtml(short)}</div>
  </div>`;
}

socket.on('chat_history', (msgs) => {
  msgInner.innerHTML = '';
  msgs.forEach(m => {
    if (m.kind === 'play_suggestion') addSuggestion(m);
    else addMsg(m.name, m.text, m.color, m.emoji, m.avatar_url);
  });
});

socket.on('chat_message', (d) => {
  addMsg(d.name, d.text, d.color, d.emoji, d.avatar_url);
});

socket.on('play_suggestion', (d) => {
  addSuggestion(d);
});

socket.on('system_message', (d) => {
  addSystem(d.text);
});

socket.on('viewer_count', (d) => {
  countEl.textContent = d.count + ' aktiva';
});
</script>
</body>
</html>