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

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

Beheer: admin-audio neemt de maten van de gedeelde romp over

Robin zag dat /admin/audio nog de oude knop had en /admin/playlists de
goede. De markup was al gelijk -- beide gebruiken de terugknop-partial --
maar admin-audio had een EIGEN .ax-btn in zijn style-blok, en die staat
na de include en wint dus.

Het bleek geen los geval. admin-audio droeg een oudere, systematisch
kleinere kopie van het hele systeem:

.ax-btn 40px / radius 6 / 0.9rem -> 44px / radius 8 / 0.95rem
.ax-track hoes 56px / radius 10 -> 64px / radius 12
.ax-track-title 0.95rem -> 1.05rem
.ax-track-sub 0.8rem -> 0.85rem
@media 480px hoes 48px -> 56px

De partial kwam van admin-playlists, en dat is precies de versie die er
"goed" uitzag. Alle tien de afwijkingen zijn nu weg; audio en playlists
hebben nul regels die van de partial verschillen.

TWEE DINGEN DIE ECHT VERANDEREN, en niet alleen in maat:

  • Lange tracktitels op /admin/audio WRAPPEN nu in plaats van af te kappen met een ellipsis. De gedeelde .ax-track-title is een flex-regel (playlists zet er een pill in) en text-overflow werkt daar niet. De hele titel blijft zo zichtbaar; dat leek me de betere kant van de ruil, maar het is een keuze en geen bijvangst.
  • Het ♫-teken in de lege toestand verliest zijn display-font en eigen kleur, en erft nu van .ax-empty.

object-fit: cover staat nu OP .ax-track-cover en niet alleen op een img
erbinnen: admin-audio zet die klasse rechtstreeks op een <img>,
admin-listeners op een omhulsel. Op een niet-vervangen element doet
object-fit niets, dus de tweede vorm merkt er niets van. Zonder dit had
het weghalen van audio's kopie de hoezen uitgerekt.

scripts/admin-css.mjs heeft er --forceer bij: die haalt OOK de afwijkende
regels weg, zodat de pagina de vorm van de partial overneemt. Apart van
--doen, want --doen verplaatst alleen en dit VERANDERT hoe een pagina
eruitziet -- dat hoort een expliciete keuze te zijn. Met --toon laat hij
per eigenschap zien wat er verschilt; zo zijn bovenstaande maten
gevonden.

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

  • Property mode set to 100644
File size: 14.9 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.ax-track-play.is-playing {
257 border-color: var(--accent);
258 color: var(--accent);
259 background: color-mix(in srgb, var(--accent) 12%, var(--paper-2));
260}
261.ax-track-play-icon { font-size: 0.85rem; line-height: 1; }
262.ax-track-cover-empty { /* span variant */ }
263.ax-track-delete { display: inline; }
264
265/* ─── Mobile tweaks ───────────────────────────────────────────── */
266@media (max-width: 480px) {
267}
268
269/* ─── Bulk drop-zone ──────────────────────────────────────────── */
270.ax-dropzone {
271 display: flex; flex-direction: column; align-items: center; justify-content: center;
272 gap: 0.4rem;
273 padding: 2rem 1rem;
274 border: 2px dashed var(--rule);
275 border-radius: 12px;
276 background: var(--paper-2);
277 cursor: pointer;
278 text-align: center;
279 transition: border-color 150ms, background 150ms;
280 position: relative;
281 min-height: 160px;
282}
283.ax-dropzone:hover,
284.ax-dropzone:focus-within {
285 border-color: var(--accent);
286}
287.ax-dropzone.is-dragover {
288 border-color: var(--accent);
289 background: color-mix(in srgb, var(--accent) 8%, var(--paper-2));
290}
291.ax-dropzone input[type="file"] {
292 /* Cover the whole dropzone but invisible — clicks bubble through to it. */
293 position: absolute; inset: 0;
294 opacity: 0;
295 cursor: pointer;
296}
297.ax-dropzone-icon {
298 font-size: 2rem;
299 opacity: 0.5;
300 line-height: 1;
301}
302.ax-dropzone-title {
303 font-size: 1rem;
304 font-weight: 600;
305 color: var(--ink);
306}
307.ax-dropzone-sub {
308 font-size: 0.85rem;
309 color: var(--ink-muted, var(--ink-soft));
310 line-height: 1.4;
311}
312.ax-dropzone-sub small {
313 font-size: 0.75rem;
314 opacity: 0.8;
315}
316
317/* ─── Upload queue ────────────────────────────────────────────── */
318.ax-upload-queue {
319 list-style: none;
320 padding: 0; margin: 0.75rem 0 0;
321 display: flex; flex-direction: column;
322 gap: 0.4rem;
323}
324.ax-queue-item {
325 display: grid;
326 grid-template-columns: 1fr auto;
327 gap: 0.75rem;
328 align-items: center;
329 padding: 0.6rem 0.85rem;
330 background: var(--paper);
331 border: 1px solid var(--rule);
332 border-radius: 8px;
333 font-size: 0.875rem;
334 transition: border-color 150ms, opacity 150ms;
335}
336.ax-queue-item--active {
337 border-color: var(--accent);
338}
339.ax-queue-item--done {
340 border-color: rgba(40,160,90,0.4);
341 opacity: 0.85;
342}
343.ax-queue-item--error {
344 border-color: rgba(220,60,60,0.5);
345}
346.ax-queue-name {
347 font-weight: 500;
348 color: var(--ink);
349 overflow: hidden;
350 text-overflow: ellipsis;
351 white-space: nowrap;
352 min-width: 0;
353}
354.ax-queue-size {
355 font-size: 0.75rem;
356 color: var(--ink-muted, var(--ink-soft));
357 margin-left: 0.5rem;
358}
359.ax-queue-status {
360 font-size: 0.75rem;
361 font-weight: 500;
362 padding: 0.15em 0.6em;
363 border-radius: 999px;
364 background: var(--paper-2);
365 color: var(--ink-soft);
366 white-space: nowrap;
367 flex-shrink: 0;
368}
369.ax-queue-status--queued { color: var(--ink-muted, var(--ink-soft)); }
370.ax-queue-status--uploading,
371.ax-queue-status--transcoding {
372 color: var(--accent);
373 background: color-mix(in srgb, var(--accent) 10%, var(--paper-2));
374}
375.ax-queue-status--done {
376 color: #2a9d5e;
377 background: rgba(40,160,90,0.12);
378}
379.ax-queue-status--error {
380 color: #dc2626;
381 background: rgba(220,60,60,0.12);
382}
383</style>
384
385<%# Track editor modal (exposes window.openTrackEditor) %>
386<%- include('../partials/track-editor', { csrfToken: (typeof csrfToken !== 'undefined' ? csrfToken : '') }) %>
387
388<%# Het script staat in assets/js/mod/admin-audio.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
389<%- 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.