source: Klonkt/src/views/pages/admin-audio.ejs@ 5300652

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

Stap 2: "Ben jij dit?" in Beheer -> Audio (shaer-mbz)

Een paneel bovenaan het audioscherm: zoek op je artiestennaam (voorgevuld met
wat er al in de site staat), kies je eigen pagina, klaar. Drie talen.

DE TOELICHTING IS HET HELE PUNT. Elke kandidaat toont MusicBrainz'
disambiguation naast soort, land en jaren -- er zijn drie bands die Nirvana
heten, en met alleen een naam kiest iemand de verkeerde. Wij kiezen NIET voor
de artiest, ook niet als er precies een treffer is: een verkeerd geraden MBID
zet jouw naam onder andermans werk.

ONTKOPPELEN KAN. Om dezelfde reden: zo'n vergissing hoort met een klik terug
te draaien te zijn, niet met een databasebewerking.

Alleen een echte MBID komt de kolom in; de route weigert een URL of een handle.
De naam bewaren we ernaast zodat het scherm kan tonen WAT er hangt zonder
ervoor te netwerken -- en zodat een verkeerde koppeling opvalt.

TWEE DINGEN UIT DE HUISREGELS. De kandidaten gaan met textContent het scherm op
en niet als HTML: dat is tekst uit een vreemd register. En het blok hangt in
run() en niet op moduleniveau -- deze modules krijgen bij elke paginawissel
opnieuw init(), en een blok dat maar een keer per sessie draait is precies wat
shaer-5s1 opleverde. Ik had hem er eerst buiten gezet.

Als iemand niet in MusicBrainz staat zegt het scherm dat hij zich daar kan
aanmelden -- en erbij dat dat alleen dáár kan, niet vanuit Klonkt. Dat is geen
bescheidenheid maar hun API: artiesten zijn er niet mee aan te maken.

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

  • Property mode set to 100644
