source: Klonkt/src/views/pages/admin-videos.ejs@ 97bcf7e

main
Last change on this file since 97bcf7e was 97bcf7e, checked in by Robin <roboburr@…>, 6 weeks ago

Video op maat in de post, en een Video-tab in Media

Robins opdracht (30-7): videoverwerking beter aanpakken. Een video die vanuit
Shaer op de Klonkt van een ward landt, rendert nu op maat in plaats van te
groot: dezelfde regels als afbeeldingen in de post-tekst, plus een plafond
(70vh/540px) en object-fit zodat een portret-video de kolom niet meer
volduwt. Geldt ook in de Krant en Berichten (tl-content), waar dezelfde
content langskomt. En de video-tag uit de composer krijgt preload=metadata,
dus de browser toont het eerste frame als voorlopige thumbnail.

Nieuw: Beheer -> Media -> Video's. De videos die C2S-uploads binnenbrachten
(Shaers composer, de boei), met bestandsnaam, grootte, gebruiksteller uit
content plus c2s_attachments, kopieer-URL, en verwijderen alleen voor
ongebruikte, met dezelfde basename-en-binnen-de-map-teugels als bij
afbeeldingen.

Echte posterframes (server-side thumbnails) kunnen hier nog niet: er staat
geen ffmpeg op de VPS. Dat wordt een bead, met de aantekening dat Shaer
thumbnails straks native kan maken (AVAssetImageGenerator /
MediaMetadataRetriever), zonder server-hulp.

Changed files:
src/assets/css/style.css

  • .post-content video en .tl-content video: maatvoering + plafond

src/services/ActivityPubService.js

  • de gevouwen video-tag preloadt zijn metadata

src/routes/admin-media.js

  • videoEntries + GET /videos + POST /videos/delete

src/views/partials/media-tabs.ejs

  • de Video-tab tussen Afbeeldingen en Audio

src/services/i18n.js

  • media_videos/videos_*-strings, NL/EN/DE

New file:
src/views/pages/admin-videos.ejs

  • de Video-tab zelf, zelfde snit als de afbeeldingenpagina

remarks: 336 tests groen, server start. Draait nog nergens; beta/demo
bijwerken maakt het zichtbaar.

-robo
Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 3.8 KB
Line 
1<%# Beheer → Media → Video's (Robins opdracht, 30-7): the videos C2S uploads
2 brought in (Shaer's composer, the help buoy), with usage and cleanup.
3 Same shape as the image page; the preview is the video itself with
4 preload="metadata", so the browser shows the first frame as its
5 thumbnail until real poster generation lands (bead: ffmpeg ontbreekt). %>
6<div class="admin-wrap" style="max-width:980px;margin:0 auto;padding:1.5rem 1rem;">
7 <a href="/admin" class="btn" style="margin-bottom:1rem;display:inline-block;">← <%= t('nav.admin') %></a>
8 <h1 style="font-family:var(--display,serif);margin:.2rem 0 1rem;"><%= t('admin.t_media') %></h1>
9
10 <%- include('../partials/media-tabs', { active: 'videos', audioOn: audioOn }) %>
11
12 <% if (success) { %><p style="background:rgba(60,160,90,.15);border:1px solid rgba(60,160,90,.4);padding:.5rem .8rem;border-radius:8px;"><%= success %></p><% } %>
13
14 <p style="margin:0 0 1rem;opacity:.75;"><%= items.length %> <%= t('admin.videos_count') %></p>
15
16 <% if (!items.length) { %>
17 <p style="opacity:.7;"><%= t('admin.videos_empty') %></p>
18 <% } else { %>
19 <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:1rem;">
20 <% items.forEach(function(it){ %>
21 <div class="media-card" data-file="<%= it.file %>" style="border:1px solid var(--rule,rgba(128,128,128,.25));border-radius:10px;overflow:hidden;background:var(--paper-2,rgba(128,128,128,.06));">
22 <div style="position:relative;aspect-ratio:16/10;background:#000;">
23 <video src="<%= it.url %>" preload="metadata" controls playsinline style="width:100%;height:100%;object-fit:contain;display:block;"></video>
24 <% if (!it.usedCount) { %><span style="position:absolute;top:6px;left:6px;background:#b4452e;color:#fff;border-radius:6px;padding:1px 6px;font-size:11px;z-index:1;"><%= t('admin.media_unused') %></span><% } %>
25 </div>
26 <div style="padding:.5rem .6rem;font-size:13px;">
27 <div style="opacity:.7;word-break:break-all;"><%= it.file %></div>
28 <div style="opacity:.7;"><%= it.kb %> KB · <%= it.usedCount %>×</div>
29 <div style="display:flex;gap:.4rem;margin-top:.45rem;">
30 <button type="button" class="btn" style="padding:3px 8px;font-size:12px;" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
31 <% if (!it.usedCount) { %>
32 <button type="button" class="btn" style="padding:3px 8px;font-size:12px;background:transparent;border-color:#b4452e;color:#b4452e;" data-del="<%= it.file %>">🗑</button>
33 <% } %>
34 </div>
35 </div>
36 </div>
37 <% }) %>
38 </div>
39 <% } %>
40</div>
41
42<script>
43(function () {
44 if (window.__videosWired) return; window.__videosWired = true;
45 var T = <%- JSON.stringify({ copy: t('admin.media_copy'), delC: t('admin.videos_del_confirm') }) %>;
46 document.addEventListener('click', function (e) {
47 var c = e.target.closest('[data-copy]');
48 if (c) {
49 var u = location.origin + c.getAttribute('data-copy');
50 var done = function () { var o = c.textContent; c.textContent = '✓'; setTimeout(function () { c.textContent = o === '✓' ? T.copy : o; }, 1200); };
51 if (navigator.clipboard) navigator.clipboard.writeText(u).then(done).catch(function () { window.prompt('URL', u); });
52 else window.prompt('URL', u);
53 return;
54 }
55 var d = e.target.closest('[data-del]');
56 if (d) {
57 if (!window.confirm(T.delC)) return;
58 fetch('/admin/media/videos/delete', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file: d.getAttribute('data-del') }) })
59 .then(function (r) { return r.json(); })
60 .then(function (j) { if (j && j.ok) { var card = d.closest('.media-card'); if (card) card.remove(); } })
61 .catch(function () {});
62 }
63 });
64})();
65</script>
Note: See TracBrowser for help on using the repository browser.