archive-ccc8458/video/admin.php 6.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sianel — Admin</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/video/sianel.css">
</head>
<body class="page-admin">
<script>
  // Gate the page: no stored password → go to auth page.
  if (!localStorage.getItem('vidpass')) location.replace('/video/login');
</script>

<div class="topbar">
  <a href="/video/" class="brand"><img class="logo" src="/video/sianel_logo.png" alt="Sianel"></a>
  <span><a href="/video/">Public page</a> &nbsp;·&nbsp; <a href="/video/login">Password</a></span>
</div>

<label for="file">Video file (mp4, webm, mov, mkv — max 100 MB)</label>
<input id="file" type="file" accept="video/mp4,video/webm,video/quicktime,video/x-matroska">

<label class="check"><input type="checkbox" id="unlisted"> Unlisted (hidden from the public page; still reachable by direct link)</label>

<div class="row">
  <button id="go">Upload</button>
</div>

<div id="progress"><div id="bar"></div></div>
<div id="out"></div>

<h2>Your videos</h2>
<div class="row" style="margin-bottom:.5rem">
  <button id="refresh" type="button">Refresh list</button>
</div>
<div id="list"><p class="muted">Loading…</p></div>
<div id="pager" class="pager"></div>

<script>
const PAGE_SIZE = 20;
let listOffset = 0;

const fileEl  = document.getElementById('file');
const out     = document.getElementById('out');
const bar     = document.getElementById('bar');
const prog    = document.getElementById('progress');
const listEl  = document.getElementById('list');
const pagerEl = document.getElementById('pager');

// Returns the stored password, or sends the user to the login page if it's gone.
function getPassword() {
  const pw = localStorage.getItem('vidpass');
  if (!pw) { location.replace('/video/login'); return null; }
  return pw;
}

document.getElementById('go').onclick = () => {
  const pw = getPassword(); if (!pw) return;
  const file = fileEl.files[0];
  if (!file) { out.textContent = 'Choose a file.'; return; }

  const fd = new FormData();
  fd.append('file', file);
  if (document.getElementById('unlisted').checked) fd.append('unlisted', '1');
  const xhr = new XMLHttpRequest();
  xhr.open('POST', '/video/api/upload');
  xhr.setRequestHeader('Authorization', 'Bearer ' + pw);
  prog.style.display = 'block'; bar.style.width = '0';
  xhr.upload.onprogress = e => {
    if (e.lengthComputable) bar.style.width = (e.loaded / e.total * 100) + '%';
  };
  xhr.onload = () => {
    let j = null; try { j = JSON.parse(xhr.responseText); } catch {}
    if (xhr.status >= 200 && xhr.status < 300 && j) {
      out.innerHTML = 'Uploaded.\n\nPage:  <a href="' + j.url + '">' + j.url + '</a>'
                    + '\nEmbed: ' + j.embed;
      listOffset = 0; // jump to the first page so the new upload is visible
      refreshList();
    } else {
      out.textContent = 'Error ' + xhr.status + ': ' + ((j && j.error) || xhr.responseText);
    }
  };
  xhr.onerror = () => out.textContent = 'Network error.';
  xhr.send(fd);
};

function fmtSize(n) {
  if (n < 1024) return n + ' B';
  if (n < 1024*1024) return (n/1024).toFixed(1) + ' KB';
  return (n/1024/1024).toFixed(1) + ' MB';
}
function fmtDate(s) {
  if (!s) return '';
  return new Date(s * 1000).toLocaleString();
}

function renderPager(total) {
  const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
  const page = Math.floor(listOffset / PAGE_SIZE) + 1;
  if (totalPages <= 1) { pagerEl.innerHTML = ''; return; }

  pagerEl.innerHTML = '';
  const prev = document.createElement('button');
  prev.textContent = '← Prev';
  prev.disabled = page <= 1;
  prev.onclick = () => { listOffset = Math.max(0, listOffset - PAGE_SIZE); refreshList(); };

  const label = document.createElement('span');
  label.className = 'count';
  label.textContent = `Page ${page} of ${totalPages}`;

  const next = document.createElement('button');
  next.textContent = 'Next →';
  next.disabled = page >= totalPages;
  next.onclick = () => { listOffset += PAGE_SIZE; refreshList(); };

  pagerEl.append(prev, label, next);
}

async function refreshList() {
  const pw = getPassword(); if (!pw) return;
  listEl.innerHTML = '<p class="muted">Loading…</p>';
  pagerEl.innerHTML = '';
  try {
    const res = await fetch(`/video/api/list?offset=${listOffset}&count=${PAGE_SIZE}`, {
      headers: { 'Authorization': 'Bearer ' + pw },
    });
    const j = await res.json();
    if (!res.ok) { listEl.innerHTML = '<p>Error: ' + (j.error || res.status) + '</p>'; return; }

    // Paged past the end (e.g. deleted the last item on a page) → step back.
    if (j.videos.length === 0 && j.total > 0 && listOffset > 0) {
      listOffset = (Math.ceil(j.total / PAGE_SIZE) - 1) * PAGE_SIZE;
      return refreshList();
    }
    if (!j.videos.length) { listEl.innerHTML = '<p class="muted">No videos yet.</p>'; return; }

    const rows = j.videos.map(v => {
      const thumbCell = v.thumbnail
        ? `<img class="thumb" src="${v.thumbnail}" alt="">`
        : `<div class="thumb-placeholder" aria-label="No thumbnail">▶</div>`;
      const badge = v.unlisted ? '<span class="badge">unlisted</span>' : '';
      return `
      <tr data-hash="${v.hash}">
        <td>${thumbCell}</td>
        <td><a href="${v.url}">${v.hash}</a>${badge}<br><span class="muted">${v.filename}</span></td>
        <td>${fmtSize(v.size)}<br><span class="muted">${fmtDate(v.uploaded)}</span></td>
        <td><button class="del">Delete</button></td>
      </tr>`;
    }).join('');
    listEl.innerHTML = `<table>
      <tbody>${rows}</tbody></table>`;
    listEl.querySelectorAll('button.del').forEach(btn => {
      btn.onclick = () => deleteVideo(btn.closest('tr').dataset.hash);
    });
    renderPager(j.total);
  } catch (e) {
    listEl.innerHTML = '<p>Network error.</p>';
  }
}

async function deleteVideo(hash) {
  const pw = getPassword(); if (!pw) return;
  if (!confirm('Delete ' + hash + '?')) return;
  const fd = new FormData(); fd.append('hash', hash);
  const res = await fetch('/video/api/delete', {
    method: 'POST',
    headers: { 'Authorization': 'Bearer ' + pw },
    body: fd,
  });
  const j = await res.json().catch(() => ({}));
  if (res.ok) { refreshList(); }
  else { alert('Delete failed: ' + (j.error || res.status)); }
}

document.getElementById('refresh').onclick = refreshList;

refreshList();
</script>
</body>
</html>