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

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

Beheer: de terugknop op alle vijf dezelfde maat

Robin zag het (16-8): de knop naar /admin verschilde van grootte. Drie
pagina's kregen bij de omzetting class="ax-btn", audio en playlists
hielden de globale class="btn". Dat scheelt zichtbaar --

.btn padding 0.55rem 1rem · radius 5 · geen minimumhoogte
.ax-btn padding 0.6rem 1.1rem · radius 8 · min-height 44px

Het wordt .ax-btn: dit zijn ax-pagina's, en 44px is de maat waar een
vinger op past.

Nu een partial, voor een regel van niets, om dezelfde reden als bij de
tabbalk: een gedeeld element dat op elke pagina los staat loopt uiteen.
Dit is de tweede keer vandaag dat dat gebeurt, en de eerste keer had ik
het kunnen zien -- ik heb de tabbalk toen wel gedeeld gemaakt en de knop
ernaast niet.

scripts/render-admin-views.mjs dekt nu alle vijf de pagina's en
controleert de terugknop op zijn KLASSENAAM. Dat verschil ziet er in de
bron uit als hetzelfde, dus een controle die alleen kijkt of er een link
staat zou het niet vinden. Nagelopen dat hij kan falen: met .btn erin
meldt hij 10 stuk, hersteld nul.

Opmaak nagemeten met admin-css-gelijk.mjs: geen regel verdwenen, geen
regel veranderd, geen regel erbij op alle vijf.

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

  • Property mode set to 100644