File size: 22.0 KB
Line 
1<div class="container ax-page">
2
3 <p><a href="/admin" class="btn">&larr; <%= t('nav.admin') %></a></p>
4 <%- include('../partials/media-tabs', { active: 'audio', audioOn: true }) %>
5 <header class="ax-header">
6 <div>
7 <h1><%= t('aaud.title') %></h1>
8 <p class="ax-tagline"><%= t('aaud.tagline_pre') %> <code>[[track:&lt;id&gt;]]</code> <%= t('aaud.tagline_post') %></p>
9 </div>
10 </header>
11
12 <% if (success) { %><div class="ax-flash ax-flash-ok"><%= success %></div><% } %>
13 <% if (error) { %><div class="ax-flash ax-flash-err"><%= error %></div><% } %>
14
15 <%# ── MUSICBRAINZ ─────────────────────────────────────────────
16 Wie ben jij, buiten Klonkt? Een MBID is geen vocabulaire maar een
17 register: eraan koppelen is als je ISBN noemen. Andere muziekdiensten
18 -- Funkwhale voorop -- herkennen je eraan zonder dat wij hun model
19 hoeven over te nemen (shaer-mbz). %>
20 <section class="ax-card" id="mb-card">
21 <div class="ax-card-title"><%= t('aaud.mb_title') %></div>
22 <p class="ax-hint"><%= t('aaud.mb_hint') %></p>
23
24 <% if (site.mb_artist_id) { %>
25 <p class="mb-linked">
26 <%= t('aaud.mb_linked') %>
27 <strong><%= site.mb_artist_name || site.mb_artist_id %></strong>
28 <a href="https://musicbrainz.org/artist/<%= site.mb_artist_id %>"
29 target="_blank" rel="noopener"><%= t('aaud.mb_open') %></a>
30 </p>
31 <%# Ontkoppelen moet kunnen: een verkeerde koppeling zet jouw naam onder
32 andermans werk, en dat hoort met een klik terug te draaien zijn. %>
33 <form method="POST" action="<%= (typeof siteUrlBase !== 'undefined' ? siteUrlBase : '') %>/admin/audio/musicbrainz/unlink">
34 <input type="hidden" name="_csrf" value="<%= csrfToken %>">
35 <button type="submit" class="ax-btn ax-btn-quiet"><%= t('aaud.mb_unlink') %></button>
36 </form>
37 <% } %>
38
39 <div class="mb-zoek">
40 <input type="text" id="mb-q" value="<%= site.author || site.title || '' %>"
41 placeholder="<%= t('aaud.artist') %>" autocomplete="off">
42 <button type="button" class="ax-btn" id="mb-zoek-btn"><%= t('aaud.mb_search') %></button>
43 </div>
44 <div id="mb-uit" class="mb-uit" hidden></div>
45 </section>
46
47 <%# ── UPLOAD ──────────────────────────────────────────────── %>
48 <section class="ax-card">
49 <div class="ax-card-title"><%= t('aaud.upload') %></div>
50
51 <%# Bulk drag-drop zone. Multiple files at once, transcoded sequentially.
52 Title is auto-derived from each filename. Artist/album/cover are
53 SHARED across the batch (set once, applied to every track) — same
54 pattern as iTunes/Plex bulk-add. %>
55 <div class="ax-form">
56
57 <%# Shared metadata fields (apply to ALL files in the batch). %>
58 <div class="ax-row ax-row-2">
59 <label class="ax-field">
60 <span><%= t('aaud.artist') %> <small><%= t('aaud.applied_all') %></small></span>
61 <input type="text" id="batch-artist" placeholder="<%= t('aaud.optional') %>">
62 </label>
63 <label class="ax-field">
64 <span><%= t('aaud.album') %> <small><%= t('aaud.applied_all') %></small></span>
65 <input type="text" id="batch-album" placeholder="<%= t('aaud.optional') %>">
66 </label>
67 </div>
68 <label class="ax-field ax-file">
69 <span><%= t('aaud.cover') %> <small><%= t('aaud.cover_hint') %></small></span>
70 <span class="ax-file-control">
71 <span class="ax-btn ax-btn-secondary ax-file-btn">🖼 <%= t('aaud.choose_cover') %></span>
72 <span class="ax-file-name" data-empty><%= t('aaud.no_file') %></span>
73 <input type="file" id="batch-cover" accept="image/jpeg,image/png,image/webp,image/gif">
74 </span>
75 </label>
76
77 <%# The drop-zone itself. Click anywhere to open file picker. %>
78 <label class="ax-dropzone" id="audio-dropzone" tabindex="0">
79 <span class="ax-dropzone-icon">🎵</span>
80 <strong class="ax-dropzone-title"><%= t('aaud.drag_here') %></strong>
81 <span class="ax-dropzone-sub"><%= t('aaud.or_click') %><br><small>mp3, m4a, ogg, opus, flac · max <%= maxBytesMb %> MB · wav max <%= maxWavMb %> MB</small></span>
82 <input type="file" id="audio-files" accept="audio/*" multiple>
83 </label>
84
85 <%# Per-file progress list. Populated by JS as files are queued. %>
86 <ul class="ax-upload-queue" id="upload-queue" hidden></ul>
87
88 <div class="ax-form-actions" id="upload-actions" hidden>
89 <button type="button" class="ax-btn ax-btn-primary" id="start-upload-btn">⬆ <%= t('aaud.start_upload') %></button>
90 <button type="button" class="ax-btn" id="clear-queue-btn"><%= t('aaud.clear_list') %></button>
91 </div>
92 </div>
93 </section>
94
95 <%# ── TRACK LIST ─────────────────────────────────────────── %>
96 <section class="ax-card">
97 <div class="ax-card-title" style="display:flex;align-items:center;gap:.5rem">
98 <%= t('aaud.tracks') %> <span class="ax-count"><%= tracks.length %></span>
99 <button type="button" class="ax-btn ax-btn-secondary" id="add-link-track-btn"
100 style="margin-left:auto;font-size:.85rem" title="<%= t('aaud.add_link_track_title') %>">
101 + <%= t('aaud.add_link_track') %>
102 </button>
103 </div>
104
105 <% if (!tracks.length) { %>
106 <div class="ax-empty">
107 <div class="ax-empty-icon">♫</div>
108 <p><%= t('aaud.no_tracks') %></p>
109 </div>
110 <% } else { %>
111 <%
112 // Pre-fetch loop labels — `t` is shadowed by the loop var below.
113 var _aaudUntitled = t('aaud.untitled');
114 var _aaudPlay = t('aaud.play');
115 var _aaudEdit = t('aaud.edit');
116 var _aaudDlOn = t('aaud.dl_on');
117 var _aaudDlOff = t('aaud.dl_off');
118 var _aaudDelete = t('aaud.delete');
119 var _aaudCopy = t('aaud.copy_click');
120 var _aaudDelConfirm = t('aaud.delete_confirm');
121 %>
122 <ul class="ax-list">
123 <% tracks.forEach(function(t) { %>
124 <li class="ax-track" data-track-id="<%= t.id %>">
125 <% if (t.cover_url) { %>
126 <img class="ax-track-cover" src="<%= t.cover_url %>" alt="" data-cover-thumb>
127 <% } else { %>
128 <span class="ax-track-cover ax-track-cover-empty" data-cover-thumb>♫</span>
129 <% } %>
130
131 <div class="ax-track-meta">
132 <div class="ax-track-title" data-cell="title"><%= t.title || _aaudUntitled %></div>
133 <div class="ax-track-sub">
134 <span data-cell="artist"><%= t.artist || '—' %></span>
135 <% if (t.album) { %><span class="ax-track-sep">·</span><span data-cell="album"><%= t.album %></span><% } else { %><span data-cell="album" hidden></span><% } %>
136 <span class="ax-track-sep">·</span><span><%= t.size ? Math.round(t.size/1024) + ' KB' : '—' %></span>
137 </div>
138 <code class="ax-embed" data-copy="[[track:<%= t.id %>]]" title="<%= _aaudCopy %>">[[track:<%= t.id %>]]</code>
139 </div>
140
141 <div class="ax-track-actions">
142 <% if (t.stream_url) { %>
143 <button type="button" class="ax-icon-btn ax-track-play"
144 data-stream-url="<%= t.stream_url %>"
145 data-track-id="<%= t.id %>"
146 aria-label="<%= _aaudPlay %>" title="<%= _aaudPlay %>">
147 <span class="ax-track-play-icon" aria-hidden="true">▶</span>
148 </button>
149 <% } %>
150 <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label="<%= _aaudEdit %>" title="<%= _aaudEdit %>">✎</button>
151 <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
152 <button type="button" class="ax-icon-btn" data-track-dl data-id="<%= t.id %>" data-on="<%= t.downloadable ? '1' : '0' %>"
153 aria-label="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>" title="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>"
154 style="<%= t.downloadable ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">⬇</button>
155 <% } %>
156 <form action="/admin/audio/<%= t.id %>/delete" method="post" data-confirm="<%= _aaudDelConfirm %>" class="ax-track-delete">
157 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _aaudDelete %>" title="<%= _aaudDelete %>">🗑</button>
158 </form>
159 </div>
160 </li>
161 <% }); %>
162 </ul>
163 <% } %>
164 </section>
165
166 <% if ((typeof premiumUnlocked === 'undefined' || premiumUnlocked) && typeof embedUrl !== 'undefined') { %>
167 <section class="ax-section" style="margin-top:1.5rem">
168 <h2 style="margin:0 0 .4rem">🔊 <%= t('aaud.embed_player') %></h2>
169 <p style="opacity:.75;font-size:.9rem;margin:0 0 .6rem"><%= t('aaud.embed_hint') %></p>
170 <textarea readonly rows="3" data-selectall style="width:100%;font-family:monospace;font-size:12px;padding:10px;border-radius:8px;border:1px solid var(--rule);background:var(--paper-2);color:inherit;">&lt;iframe src="<%= embedUrl %>" width="100%" height="420" frameborder="0" loading="lazy" style="border-radius:12px"&gt;&lt;/iframe&gt;</textarea>
171 <p style="margin:.6rem 0 0"><a class="btn" href="<%= embedUrl %>" target="_blank">▶ <%= t('aaud.preview_player') %></a></p>
172 </section>
173 <% } %>
174
175</div>
176
177<style>
178/* ─── Page wrapper ──────────────────────────────────────────────── */
179.ax-page {
180 max-width: 880px;
181 margin: 1.5rem auto 4rem;
182 padding: 0 1rem;
183}
184.ax-header {
185 display: flex; align-items: flex-start; gap: 0.75rem;
186 margin-bottom: 1.5rem;
187}
188.ax-header h1 {
189 font-family: var(--font-display, serif);
190 font-size: 1.75rem;
191 margin: 0 0 0.25rem;
192}
193.ax-tagline {
194 color: var(--ink-muted, var(--ink-soft));
195 margin: 0;
196 font-size: 0.9rem;
197 line-height: 1.5;
198}
199.ax-tagline code {
200 background: var(--paper-2);
201 padding: 0.1em 0.4em;
202 border-radius: 4px;
203 font-size: 0.9em;
204}
205.ax-back {
206 display: inline-flex; align-items: center; justify-content: center;
207 width: 40px; height: 40px;
208 border: 1px solid var(--rule);
209 border-radius: 8px;
210 background: var(--paper);
211 color: var(--ink);
212 text-decoration: none;
213 font-size: 1.1rem;
214 flex-shrink: 0;
215 transition: border-color 120ms;
216}
217.ax-back:hover { border-color: var(--accent); }
218
219/* ─── Flash messages ───────────────────────────────────────────── */
220.ax-flash {
221 padding: 0.75rem 1rem;
222 border-radius: 8px;
223 margin-bottom: 1rem;
224 font-size: 0.9rem;
225}
226.ax-flash-ok { background: rgba(40,160,90,.15); color: #2a9d5e; border: 1px solid rgba(40,160,90,.3); }
227.ax-flash-err { background: rgba(200,60,60,.15); color: #c33; border: 1px solid rgba(200,60,60,.3); }
228
229/* ─── Card sections ────────────────────────────────────────────── */
230.ax-card {
231 background: var(--paper);
232 border: 1px solid var(--rule);
233 border-radius: 12px;
234 padding: 1.25rem;
235 margin-bottom: 1rem;
236}
237.ax-card-title {
238 font-family: var(--font-display, serif);
239 font-size: 1.15rem;
240 font-weight: 600;
241 margin-bottom: 1rem;
242 display: flex; align-items: baseline; gap: 0.5rem;
243}
244.ax-count {
245 font-family: var(--font-ui, system-ui);
246 font-size: 0.8rem;
247 color: var(--ink-muted, var(--ink-soft));
248 background: var(--paper-2);
249 padding: 0.1em 0.55em;
250 border-radius: 999px;
251 font-weight: 500;
252}
253
254/* ─── Form fields ──────────────────────────────────────────────── */
255.ax-form {
256 display: flex; flex-direction: column;
257 gap: 0.85rem;
258}
259.ax-row {
260 display: grid; gap: 0.85rem;
261}
262.ax-row-2 { grid-template-columns: 1fr 1fr; }
263@media (max-width: 600px) {
264 .ax-row-2 { grid-template-columns: 1fr; }
265}
266.ax-field {
267 display: flex; flex-direction: column;
268 gap: 0.35rem;
269 min-width: 0;
270}
271.ax-field > span {
272 font-size: 0.8rem;
273 font-weight: 600;
274 color: var(--ink-soft);
275 display: flex; align-items: baseline; gap: 0.4rem;
276 flex-wrap: wrap;
277}
278.ax-field > span small {
279 font-weight: 400; color: var(--ink-muted);
280 font-size: 0.95em;
281}
282.ax-field input[type="text"],
283.ax-field input[type="url"],
284.ax-field input[type="email"],
285.ax-field select,
286.ax-field textarea {
287 width: 100%;
288 box-sizing: border-box;
289 padding: 0.6rem 0.75rem;
290 border: 1px solid var(--rule);
291 border-radius: 6px;
292 background: var(--paper-2);
293 color: var(--ink);
294 font-family: var(--font-ui, system-ui), sans-serif;
295 font-size: 0.95rem;
296 -webkit-appearance: none; appearance: none;
297 transition: border-color 120ms;
298}
299.ax-field input:focus,
300.ax-field select:focus,
301.ax-field textarea:focus {
302 outline: 2px solid var(--accent);
303 outline-offset: -1px;
304 border-color: var(--accent);
305}
306
307/* ─── File inputs (custom-styled wrapper) ─────────────────────── */
308.ax-file-control {
309 display: flex; align-items: center; gap: 0.5rem;
310 flex-wrap: wrap;
311 position: relative;
312 min-height: 40px;
313}
314.ax-file-control input[type="file"] {
315 position: absolute; inset: 0;
316 opacity: 0; cursor: pointer;
317 z-index: 2;
318}
319.ax-file-btn {
320 pointer-events: none; /* let clicks pass through to the file input */
321}
322.ax-file-name {
323 font-size: 0.85rem;
324 color: var(--ink);
325 word-break: break-all;
326 flex: 1;
327 min-width: 0;
328}
329.ax-file-name[data-empty] { color: var(--ink-muted, var(--ink-soft)); }
330
331.ax-form-actions {
332 display: flex; gap: 0.5rem;
333 margin-top: 0.25rem;
334}
335.ax-form-actions[hidden] { display: none; }
336
337/* ─── Buttons ──────────────────────────────────────────────────── */
338.ax-btn {
339 display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
340 padding: 0.55rem 1rem;
341 border: 1px solid var(--rule);
342 background: var(--paper-2);
343 color: var(--ink);
344 font-family: var(--font-ui, system-ui), sans-serif;
345 font-size: 0.9rem; font-weight: 500;
346 text-decoration: none;
347 border-radius: 6px;
348 cursor: pointer;
349 min-height: 40px;
350 transition: background 120ms, border-color 120ms;
351 -webkit-tap-highlight-color: transparent;
352}
353.ax-btn:hover { border-color: var(--accent); }
354.ax-btn-secondary { background: var(--paper-2); }
355.ax-btn-primary {
356 background: var(--accent); color: white;
357 border-color: var(--accent);
358}
359.ax-btn-primary:hover { opacity: 0.92; border-color: var(--accent); }
360
361/* Tiny icon-only button for row actions */
362.ax-icon-btn {
363 display: inline-flex; align-items: center; justify-content: center;
364 width: 40px; height: 40px;
365 border: 1px solid var(--rule);
366 background: var(--paper-2);
367 color: var(--ink);
368 border-radius: 8px;
369 cursor: pointer;
370 font-size: 1rem;
371 transition: background 120ms, border-color 120ms, color 120ms;
372}
373.ax-icon-btn:hover { border-color: var(--accent); }
374.ax-icon-btn-danger:hover { color: #dc2626; border-color: #dc2626; }
375
376.ax-track-play.is-playing {
377 border-color: var(--accent);
378 color: var(--accent);
379 background: color-mix(in srgb, var(--accent) 12%, var(--paper-2));
380}
381.ax-track-play-icon { font-size: 0.85rem; line-height: 1; }
382
383/* ─── Empty state ──────────────────────────────────────────────── */
384.ax-empty {
385 text-align: center;
386 padding: 2rem 1rem;
387 color: var(--ink-muted, var(--ink-soft));
388}
389.ax-empty-icon {
390 font-size: 2.5rem;
391 font-family: var(--font-display, serif);
392 color: var(--ink-soft);
393 opacity: 0.4;
394 margin-bottom: 0.5rem;
395}
396.ax-empty p { margin: 0; }
397
398/* ─── Track list (card per row) ────────────────────────────────── */
399.ax-list {
400 list-style: none; padding: 0; margin: 0;
401 display: flex; flex-direction: column;
402 gap: 0.6rem;
403}
404.ax-track {
405 display: grid;
406 grid-template-columns: 56px 1fr auto;
407 gap: 0.85rem;
408 align-items: center;
409 padding: 0.75rem;
410 background: var(--paper-2);
411 border: 1px solid var(--rule);
412 border-radius: 10px;
413 transition: border-color 120ms;
414}
415.ax-track:hover { border-color: var(--accent); }
416.ax-track-cover {
417 width: 56px; height: 56px;
418 border-radius: 6px;
419 object-fit: cover;
420 background: var(--paper);
421 display: flex; align-items: center; justify-content: center;
422 font-family: var(--font-display, serif);
423 font-size: 1.5rem;
424 color: var(--accent);
425 flex-shrink: 0;
426}
427.ax-track-cover-empty { /* span variant */ }
428.ax-track-meta {
429 min-width: 0; /* allow text-overflow on long titles */
430 display: flex; flex-direction: column; gap: 0.2rem;
431}
432.ax-track-title {
433 font-weight: 600;
434 font-size: 0.95rem;
435 color: var(--ink);
436 overflow: hidden;
437 text-overflow: ellipsis;
438 white-space: nowrap;
439}
440.ax-track-sub {
441 font-size: 0.8rem;
442 color: var(--ink-muted, var(--ink-soft));
443 display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
444}
445.ax-track-sep { opacity: 0.5; }
446.ax-embed {
447 display: inline-block;
448 background: var(--paper);
449 padding: 0.15rem 0.5rem;
450 border-radius: 4px;
451 font-family: var(--font-mono, ui-monospace, monospace);
452 font-size: 0.75rem;
453 color: var(--ink-soft);
454 cursor: pointer;
455 user-select: all;
456 margin-top: 0.15rem;
457 word-break: break-all;
458 align-self: flex-start;
459 transition: background 120ms;
460}
461.ax-embed:hover { background: var(--rule); }
462.ax-embed.is-copied { background: rgba(40,160,90,0.2); color: #2a9d5e; }
463
464.ax-track-actions {
465 display: flex; gap: 0.4rem;
466 flex-shrink: 0;
467}
468.ax-track-delete { display: inline; }
469
470/* ─── Mobile tweaks ───────────────────────────────────────────── */
471@media (max-width: 480px) {
472 .ax-track {
473 grid-template-columns: 48px 1fr;
474 grid-template-areas: "cover meta" "actions actions";
475 gap: 0.6rem;
476 }
477 .ax-track-cover { grid-area: cover; width: 48px; height: 48px; }
478 .ax-track-meta { grid-area: meta; }
479 .ax-track-actions {
480 grid-area: actions;
481 justify-content: flex-end;
482 border-top: 1px solid var(--rule);
483 padding-top: 0.5rem;
484 margin-top: 0.25rem;
485 }
486}
487
488/* ─── Bulk drop-zone ──────────────────────────────────────────── */
489.ax-dropzone {
490 display: flex; flex-direction: column; align-items: center; justify-content: center;
491 gap: 0.4rem;
492 padding: 2rem 1rem;
493 border: 2px dashed var(--rule);
494 border-radius: 12px;
495 background: var(--paper-2);
496 cursor: pointer;
497 text-align: center;
498 transition: border-color 150ms, background 150ms;
499 position: relative;
500 min-height: 160px;
501}
502.ax-dropzone:hover,
503.ax-dropzone:focus-within {
504 border-color: var(--accent);
505}
506.ax-dropzone.is-dragover {
507 border-color: var(--accent);
508 background: color-mix(in srgb, var(--accent) 8%, var(--paper-2));
509}
510.ax-dropzone input[type="file"] {
511 /* Cover the whole dropzone but invisible — clicks bubble through to it. */
512 position: absolute; inset: 0;
513 opacity: 0;
514 cursor: pointer;
515}
516.ax-dropzone-icon {
517 font-size: 2rem;
518 opacity: 0.5;
519 line-height: 1;
520}
521.ax-dropzone-title {
522 font-size: 1rem;
523 font-weight: 600;
524 color: var(--ink);
525}
526.ax-dropzone-sub {
527 font-size: 0.85rem;
528 color: var(--ink-muted, var(--ink-soft));
529 line-height: 1.4;
530}
531.ax-dropzone-sub small {
532 font-size: 0.75rem;
533 opacity: 0.8;
534}
535
536/* ─── Upload queue ────────────────────────────────────────────── */
537.ax-upload-queue {
538 list-style: none;
539 padding: 0; margin: 0.75rem 0 0;
540 display: flex; flex-direction: column;
541 gap: 0.4rem;
542}
543.ax-queue-item {
544 display: grid;
545 grid-template-columns: 1fr auto;
546 gap: 0.75rem;
547 align-items: center;
548 padding: 0.6rem 0.85rem;
549 background: var(--paper);
550 border: 1px solid var(--rule);
551 border-radius: 8px;
552 font-size: 0.875rem;
553 transition: border-color 150ms, opacity 150ms;
554}
555.ax-queue-item--active {
556 border-color: var(--accent);
557}
558.ax-queue-item--done {
559 border-color: rgba(40,160,90,0.4);
560 opacity: 0.85;
561}
562.ax-queue-item--error {
563 border-color: rgba(220,60,60,0.5);
564}
565.ax-queue-name {
566 font-weight: 500;
567 color: var(--ink);
568 overflow: hidden;
569 text-overflow: ellipsis;
570 white-space: nowrap;
571 min-width: 0;
572}
573.ax-queue-size {
574 font-size: 0.75rem;
575 color: var(--ink-muted, var(--ink-soft));
576 margin-left: 0.5rem;
577}
578.ax-queue-status {
579 font-size: 0.75rem;
580 font-weight: 500;
581 padding: 0.15em 0.6em;
582 border-radius: 999px;
583 background: var(--paper-2);
584 color: var(--ink-soft);
585 white-space: nowrap;
586 flex-shrink: 0;
587}
588.ax-queue-status--queued { color: var(--ink-muted, var(--ink-soft)); }
589.ax-queue-status--uploading,
590.ax-queue-status--transcoding {
591 color: var(--accent);
592 background: color-mix(in srgb, var(--accent) 10%, var(--paper-2));
593}
594.ax-queue-status--done {
595 color: #2a9d5e;
596 background: rgba(40,160,90,0.12);
597}
598.ax-queue-status--error {
599 color: #dc2626;
600 background: rgba(220,60,60,0.12);
601}
602</style>
603
604<%# Track editor modal (exposes window.openTrackEditor) %>
605<%- include('../partials/track-editor', { csrfToken: (typeof csrfToken !== 'undefined' ? csrfToken : '') }) %>
606
607<%# Het script staat in assets/js/mod/admin-audio.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
608<%- include('../partials/page-data', { pageData: { change_failed: t('aaud.change_failed'), copied: t('aaud.copied'), create_failed: t('aaud.create_failed'), dl_off: t('aaud.dl_off'), dl_on: t('aaud.dl_on'), editor_not_loaded: t('aaud.editor_not_loaded'), err_unexpected: t('aaud.err_unexpected'), failed: t('aaud.failed'), new_track: t('aaud.new_track'), no_file: t('aaud.no_file'), pause: t('aaud.pause'), play: t('aaud.play'), st_done: t('aaud.st_done'), st_error: t('aaud.st_error'), st_queued: t('aaud.st_queued'), st_transcoding: t('aaud.st_transcoding'), st_uploading: t('aaud.st_uploading'), untitled: t('aaud.untitled') } }) %>
Note: See TracBrowser for help on using the repository browser.