Files
KantSites/templates/admin/media.html.twig
T
kantarciandClaude Opus 5.5 76613b9f8e 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
2026-11-05 05:23:17 +00:00

133 lines
9.2 KiB
Twig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'admin/shell.html.twig' %}
{% set nav = 'media' %}
{% 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, 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') %}
<form action="{{ path('admin_media_upload') }}" method="post" enctype="multipart/form-data" class="card card-body">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('media-form') }}">
<input type="hidden" name="seite" value="{{ library.page }}"><input type="hidden" name="q" value="{{ query }}">
<input type="hidden" name="archiv" value="{{ archived ? '1' : '' }}">
{# A drop area rather than the browser's file widget. The input stays a real file input and
stays focusable, so the form works by keyboard and without JavaScript; the script only adds
dragging and the list of chosen files. No "required": the input is visually hidden, and a
browser cannot show its validation bubble on something invisible. The server refuses an
empty upload with a readable message instead. #}
<div class="dropzone" data-dropzone>
<label class="dropzone-label" for="files">
<span class="dropzone-icon" aria-hidden="true">▩</span>
<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,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, 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 %}
<form method="get" action="{{ path('admin_media') }}" class="list-toolbar" role="search" data-live-search>
<div class="list-search"><label for="media-list-search">Bilder durchsuchen</label><input id="media-list-search" type="search" name="q" maxlength="160" value="{{ query }}" placeholder="Titel oder Alternativtext …"></div>
<div><label for="media-list-archive">Status</label><select id="media-list-archive" name="archiv" data-search-default="0"><option value="0"{{ not archived ? ' selected' : '' }}>Aktive Bilder</option><option value="1"{{ archived ? ' selected' : '' }}>Mit archivierten Bildern</option></select></div>
<button class="secondary search-submit" type="submit">Anwenden</button>
<a class="text-button" data-search-reset href="{{ path('admin_media') }}"{{ not query and not archived ? ' hidden' : '' }}>Zurücksetzen</a>
</form>
{% 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">
<div class="section-head">
<h2 id="documents-heading">Dokumente <span class="count">{{ documents|length }}</span></h2>
</div>
{% if documents is empty %}
<div class="empty">
<h3>Keine PDFs in der Mediathek.</h3>
<p>{{ is_granted('ROLE_EDITOR') ? 'Lade oben ein PDF hoch. Danach lässt es sich in jedem Linkziel wählen – etwa für einen Button „Preisliste“.' : 'Ein Redakteur kann PDFs hochladen.' }}</p>
</div>
{% else %}
<div class="rows">
{% for item in documents %}
<article class="row row-stack">
<div class="row-line">
<span class="row-icon row-icon-text" aria-hidden="true">PDF</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_document_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="document-title-{{ item.id }}">Titel</label>
<input id="document-title-{{ item.id }}" name="title" maxlength="160" required value="{{ item.title }}">
<p class="field-help">Wird zum Dateinamen, wenn jemand das PDF speichert, und steht über dem Betrachter.</p>
<label class="check-label" for="document-archived-{{ item.id }}"><input id="document-archived-{{ item.id }}" name="archived" type="checkbox" value="1"{{ item.archived ? ' checked' : '' }}> Archiviert</label>
<p class="field-help">Archivierte PDFs erscheinen nicht mehr in der Auswahl, bleiben aber erreichbar – ein Link darauf geht nicht ins Leere.</p>
<button class="primary" type="submit">Angaben speichern</button>
</form>
</div>
</details>
{% endif %}
</article>
{% endfor %}
</div>
{% endif %}
</section>
{% endblock %}