File size: 17.2 KB
Line 
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') %>
5<div class="container ax-page">
6
7 <%- include('../partials/admin-back') %>
8 <%- include('../partials/media-tabs', { active: 'audio', audioOn: true }) %>
9 <header class="ax-header">
10 <div>
11 <h1><%= t('aaud.title') %></h1>
12 <p class="ax-tagline"><%= t('aaud.tagline_pre') %> <code>[[track:&lt;id&gt;]]</code> <%= t('aaud.tagline_post') %></p>
13 </div>
14 </header>
15
16 <% if (success) { %><div class="ax-flash ax-flash-ok"><%= success %></div><% } %>
17 <% if (error) { %><div class="ax-flash ax-flash-err"><%= error %></div><% } %>
18
19 <%# ── UPLOAD ──────────────────────────────────────────────── %>
20 <section class="ax-card">
21 <div class="ax-card-title"><%= t('aaud.upload') %></div>
22
23 <%# Bulk drag-drop zone. Multiple files at once, transcoded sequentially.
24 Title is auto-derived from each filename. Artist/album/cover are
25 SHARED across the batch (set once, applied to every track) — same
26 pattern as iTunes/Plex bulk-add. %>
27 <div class="ax-form">
28
29 <%# Shared metadata fields (apply to ALL files in the batch). %>
30 <div class="ax-row ax-row-2">
31 <label class="ax-field">
32 <span><%= t('aaud.artist') %> <small><%= t('aaud.applied_all') %></small></span>
33 <input type="text" id="batch-artist" placeholder="<%= t('aaud.optional') %>">
34 </label>
35 <label class="ax-field">
36 <span><%= t('aaud.album') %> <small><%= t('aaud.applied_all') %></small></span>
37 <input type="text" id="batch-album" placeholder="<%= t('aaud.optional') %>">
38 </label>
39 </div>
40 <label class="ax-field ax-file">
41 <span><%= t('aaud.cover') %> <small><%= t('aaud.cover_hint') %></small></span>
42 <span class="ax-file-control">
43 <span class="ax-btn ax-btn-secondary ax-file-btn">🖼 <%= t('aaud.choose_cover') %></span>
44 <span class="ax-file-name" data-empty><%= t('aaud.no_file') %></span>
45 <input type="file" id="batch-cover" accept="image/jpeg,image/png,image/webp,image/gif">
46 </span>
47 </label>
48
49 <%# The drop-zone itself. Click anywhere to open file picker. %>
50 <label class="ax-dropzone" id="audio-dropzone" tabindex="0">
51 <span class="ax-dropzone-icon">🎵</span>
52 <strong class="ax-dropzone-title"><%= t('aaud.drag_here') %></strong>
53 <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>
54 <input type="file" id="audio-files" accept="audio/*" multiple>
55 </label>
56
57 <%# Per-file progress list. Populated by JS as files are queued. %>
58 <ul class="ax-upload-queue" id="upload-queue" hidden></ul>
59
60 <div class="ax-form-actions" id="upload-actions" hidden>
61 <button type="button" class="ax-btn ax-btn-primary" id="start-upload-btn">⬆ <%= t('aaud.start_upload') %></button>
62 <button type="button" class="ax-btn" id="clear-queue-btn"><%= t('aaud.clear_list') %></button>
63 </div>
64 </div>
65 </section>
66
67 <%# ── TRACK LIST ─────────────────────────────────────────── %>
68 <section class="ax-card">
69 <div class="ax-card-title" style="display:flex;align-items:center;gap:.5rem">
70 <%= t('aaud.tracks') %> <span class="ax-count"><%= tracks.length %></span>
71 <button type="button" class="ax-btn ax-btn-secondary" id="add-link-track-btn"
72 style="margin-left:auto;font-size:.85rem" title="<%= t('aaud.add_link_track_title') %>">
73 + <%= t('aaud.add_link_track') %>
74 </button>
75 </div>
76
77 <% if (!tracks.length) { %>
78 <div class="ax-empty">
79 <div class="ax-empty-icon">♫</div>
80 <p><%= t('aaud.no_tracks') %></p>
81 </div>
82 <% } else { %>
83 <%
84 // Pre-fetch loop labels — `t` is shadowed by the loop var below.
85 var _aaudUntitled = t('aaud.untitled');
86 var _aaudPlay = t('aaud.play');
87 var _aaudEdit = t('aaud.edit');
88 var _aaudDlOn = t('aaud.dl_on');
89 var _aaudDlOff = t('aaud.dl_off');
90 var _aaudDelete = t('aaud.delete');
91 var _aaudCopy = t('aaud.copy_click');
92 var _aaudDelConfirm = t('aaud.delete_confirm');
93 %>
94 <ul class="ax-list">
95 <% tracks.forEach(function(t) { %>
96 <li class="ax-track" data-track-id="<%= t.id %>">
97 <% if (t.cover_url) { %>
98 <img class="ax-track-cover" src="<%= t.cover_url %>" alt="" data-cover-thumb>
99 <% } else { %>
100 <span class="ax-track-cover ax-track-cover-empty" data-cover-thumb>♫</span>
101 <% } %>
102
103 <div class="ax-track-meta">
104 <div class="ax-track-title" data-cell="title"><%= t.title || _aaudUntitled %></div>
105 <div class="ax-track-sub">
106 <span data-cell="artist"><%= t.artist || '—' %></span>
107 <% if (t.album) { %><span class="ax-track-sep">·</span><span data-cell="album"><%= t.album %></span><% } else { %><span data-cell="album" hidden></span><% } %>
108 <span class="ax-track-sep">·</span><span><%= t.size ? Math.round(t.size/1024) + ' KB' : '—' %></span>
109 </div>
110 <code class="ax-embed" data-copy="[[track:<%= t.id %>]]" title="<%= _aaudCopy %>">[[track:<%= t.id %>]]</code>
111 </div>
112
113 <div class="ax-track-actions">
114 <% if (t.stream_url) { %>
115 <button type="button" class="ax-icon-btn ax-track-play"
116 data-stream-url="<%= t.stream_url %>"
117 data-track-id="<%= t.id %>"
118 aria-label="<%= _aaudPlay %>" title="<%= _aaudPlay %>">
119 <span class="ax-track-play-icon" aria-hidden="true">▶</span>
120 </button>
121 <% } %>
122 <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label="<%= _aaudEdit %>" title="<%= _aaudEdit %>">✎</button>
123 <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
124 <button type="button" class="ax-icon-btn" data-track-dl data-id="<%= t.id %>" data-on="<%= t.downloadable ? '1' : '0' %>"
125 aria-label="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>" title="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>"
126 style="<%= t.downloadable ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">⬇</button>
127 <% } %>
128 <form action="/admin/audio/<%= t.id %>/delete" method="post" data-confirm="<%= _aaudDelConfirm %>" class="ax-track-delete">
129 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _aaudDelete %>" title="<%= _aaudDelete %>">🗑</button>
130 </form>
131 </div>
132 </li>
133 <% }); %>
134 </ul>
135 <% } %>
136 </section>
137
138 <% if ((typeof premiumUnlocked === 'undefined' || premiumUnlocked) && typeof embedUrl !== 'undefined') { %>
139 <section class="ax-section" style="margin-top:1.5rem">
140 <h2 style="margin:0 0 .4rem">🔊 <%= t('aaud.embed_player') %></h2>
141 <p style="opacity:.75;font-size:.9rem;margin:0 0 .6rem"><%= t('aaud.embed_hint') %></p>
142 <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>
143 <p style="margin:.6rem 0 0"><a class="btn" href="<%= embedUrl %>" target="_blank">▶ <%= t('aaud.preview_player') %></a></p>
144 </section>
145 <% } %>
146
147</div>
148
149<style>
150/* ─── Page wrapper ──────────────────────────────────────────────── */
151.ax-page {
152 max-width: 880px;
153 margin: 1.5rem auto 4rem;
154 padding: 0 1rem;
155}
156.ax-card-title {
157 font-family: var(--font-display, serif);
158 font-size: 1.15rem;
159 font-weight: 600;
160 margin-bottom: 1rem;
161 display: flex; align-items: baseline; gap: 0.5rem;
162}
163.ax-count {
164 font-family: var(--font-ui, system-ui);
165 font-size: 0.8rem;
166 color: var(--ink-muted, var(--ink-soft));
167 background: var(--paper-2);
168 padding: 0.1em 0.55em;
169 border-radius: 999px;
170 font-weight: 500;
171}
172
173/* ─── Form fields ──────────────────────────────────────────────── */
174.ax-form {
175 display: flex; flex-direction: column;
176 gap: 0.85rem;
177}
178.ax-row {
179 display: grid; gap: 0.85rem;
180}
181.ax-row-2 { grid-template-columns: 1fr 1fr; }
182@media (max-width: 600px) {
183 .ax-row-2 { grid-template-columns: 1fr; }
184}
185.ax-field {
186 display: flex; flex-direction: column;
187 gap: 0.35rem;
188 min-width: 0;
189}
190.ax-field > span {
191 font-size: 0.8rem;
192 font-weight: 600;
193 color: var(--ink-soft);
194 display: flex; align-items: baseline; gap: 0.4rem;
195 flex-wrap: wrap;
196}
197.ax-field > span small {
198 font-weight: 400; color: var(--ink-muted);
199 font-size: 0.95em;
200}
201.ax-field input[type="text"],
202.ax-field input[type="url"],
203.ax-field input[type="email"],
204.ax-field select,
205.ax-field textarea {
206 width: 100%;
207 box-sizing: border-box;
208 padding: 0.6rem 0.75rem;
209 border: 1px solid var(--rule);
210 border-radius: 6px;
211 background: var(--paper-2);
212 color: var(--ink);
213 font-family: var(--font-ui, system-ui), sans-serif;
214 font-size: 0.95rem;
215 -webkit-appearance: none; appearance: none;
216 transition: border-color 120ms;
217}
218.ax-field input:focus,
219.ax-field select:focus,
220.ax-field textarea:focus {
221 outline: 2px solid var(--accent);
222 outline-offset: -1px;
223 border-color: var(--accent);
224}
225
226/* ─── File inputs (custom-styled wrapper) ─────────────────────── */
227.ax-file-control {
228 display: flex; align-items: center; gap: 0.5rem;
229 flex-wrap: wrap;
230 position: relative;
231 min-height: 40px;
232}
233.ax-file-control input[type="file"] {
234 position: absolute; inset: 0;
235 opacity: 0; cursor: pointer;
236 z-index: 2;
237}
238.ax-file-btn {
239 pointer-events: none; /* let clicks pass through to the file input */
240}
241.ax-file-name {
242 font-size: 0.85rem;
243 color: var(--ink);
244 word-break: break-all;
245 flex: 1;
246 min-width: 0;
247}
248.ax-file-name[data-empty] { color: var(--ink-muted, var(--ink-soft)); }
249
250.ax-form-actions {
251 display: flex; gap: 0.5rem;
252 margin-top: 0.25rem;
253}
254.ax-form-actions[hidden] { display: none; }
255
256/* ─── Buttons ──────────────────────────────────────────────────── */
257.ax-btn {
258 display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem;
259 padding: 0.55rem 1rem;
260 border: 1px solid var(--rule);
261 background: var(--paper-2);
262 color: var(--ink);
263 font-family: var(--font-ui, system-ui), sans-serif;
264 font-size: 0.9rem; font-weight: 500;
265 text-decoration: none;
266 border-radius: 6px;
267 cursor: pointer;
268 min-height: 40px;
269 transition: background 120ms, border-color 120ms;
270 -webkit-tap-highlight-color: transparent;
271}
272
273.ax-track-play.is-playing {
274 border-color: var(--accent);
275 color: var(--accent);
276 background: color-mix(in srgb, var(--accent) 12%, var(--paper-2));
277}
278.ax-track-play-icon { font-size: 0.85rem; line-height: 1; }
279.ax-empty-icon {
280 font-size: 2.5rem;
281 font-family: var(--font-display, serif);
282 color: var(--ink-soft);
283 opacity: 0.4;
284 margin-bottom: 0.5rem;
285}
286.ax-track {
287 display: grid;
288 grid-template-columns: 56px 1fr auto;
289 gap: 0.85rem;
290 align-items: center;
291 padding: 0.75rem;
292 background: var(--paper-2);
293 border: 1px solid var(--rule);
294 border-radius: 10px;
295 transition: border-color 120ms;
296}
297.ax-track-cover {
298 width: 56px; height: 56px;
299 border-radius: 6px;
300 object-fit: cover;
301 background: var(--paper);
302 display: flex; align-items: center; justify-content: center;
303 font-family: var(--font-display, serif);
304 font-size: 1.5rem;
305 color: var(--accent);
306 flex-shrink: 0;
307}
308.ax-track-cover-empty { /* span variant */ }
309.ax-track-meta {
310 min-width: 0; /* allow text-overflow on long titles */
311 display: flex; flex-direction: column; gap: 0.2rem;
312}
313.ax-track-title {
314 font-weight: 600;
315 font-size: 0.95rem;
316 color: var(--ink);
317 overflow: hidden;
318 text-overflow: ellipsis;
319 white-space: nowrap;
320}
321.ax-track-sub {
322 font-size: 0.8rem;
323 color: var(--ink-muted, var(--ink-soft));
324 display: flex; flex-wrap: wrap; gap: 0.3rem; align-items: center;
325}
326.ax-embed {
327 display: inline-block;
328 background: var(--paper);
329 padding: 0.15rem 0.5rem;
330 border-radius: 4px;
331 font-family: var(--font-mono, ui-monospace, monospace);
332 font-size: 0.75rem;
333 color: var(--ink-soft);
334 cursor: pointer;
335 user-select: all;
336 margin-top: 0.15rem;
337 word-break: break-all;
338 align-self: flex-start;
339 transition: background 120ms;
340}
341.ax-track-delete { display: inline; }
342
343/* ─── Mobile tweaks ───────────────────────────────────────────── */
344@media (max-width: 480px) {
345 .ax-track {
346 grid-template-columns: 48px 1fr;
347 grid-template-areas: "cover meta" "actions actions";
348 gap: 0.6rem;
349 }
350 .ax-track-cover { grid-area: cover; width: 48px; height: 48px; }
351}
352
353/* ─── Bulk drop-zone ──────────────────────────────────────────── */
354.ax-dropzone {
355 display: flex; flex-direction: column; align-items: center; justify-content: center;
356 gap: 0.4rem;
357 padding: 2rem 1rem;
358 border: 2px dashed var(--rule);
359 border-radius: 12px;
360 background: var(--paper-2);
361 cursor: pointer;
362 text-align: center;
363 transition: border-color 150ms, background 150ms;
364 position: relative;
365 min-height: 160px;
366}
367.ax-dropzone:hover,
368.ax-dropzone:focus-within {
369 border-color: var(--accent);
370}
371.ax-dropzone.is-dragover {
372 border-color: var(--accent);
373 background: color-mix(in srgb, var(--accent) 8%, var(--paper-2));
374}
375.ax-dropzone input[type="file"] {
376 /* Cover the whole dropzone but invisible — clicks bubble through to it. */
377 position: absolute; inset: 0;
378 opacity: 0;
379 cursor: pointer;
380}
381.ax-dropzone-icon {
382 font-size: 2rem;
383 opacity: 0.5;
384 line-height: 1;
385}
386.ax-dropzone-title {
387 font-size: 1rem;
388 font-weight: 600;
389 color: var(--ink);
390}
391.ax-dropzone-sub {
392 font-size: 0.85rem;
393 color: var(--ink-muted, var(--ink-soft));
394 line-height: 1.4;
395}
396.ax-dropzone-sub small {
397 font-size: 0.75rem;
398 opacity: 0.8;
399}
400
401/* ─── Upload queue ────────────────────────────────────────────── */
402.ax-upload-queue {
403 list-style: none;
404 padding: 0; margin: 0.75rem 0 0;
405 display: flex; flex-direction: column;
406 gap: 0.4rem;
407}
408.ax-queue-item {
409 display: grid;
410 grid-template-columns: 1fr auto;
411 gap: 0.75rem;
412 align-items: center;
413 padding: 0.6rem 0.85rem;
414 background: var(--paper);
415 border: 1px solid var(--rule);
416 border-radius: 8px;
417 font-size: 0.875rem;
418 transition: border-color 150ms, opacity 150ms;
419}
420.ax-queue-item--active {
421 border-color: var(--accent);
422}
423.ax-queue-item--done {
424 border-color: rgba(40,160,90,0.4);
425 opacity: 0.85;
426}
427.ax-queue-item--error {
428 border-color: rgba(220,60,60,0.5);
429}
430.ax-queue-name {
431 font-weight: 500;
432 color: var(--ink);
433 overflow: hidden;
434 text-overflow: ellipsis;
435 white-space: nowrap;
436 min-width: 0;
437}
438.ax-queue-size {
439 font-size: 0.75rem;
440 color: var(--ink-muted, var(--ink-soft));
441 margin-left: 0.5rem;
442}
443.ax-queue-status {
444 font-size: 0.75rem;
445 font-weight: 500;
446 padding: 0.15em 0.6em;
447 border-radius: 999px;
448 background: var(--paper-2);
449 color: var(--ink-soft);
450 white-space: nowrap;
451 flex-shrink: 0;
452}
453.ax-queue-status--queued { color: var(--ink-muted, var(--ink-soft)); }
454.ax-queue-status--uploading,
455.ax-queue-status--transcoding {
456 color: var(--accent);
457 background: color-mix(in srgb, var(--accent) 10%, var(--paper-2));
458}
459.ax-queue-status--done {
460 color: #2a9d5e;
461 background: rgba(40,160,90,0.12);
462}
463.ax-queue-status--error {
464 color: #dc2626;
465 background: rgba(220,60,60,0.12);
466}
467</style>
468
469<%# Track editor modal (exposes window.openTrackEditor) %>
470<%- include('../partials/track-editor', { csrfToken: (typeof csrfToken !== 'undefined' ? csrfToken : '') }) %>
471
472<%# Het script staat in assets/js/mod/admin-audio.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
473<%- 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.