Changeset 749605a in Klonkt for src/views


Ignore:
Timestamp:
08/16/2026 03:06:18 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
a5caa73
Parents:
a5515b4 (diff), 9026de5 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Bibliotheek en album-federatie terug op main (shaer-0nh, shaer-756s)

Achttien commits, in drie brokken.

DE BIBLIOTHEEK. Een Library-skelet met een eigen volgerssoort: wie de
bibliotheek volgt krijgt de muziek en met opzet niet de gewone posts, en
die staan daarom in een eigen tabel -- een vlag die iemand vergeet te
filteren bezorgt stilletjes de Krant aan mensen die er niet om vroegen.
Daarna wat er nodig bleek om werkelijk gelezen te worden: audience
(zonder dat gaf hun create() een KeyError en dus een 500 op onze URL),
de ongeordende collectievorm, en paginering op elke collectie die first
belooft -- die laatste vond meteen dat featured hetzelfde gat had.

HET VOCABULAIRE. fw:track in de kleine vorm die Emissary ook stuurt,
artist_credit met de site-actor als Artist (mogelijk geworden door de
MusicBrainz-koppeling), en het album als ingesloten fw:Album met
released, musicbrainzId en een eigen artist_credit. Daarvoor kwamen er
twee kolommen op playlists, alleen bij kind='album' en afgedwongen in
PlaylistService -- niet in het scherm, want de API ligt open.

HET BEHEER. De vijf mediapagina's stonden op drie verschillende
ontwerpen; nu op een gedeelde ax-romp, met de primitieven op een plek in
plaats van in acht kopieen. De playlist-modal ging alsnog door i18n: die
teksten stonden hard in het script, wat niet opviel omdat het toevallig
de brontaal was.

WAT ER NIET IN ZIT: bitrate en size top-level als fw:. Dat is bewust
(shaer-3f8a). Hun eigen outbox zet ze op de Link, net als wij, en het
wordt een gesprek in plaats van een aanpassing -- briefing in
~/bewaard/funkwhale-briefing-en.md.

Gereedschap dat meekomt: scripts/funkwhale-gat.mjs meet het gat naar hun
ingest, admin-css.mjs en admin-css-gelijk.mjs ontleden en vergelijken de
beheer-CSS, render-admin-views.mjs rendert de views gevuld en leeg. Die
laatste drie omdat de testsuite geen enkele view aanraakt.

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

