Add videos: library, block and section background

Videos join the media library as their own table (schema v28), with a
video block and a looping background on sections. video.js starts loops
only while visible, offers a pause button and leaves the still image for
reduced motion and data saving.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWYm8kjAWmZaB2QB2xuBhU
This commit is contained in:
kantarciandClaude Opus 5.5 committed 2026-11-05 05:23:17 +00:00
1 parent 592e3d5342
commit 76613b9f8e
25 files changed
+674 -20

No files matched your search

+48 -4
View File
@@ -3,7 +3,7 @@
{% block title %}Mediathek{% endblock %}
{% block scripts %}<script type="module" src="/assets/media-drop.js?v={{ kant_version() }}"></script><script type="module" src="/assets/live-search.js?v={{ kant_version() }}"></script>{% endblock %}
{% block heading %}Mediathek{% endblock %}
{% block lead %}Alle Bilder und PDFs, neueste zuerst. Beim Hochladen wird jedes Bild in WebP umgewandelt und in mehreren Breiten abgelegt; die Seite liefert dann die passende aus. PDFs bleiben, wie sie sind, und lassen sich in jedem Linkziel wählen.{% endblock %}
{% block lead %}Alle Bilder, PDFs und Videos, neueste zuerst. Beim Hochladen wird jedes Bild in WebP umgewandelt und in mehreren Breiten abgelegt; die Seite liefert dann die passende aus. PDFs und Videos bleiben, wie sie sind. PDFs lassen sich in jedem Linkziel wählen, Videos im Video-Baustein und als Hintergrund einer Sektion.{% endblock %}
{% block content %}
{% if is_granted('ROLE_EDITOR') %}
@@ -19,13 +19,13 @@
<div class="dropzone" data-dropzone>
<label class="dropzone-label" for="files">
<span class="dropzone-icon" aria-hidden="true">▩</span>
<strong>Bilder oder PDFs hierher ziehen</strong>
<strong>Bilder, PDFs oder Videos hierher ziehen</strong>
<span class="dropzone-hint">oder klicken, um Dateien zu wählen</span>
</label>
<input id="files" name="files[]" type="file" accept="image/jpeg,image/png,image/webp,application/pdf,.pdf" multiple class="dropzone-input">
<input id="files" name="files[]" type="file" accept="image/jpeg,image/png,image/webp,application/pdf,.pdf,video/mp4,video/webm,.mp4,.webm" multiple class="dropzone-input">
</div>
<p class="dropzone-chosen" data-chosen role="status">Noch keine Datei gewählt.</p>
<p class="field-help">JPEG, PNG oder WebP bis 8 MB je Bild, PDF bis 20 MB. Mehrere Dateien sind möglich; das Hosting kann ein kleineres Limit setzen. Hochgeladene Dateien sind über ihre Adresse öffentlich – nichts Vertrauliches hochladen.</p>
<p class="field-help">JPEG, PNG oder WebP bis 8 MB je Bild, PDF bis 20 MB, MP4 oder WebM bis 64 MB. Mehrere Dateien sind möglich; das Hosting kann ein kleineres Limit setzen. Hochgeladene Dateien sind über ihre Adresse öffentlich – nichts Vertrauliches hochladen.</p>
<button class="primary" type="submit">Hochladen</button>
</form>
{% endif %}
@@ -39,6 +39,50 @@
{% include 'admin/search-feedback.html.twig' %}
<div id="list-results" class="stack" role="region" aria-label="Suchergebnisse" tabindex="-1">{% include 'admin/media-results.html.twig' %}</div>
<section class="stack" aria-labelledby="videos-heading">
<div class="section-head">
<h2 id="videos-heading">Videos <span class="count">{{ videos|length }}</span></h2>
</div>
{% if videos is empty %}
<div class="empty">
<h3>Keine Videos in der Mediathek.</h3>
<p>{{ is_granted('ROLE_EDITOR') ? 'Lade oben ein MP4 oder WebM hoch. Danach lässt es sich im Video-Baustein oder als Hintergrund einer Sektion wählen.' : 'Ein Redakteur kann Videos hochladen.' }}</p>
</div>
{% else %}
<div class="rows">
{% for item in videos %}
<article class="row row-stack">
<div class="row-line">
<span class="row-icon row-icon-text" aria-hidden="true">{{ item.mime == 'video/webm' ? 'WEBM' : 'MP4' }}</span>
<div class="row-main">
<h3><a href="{{ item.src }}" target="_blank" rel="noopener">{{ item.title }}</a></h3>
<p class="muted">{{ (item.bytes / 1048576)|number_format(1, ',') }} MB · <code>{{ item.src }}</code>{% if item.archived %} · archiviert{% endif %}</p>
</div>
</div>
{% if is_granted('ROLE_EDITOR') %}
<details class="sheet">
<summary aria-label="Angaben zu {{ item.title }}">Angaben</summary>
<div class="sheet-body">
<form action="{{ path('admin_video_save', {id: item.id}) }}" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('media-form') }}">
<input type="hidden" name="version" value="{{ item.version }}">
<input type="hidden" name="seite" value="{{ library.page }}"><input type="hidden" name="q" value="{{ query }}">
<input type="hidden" name="archiv" value="{{ archived ? '1' : '' }}">
<label for="video-title-{{ item.id }}">Titel</label>
<input id="video-title-{{ item.id }}" name="title" maxlength="160" required value="{{ item.title }}">
<label class="check-label" for="video-archived-{{ item.id }}"><input id="video-archived-{{ item.id }}" name="archived" type="checkbox" value="1"{{ item.archived ? ' checked' : '' }}> Archiviert</label>
<p class="field-help">Archivierte Videos erscheinen nicht mehr in der Auswahl, bleiben aber erreichbar – eine Seite, die das Video zeigt, verliert es nicht.</p>
<button class="primary" type="submit">Angaben speichern</button>
</form>
</div>
</details>
{% endif %}
</article>
{% endfor %}
</div>
{% endif %}
</section>
{# Die PDFs stehen unter den Bildern und ohne Suche: es sind wenige, und ein Titel reicht, um
eines wiederzufinden. Der Schalter für das Archiv oben gilt auch hier. #}
<section class="stack" aria-labelledby="documents-heading">