Location:
src/views
Files:
3 added
6 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/admin-audio.ejs

    ra5515b4 r749605a  
     1<%# De gedeelde ax-*-primitieven. MOET boven het eigen style-blok van deze
     2    pagina staan: wat hieronder blijft staan wijkt bewust af en hoort dus
     3    later te komen, anders wint de partial ervan. %>
     4<%- include('../partials/admin-styles') %>
    15<div class="container ax-page">
    26
    3   <p><a href="/admin" class="btn">&larr; <%= t('nav.admin') %></a></p>
     7  <%- include('../partials/admin-back') %>
    48  <%- include('../partials/media-tabs', { active: 'audio', audioOn: true }) %>
    59  <header class="ax-header">
     
    150154  padding: 0 1rem;
    151155}
    152 .ax-header {
    153   display: flex; align-items: flex-start; gap: 0.75rem;
    154   margin-bottom: 1.5rem;
    155 }
    156 .ax-header h1 {
    157   font-family: var(--font-display, serif);
    158   font-size: 1.75rem;
    159   margin: 0 0 0.25rem;
    160 }
    161 .ax-tagline {
    162   color: var(--ink-muted, var(--ink-soft));
    163   margin: 0;
    164   font-size: 0.9rem;
    165   line-height: 1.5;
    166 }
    167 .ax-tagline code {
    168   background: var(--paper-2);
    169   padding: 0.1em 0.4em;
    170   border-radius: 4px;
    171   font-size: 0.9em;
    172 }
    173 .ax-back {
    174   display: inline-flex; align-items: center; justify-content: center;
    175   width: 40px; height: 40px;
    176   border: 1px solid var(--rule);
    177   border-radius: 8px;
    178   background: var(--paper);
    179   color: var(--ink);
    180   text-decoration: none;
    181   font-size: 1.1rem;
    182   flex-shrink: 0;
    183   transition: border-color 120ms;
    184 }
    185 .ax-back:hover { border-color: var(--accent); }
    186 
    187 /* ─── Flash messages ───────────────────────────────────────────── */
    188 .ax-flash {
    189   padding: 0.75rem 1rem;
    190   border-radius: 8px;
    191   margin-bottom: 1rem;
    192   font-size: 0.9rem;
    193 }
    194 .ax-flash-ok  { background: rgba(40,160,90,.15); color: #2a9d5e; border: 1px solid rgba(40,160,90,.3); }
    195 .ax-flash-err { background: rgba(200,60,60,.15); color: #c33;    border: 1px solid rgba(200,60,60,.3); }
    196 
    197 /* ─── Card sections ────────────────────────────────────────────── */
    198 .ax-card {
    199   background: var(--paper);
    200   border: 1px solid var(--rule);
    201   border-radius: 12px;
    202   padding: 1.25rem;
    203   margin-bottom: 1rem;
    204 }
    205156.ax-card-title {
    206157  font-family: var(--font-display, serif);
     
    303254.ax-form-actions[hidden] { display: none; }
    304255
    305 /* ─── Buttons ──────────────────────────────────────────────────── */
    306 .ax-btn {
    307   display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
    308   padding: 0.55rem 1rem;
    309   border: 1px solid var(--rule);
    310   background: var(--paper-2);
    311   color: var(--ink);
    312   font-family: var(--font-ui, system-ui), sans-serif;
    313   font-size: 0.9rem; font-weight: 500;
    314   text-decoration: none;
    315   border-radius: 6px;
    316   cursor: pointer;
    317   min-height: 40px;
    318   transition: background 120ms, border-color 120ms;
    319   -webkit-tap-highlight-color: transparent;
    320 }
    321 .ax-btn:hover { border-color: var(--accent); }
    322 .ax-btn-secondary { background: var(--paper-2); }
    323 .ax-btn-primary {
    324   background: var(--accent); color: white;
    325   border-color: var(--accent);
    326 }
    327 .ax-btn-primary:hover { opacity: 0.92; border-color: var(--accent); }
    328 
    329 /* Tiny icon-only button for row actions */
    330 .ax-icon-btn {
    331   display: inline-flex; align-items: center; justify-content: center;
    332   width: 40px; height: 40px;
    333   border: 1px solid var(--rule);
    334   background: var(--paper-2);
    335   color: var(--ink);
    336   border-radius: 8px;
    337   cursor: pointer;
    338   font-size: 1rem;
    339   transition: background 120ms, border-color 120ms, color 120ms;
    340 }
    341 .ax-icon-btn:hover { border-color: var(--accent); }
    342 .ax-icon-btn-danger:hover { color: #dc2626; border-color: #dc2626; }
    343 
    344256.ax-track-play.is-playing {
    345257  border-color: var(--accent);
     
    348260}
    349261.ax-track-play-icon { font-size: 0.85rem; line-height: 1; }
    350 
    351 /* ─── Empty state ──────────────────────────────────────────────── */
    352 .ax-empty {
    353   text-align: center;
    354   padding: 2rem 1rem;
    355   color: var(--ink-muted, var(--ink-soft));
    356 }
    357 .ax-empty-icon {
    358   font-size: 2.5rem;
    359   font-family: var(--font-display, serif);
    360   color: var(--ink-soft);
    361   opacity: 0.4;
    362   margin-bottom: 0.5rem;
    363 }
    364 .ax-empty p { margin: 0; }
    365 
    366 /* ─── Track list (card per row) ────────────────────────────────── */
    367 .ax-list {
    368   list-style: none; padding: 0; margin: 0;
    369   display: flex; flex-direction: column;
    370   gap: 0.6rem;
    371 }
    372 .ax-track {
    373   display: grid;
    374   grid-template-columns: 56px 1fr auto;
    375   gap: 0.85rem;
    376   align-items: center;
    377   padding: 0.75rem;
    378   background: var(--paper-2);
    379   border: 1px solid var(--rule);
    380   border-radius: 10px;
    381   transition: border-color 120ms;
    382 }
    383 .ax-track:hover { border-color: var(--accent); }
    384 .ax-track-cover {
    385   width: 56px; height: 56px;
    386   border-radius: 6px;
    387   object-fit: cover;
    388   background: var(--paper);
    389   display: flex; align-items: center; justify-content: center;
    390   font-family: var(--font-display, serif);
    391   font-size: 1.5rem;
    392   color: var(--accent);
    393   flex-shrink: 0;
    394 }
    395262.ax-track-cover-empty { /* span variant */ }
    396 .ax-track-meta {
    397   min-width: 0;  /* allow text-overflow on long titles */
    398   display: flex; flex-direction: column; gap: 0.2rem;
    399 }
    400 .ax-track-title {
    401   font-weight: 600;
    402   font-size: 0.95rem;
    403   color: var(--ink);
    404   overflow: hidden;
    405   text-overflow: ellipsis;
    406   white-space: nowrap;
    407 }
    408 .ax-track-sub {
    409   font-size: 0.8rem;
    410   color: var(--ink-muted, var(--ink-soft));
    411   display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
    412 }
    413 .ax-track-sep { opacity: 0.5; }
    414 .ax-embed {
    415   display: inline-block;
    416   background: var(--paper);
    417   padding: 0.15rem 0.5rem;
    418   border-radius: 4px;
    419   font-family: var(--font-mono, ui-monospace, monospace);
    420   font-size: 0.75rem;
    421   color: var(--ink-soft);
    422   cursor: pointer;
    423   user-select: all;
    424   margin-top: 0.15rem;
    425   word-break: break-all;
    426   align-self: flex-start;
    427   transition: background 120ms;
    428 }
    429 .ax-embed:hover { background: var(--rule); }
    430 .ax-embed.is-copied { background: rgba(40,160,90,0.2); color: #2a9d5e; }
    431 
    432 .ax-track-actions {
    433   display: flex; gap: 0.4rem;
    434   flex-shrink: 0;
    435 }
    436263.ax-track-delete { display: inline; }
    437264
    438265/* ─── Mobile tweaks ───────────────────────────────────────────── */
    439266@media (max-width: 480px) {
    440   .ax-track {
    441     grid-template-columns: 48px 1fr;
    442     grid-template-areas: "cover meta" "actions actions";
    443     gap: 0.6rem;
    444   }
    445   .ax-track-cover { grid-area: cover; width: 48px; height: 48px; }
    446   .ax-track-meta  { grid-area: meta; }
    447   .ax-track-actions {
    448     grid-area: actions;
    449     justify-content: flex-end;
    450     border-top: 1px solid var(--rule);
    451     padding-top: 0.5rem;
    452     margin-top: 0.25rem;
    453   }
    454267}
    455268
  • src/views/pages/admin-media.ejs

    ra5515b4 r749605a  
    1 <%# Beheer → Media: image library + cleanup (the Audio half links out to /admin/audio). %>
    2 <div class="admin-wrap" style="max-width:980px;margin:0 auto;padding:1.5rem 1rem;">
    3   <a href="/admin" class="btn" style="margin-bottom:1rem;display:inline-block;">← <%= t('nav.admin') %></a>
    4   <h1 style="font-family:var(--display,serif);margin:.2rem 0 1rem;"><%= t('admin.t_media') %></h1>
     1<%# Beheer -> Media: de afbeeldingenbibliotheek met opruiming.
     2    De audio-helft zit onder /admin/audio, als eigen tab.
    53
     4    Deze pagina stond volledig op inline styles en viel daarmee buiten het
     5    ax-*-systeem waar audio en playlists al op zaten. Nu op dezelfde romp:
     6    zelfde breedte, zelfde kop, tabs op dezelfde plek. %>
     7<%- include('../partials/admin-styles') %>
     8<div class="container ax-page">
     9
     10  <%- include('../partials/admin-back') %>
    611  <%- include('../partials/media-tabs', { active: 'images', audioOn: audioOn }) %>
     12  <header class="ax-header">
     13    <div>
     14      <h1><%= t('admin.t_media') %></h1>
     15      <p class="ax-tagline">
     16        <%= items.length %> <%= t('admin.media_count') %><% if (unusedCount) { %> &middot; <strong><%= unusedCount %></strong> <%= t('admin.media_unused') %><% } %>
     17      </p>
     18    </div>
     19  </header>
    720
    8   <% 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><% } %>
     21  <% if (success) { %><div class="ax-flash ax-flash-ok"><%= success %></div><% } %>
    922
    10   <div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;margin-bottom:1rem;">
    11     <p style="margin:0;opacity:.75;">
    12       <%= items.length %> <%= t('admin.media_count') %><% if (unusedCount) { %> · <strong><%= unusedCount %></strong> <%= t('admin.media_unused') %><% } %>
    13     </p>
    14     <% if (unusedCount) { %>
    15     <button type="button" id="media-cleanup" class="btn" style="background:#b4452e;color:#fff;border-color:#b4452e;">🗑 <%= t('admin.media_cleanup') %> (<%= unusedCount %>)</button>
    16     <% } %>
     23  <% if (unusedCount) { %>
     24  <div class="ax-actions-bar">
     25    <button type="button" id="media-cleanup" class="ax-btn ax-btn-danger">&#128465; <%= t('admin.media_cleanup') %> (<%= unusedCount %>)</button>
    1726  </div>
     27  <% } %>
    1828
    1929  <% if (!items.length) { %>
    20     <p style="opacity:.7;"><%= t('admin.media_empty') %></p>
     30    <div class="ax-empty">
     31      <div class="ax-empty-icon">&#128247;</div>
     32      <p><%= t('admin.media_empty') %></p>
     33    </div>
    2134  <% } else { %>
    22   <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:1rem;">
     35  <div class="ax-grid">
    2336    <% items.forEach(function(it){ %>
    24       <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));">
    25         <div style="position:relative;aspect-ratio:1;background:#0a0e1a;">
    26           <img src="<%= thumb(it.url, 320) %>" alt="" loading="lazy" decoding="async" style="width:100%;height:100%;object-fit:cover;display:block;">
    27           <% if (it.hasVideo) { %><span title="animated cover" style="position:absolute;top:6px;right:6px;background:rgba(0,0,0,.6);border-radius:6px;padding:0 6px;font-size:13px;">🎬</span><% } %>
    28           <% if (!it.usedCount) { %><span style="position:absolute;top:6px;left:6px;background:#b4452e;color:#fff;border-radius:6px;padding:1px 6px;font-size:11px;"><%= t('admin.media_unused') %></span><% } %>
     37      <div class="ax-tile media-card" data-file="<%= it.file %>">
     38        <div class="ax-tile-media" style="aspect-ratio:1;">
     39          <img src="<%= thumb(it.url, 320) %>" alt="" loading="lazy" decoding="async" style="object-fit:cover;height:100%;">
     40          <% if (it.hasVideo) { %><span class="ax-tile-badge ax-tile-badge-r" title="animated cover">&#127916;</span><% } %>
     41          <% if (!it.usedCount) { %><span class="ax-tile-badge"><%= t('admin.media_unused') %></span><% } %>
    2942        </div>
    30         <div style="padding:.5rem .6rem;font-size:13px;">
    31           <div style="opacity:.7;"><%= it.kb %> KB · <%= it.usedCount %>×</div>
    32           <div style="display:flex;gap:.4rem;margin-top:.45rem;">
    33             <button type="button" class="btn" style="padding:3px 8px;font-size:12px;" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
    34             <button type="button" class="btn" style="padding:3px 8px;font-size:12px;background:transparent;border-color:#b4452e;color:#b4452e;" data-del="<%= it.file %>">🗑</button>
     43        <div class="ax-tile-body">
     44          <div class="ax-tile-meta"><%= it.kb %> KB &middot; <%= it.usedCount %>&times;</div>
     45          <div class="ax-tile-actions">
     46            <button type="button" class="ax-btn ax-btn-sm" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
     47            <button type="button" class="ax-btn ax-btn-sm ax-btn-danger" data-del="<%= it.file %>">&#128465;</button>
    3548          </div>
    3649        </div>
  • src/views/pages/admin-playlists.ejs

    ra5515b4 r749605a  
    55const _csrf = (typeof csrfToken !== 'undefined' && csrfToken) || '';
    66%>
     7<%# De gedeelde ax-*-primitieven. MOET boven het eigen style-blok van deze
     8    pagina staan: wat hieronder blijft staan wijkt bewust af en hoort dus
     9    later te komen, anders wint de partial ervan. %>
     10<%- include('../partials/admin-styles') %>
    711<div class="container ax-page">
    812
    9   <p><a href="/admin" class="btn">&larr; <%= t('nav.admin') %></a></p>
     13  <%- include('../partials/admin-back') %>
    1014  <%- include('../partials/media-tabs', { active: 'playlists', audioOn: true }) %>
    1115  <header class="ax-header">
     
    8387
    8488.ax-page { max-width: 880px; margin: 1.5rem auto 4rem; padding: 0 1rem; }
    85 .ax-header { display: flex; align-items: flex-start; gap: 0.75rem; margin-bottom: 1.5rem; }
    86 .ax-header h1 { font-family: var(--font-display, serif); font-size: 1.75rem; margin: 0 0 0.25rem; }
    87 .ax-tagline { color: var(--ink-muted, var(--ink-soft)); margin: 0; font-size: 0.9rem; line-height: 1.5; }
    88 .ax-tagline code { background: var(--paper-2); padding: 0.1em 0.4em; border-radius: 4px; font-size: 0.9em; }
    89 .ax-back {
    90   display: inline-flex; align-items: center; justify-content: center;
    91   width: 40px; height: 40px;
    92   border: 1px solid var(--rule); border-radius: 8px;
    93   background: var(--paper); color: var(--ink); text-decoration: none;
    94   font-size: 1.1rem; flex-shrink: 0; transition: border-color 120ms;
    95 }
    96 .ax-back:hover { border-color: var(--accent); }
    97 
    98 .ax-actions-bar {
    99   display: flex;
    100   margin-bottom: 1rem;
    101 }
    102 
    103 .ax-card {
    104   background: var(--paper);
    105   border: 1px solid var(--rule);
    106   border-radius: 12px;
    107   padding: 1.25rem;
    108   margin-bottom: 1rem;
    109 }
    110 
    111 /* ─── Buttons (same primitives as admin-audio) ─────────────────── */
    112 .ax-btn {
    113   display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
    114   padding: 0.6rem 1.1rem;
    115   border: 1px solid var(--rule); background: var(--paper-2);
    116   color: var(--ink);
    117   font-family: var(--font-ui, system-ui), sans-serif;
    118   font-size: 0.95rem; font-weight: 500;
    119   text-decoration: none; border-radius: 8px; cursor: pointer;
    120   min-height: 44px;
    121   transition: background 120ms, border-color 120ms;
    122   -webkit-tap-highlight-color: transparent;
    123 }
    124 .ax-btn:hover { border-color: var(--accent); }
    125 .ax-btn-primary { background: var(--accent); color: white; border-color: var(--accent); }
    126 .ax-btn-primary:hover { opacity: 0.92; }
    127 
    128 .ax-icon-btn {
    129   display: inline-flex; align-items: center; justify-content: center;
    130   width: 40px; height: 40px;
    131   border: 1px solid var(--rule); border-radius: 8px;
    132   background: var(--paper-2); color: var(--ink);
    133   cursor: pointer; font-size: 1rem;
    134   transition: background 120ms, border-color 120ms, color 120ms;
    135 }
    136 .ax-icon-btn:hover { border-color: var(--accent); }
    137 .ax-icon-btn-danger:hover { color: #dc2626; border-color: #dc2626; }
    138 
    139 /* ─── Empty state ──────────────────────────────────────────────── */
    140 .ax-empty { text-align: center; padding: 2rem 1rem; color: var(--ink-muted, var(--ink-soft)); }
    141 .ax-empty-icon { font-size: 2.5rem; margin-bottom: 0.5rem; opacity: 0.5; }
    142 .ax-empty p { margin: 0; }
    143 .ax-empty-sub { font-size: 0.85rem; margin-top: 0.5rem !important; }
    144 
    145 /* ─── List items (same as audio tracks) ───────────────────────── */
    146 .ax-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0.6rem; }
    147 .ax-track {
    148   display: grid;
    149   grid-template-columns: 64px 1fr auto;
    150   gap: 0.85rem; align-items: center;
    151   padding: 0.85rem;
    152   background: var(--paper);
    153   border: 1px solid var(--rule);
    154   border-radius: 12px;
    155   transition: border-color 120ms;
    156 }
    157 .ax-track:hover { border-color: var(--accent); }
    158 .ax-track-cover {
    159   width: 64px; height: 64px; border-radius: 8px;
    160   background: var(--paper-2);
    161   display: flex; align-items: center; justify-content: center;
    162   font-family: var(--font-display, serif);
    163   font-size: 1.5rem; color: var(--accent);
    164   flex-shrink: 0;
    165 }
    166 .ax-track-meta { min-width: 0; display: flex; flex-direction: column; gap: 0.25rem; }
    167 .ax-track-title {
    168   font-family: var(--font-display, serif);
    169   font-weight: 600; font-size: 1.05rem; color: var(--ink);
    170   display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap;
    171 }
    172 .ax-pill {
    173   font-family: var(--font-ui);
    174   font-size: 0.7rem; font-weight: 500;
    175   color: var(--ink-soft);
    176   background: var(--paper-2);
    177   padding: 0.15em 0.55em; border-radius: 999px;
    178   letter-spacing: 0;
    179 }
    180 .ax-track-sub {
    181   font-size: 0.85rem;
    182   color: var(--ink-muted, var(--ink-soft));
    183   display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
    184 }
    185 .ax-track-sep { opacity: 0.5; }
    186 .ax-embed {
    187   display: inline-block;
    188   background: var(--paper-2);
    189   padding: 0.15rem 0.5rem;
    190   border-radius: 4px;
    191   font-family: var(--font-mono, ui-monospace, monospace);
    192   font-size: 0.75rem;
    193   color: var(--ink-soft);
    194   cursor: pointer; user-select: all;
    195   margin-top: 0.15rem;
    196   word-break: break-all;
    197   align-self: flex-start;
    198   transition: background 120ms;
    199 }
    200 .ax-embed:hover { background: var(--rule); }
    201 .ax-embed.is-copied { background: rgba(40,160,90,0.2); color: #2a9d5e; }
    202 
    203 .ax-track-actions { display: flex; gap: 0.4rem; flex-shrink: 0; }
    20489
    20590/* ─── Mobile ──────────────────────────────────────────────────── */
    20691@media (max-width: 480px) {
    207   .ax-track {
    208     grid-template-columns: 56px 1fr;
    209     grid-template-areas: "cover meta" "actions actions";
    210     gap: 0.6rem;
    211   }
    212   .ax-track-cover { grid-area: cover; width: 56px; height: 56px; }
    213   .ax-track-meta  { grid-area: meta; }
    214   .ax-track-actions {
    215     grid-area: actions;
    216     justify-content: flex-end;
    217     border-top: 1px solid var(--rule);
    218     padding-top: 0.5rem;
    219     margin-top: 0.25rem;
    220   }
    22192}
    22293</style>
  • src/views/pages/admin-videos.ejs

    ra5515b4 r749605a  
    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>
     1<%# Beheer -> Media -> Video's (Robins opdracht, 30-7): de video's die via C2S
     2    binnenkwamen (Shaers composer, de reddingsboei), met gebruik en opruiming.
    93
     4    De voorvertoning is de video zelf met preload="metadata", zodat de browser
     5    het eerste beeld als miniatuur toont tot echte poster-generatie landt
     6    (bead: ffmpeg ontbreekt).
     7
     8    Zelfde romp als de andere mediapagina's; stond hiervoor op inline styles. %>
     9<%- include('../partials/admin-styles') %>
     10<div class="container ax-page">
     11
     12  <%- include('../partials/admin-back') %>
    1013  <%- include('../partials/media-tabs', { active: 'videos', audioOn: audioOn }) %>
     14  <header class="ax-header">
     15    <div>
     16      <h1><%= t('admin.t_media') %></h1>
     17      <p class="ax-tagline"><%= items.length %> <%= t('admin.videos_count') %></p>
     18    </div>
     19  </header>
    1120
    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>
     21  <% if (success) { %><div class="ax-flash ax-flash-ok"><%= success %></div><% } %>
    1522
    1623  <% if (!items.length) { %>
    17     <p style="opacity:.7;"><%= t('admin.videos_empty') %></p>
     24    <div class="ax-empty">
     25      <div class="ax-empty-icon">&#127916;</div>
     26      <p><%= t('admin.videos_empty') %></p>
     27    </div>
    1828  <% } else { %>
    19   <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:1rem;">
     29  <div class="ax-grid" style="--ax-tile:220px;">
    2030    <% 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><% } %>
     31      <div class="ax-tile media-card" data-file="<%= it.file %>">
     32        <div class="ax-tile-media" style="aspect-ratio:16/10;background:#000;">
     33          <video src="<%= it.url %>" preload="metadata" controls playsinline style="height:100%;object-fit:contain;"></video>
     34          <% if (!it.usedCount) { %><span class="ax-tile-badge"><%= t('admin.media_unused') %></span><% } %>
    2535        </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>
     36        <div class="ax-tile-body">
     37          <div class="ax-tile-meta" style="word-break:break-all;"><%= it.file %></div>
     38          <div class="ax-tile-meta"><%= it.kb %> KB &middot; <%= it.usedCount %>&times;</div>
     39          <div class="ax-tile-actions">
     40            <button type="button" class="ax-btn ax-btn-sm" data-copy="<%= it.url %>"><%= t('admin.media_copy') %></button>
    3141            <% 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>
     42            <button type="button" class="ax-btn ax-btn-sm ax-btn-danger" data-del="<%= it.file %>">&#128465;</button>
    3343            <% } %>
    3444          </div>
  • src/views/partials/media-tabs.ejs

    ra5515b4 r749605a  
    1 <%# Shared Media tab bar (Afbeeldingen / Audio / Playlists). Params: active = 'images'|'videos'|'audio'|'playlists'; audioOn (bool, default true). %>
     1<%# Gedeelde tabbalk voor Media (Afbeeldingen / Video's / Audio / Playlists /
     2    Luisteraars).
     3
     4    Params: active = 'images'|'videos'|'audio'|'playlists'|'listeners';
     5            audioOn (bool, standaard true).
     6
     7    De opmaak stond hier volledig inline en zit nu in partials/admin-styles.ejs
     8    bij de rest van de ax-*-primitieven. Een gedeeld element dat zijn eigen
     9    opmaak meedroeg kon niet meebewegen met de pagina's eromheen -- en dat was
     10    zichtbaar: de balk zag er hetzelfde uit op vijf pagina's die verder van
     11    twee verschillende ontwerpen waren.
     12
     13    De actieve tab is `aria-current="page"` en niet een klasse: dat is wat een
     14    schermlezer nodig heeft, en de opmaak kan er net zo goed op haken. %>
    215<% var _a = (typeof audioOn === 'undefined') ? true : audioOn; %>
    3 <nav class="media-tabs" aria-label="Media" style="display:flex;gap:.4rem;margin:0 0 1.25rem;border-bottom:1px solid var(--rule,rgba(128,128,128,.3));flex-wrap:wrap;">
    4   <a href="/admin/media" class="btn" style="border-radius:8px 8px 0 0;<%= active === 'images' ? 'border-bottom:2px solid var(--accent,#e8b04b);font-weight:600;' : 'opacity:.8;' %>"><%= t('admin.media_images') %></a>
    5   <a href="/admin/media/videos" class="btn" style="border-radius:8px 8px 0 0;<%= active === 'videos' ? 'border-bottom:2px solid var(--accent,#e8b04b);font-weight:600;' : 'opacity:.8;' %>"><%= t('admin.media_videos') %></a>
     16<nav class="ax-tabs" aria-label="Media">
     17  <a href="/admin/media" class="ax-tab"<%- active === 'images' ? ' aria-current="page"' : '' %>><%= t('admin.media_images') %></a>
     18  <a href="/admin/media/videos" class="ax-tab"<%- active === 'videos' ? ' aria-current="page"' : '' %>><%= t('admin.media_videos') %></a>
    619  <% if (_a) { %>
    7   <a href="/admin/audio" class="btn" style="border-radius:8px 8px 0 0;<%= active === 'audio' ? 'border-bottom:2px solid var(--accent,#e8b04b);font-weight:600;' : 'opacity:.8;' %>"><%= t('admin.b_audio') %></a>
    8   <a href="/admin/playlists" class="btn" style="border-radius:8px 8px 0 0;<%= active === 'playlists' ? 'border-bottom:2px solid var(--accent,#e8b04b);font-weight:600;' : 'opacity:.8;' %>"><%= t('admin.b_playlists') %></a>
     20  <a href="/admin/audio" class="ax-tab"<%- active === 'audio' ? ' aria-current="page"' : '' %>><%= t('admin.b_audio') %></a>
     21  <a href="/admin/playlists" class="ax-tab"<%- active === 'playlists' ? ' aria-current="page"' : '' %>><%= t('admin.b_playlists') %></a>
     22  <a href="/admin/listeners" class="ax-tab"<%- active === 'listeners' ? ' aria-current="page"' : '' %>><%= t('admin.b_listeners') %></a>
    923  <% } %>
    1024</nav>
  • src/views/partials/playlist-editor.ejs

    ra5515b4 r749605a  
    9999}
    100100.pl-field { display: flex; flex-direction: column; gap: .25rem; }
     101/* `display: flex` wint van het hidden-attribuut, dus zonder deze regel doet
     102   [hidden] op een .pl-field NIETS -- het veld blijft gewoon staan. De
     103   uitgavevelden schakelen daarop (album versus afspeellijst). */
     104.pl-field[hidden] { display: none; }
    101105.pl-field-full { grid-column: 1 / -1; }
    102106.pl-field > span {
     
    280284</style>
    281285
    282 <%# Het script staat in assets/js/mod/playlist-editor.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
     286<%# Het script staat in assets/js/mod/playlist-editor.js.
     287
     288    EIGEN GEGEVENSBLOK, niet dat van partials/page-data.ejs. pageData() pakt met
     289    querySelector EEN blok, en deze modal wordt ingesloten op pagina's die er al
     290    zelf een hebben (admin-playlists, post-edit) -- een tweede blok zou daar
     291    stilzwijgend genegeerd worden en dan stond de hele modal weer in het
     292    Nederlands. Eigen kenmerk, eigen lezer.
     293
     294    De teksten stonden hier tot 16-8 hard in het script. Dat viel niet op omdat
     295    het toevallig de brontaal was: een Engelse of Duitse beheerder kreeg een
     296    Nederlandse modal midden in een vertaalde pagina. %>
     297<script type="application/json" data-playlist-editor-i18n><%- JSON.stringify({
     298  titel: t('ple.titel'), artiest: t('ple.artiest'), jaar: t('ple.jaar'), type: t('ple.type'),
     299  k_album: t('ple.k_album'), k_playlist: t('ple.k_playlist'),
     300  uitgave: t('ple.uitgave'), mb_release: t('ple.mb_release'),
     301  cover: t('ple.cover'), cover_kies: t('ple.cover_kies'), cover_url: t('ple.cover_url'),
     302  tracks_in: t('ple.tracks_in'), sleep_hint: t('ple.sleep_hint'),
     303  beschikbaar: t('ple.beschikbaar'), zoek: t('ple.zoek'), geen_res: t('ple.geen_res'),
     304  leeg_sel: t('ple.leeg_sel'),
     305  t_edit: t('ple.t_edit'), t_new: t('ple.t_new'), dialoog: t('ple.dialoog'),
     306  sluiten: t('ple.sluiten'), annuleren: t('ple.annuleren'),
     307  opslaan: t('ple.opslaan'), aanmaken: t('ple.aanmaken'),
     308  bezig_opslaan: t('ple.bezig_opslaan'),
     309  versleep: t('ple.versleep'), verwijder: t('ple.verwijder'),
     310  geen_audio: t('ple.geen_audio'), e_geen_tracks: t('ple.e_geen_tracks'),
     311  e_tracks: t('ple.e_tracks'), e_opslaan: t('ple.e_opslaan'), e_mislukt: t('ple.e_mislukt'),
     312  e_alleen_afb: t('ple.e_alleen_afb'), bezig: t('ple.bezig'), e_upload: t('ple.e_upload'),
     313}).replace(/</g, '\\u003c') %></script>
Note: See TracChangeset for help on using the changeset viewer.