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

main
Last change on this file since 746d98a was ca260d1, checked in by Robin <roboburr@…>, 5 hours ago

Fedi open audio per track, vanuit de mediamanager

Robin, 30-9: "Fedi open audio ook per track aan kunnen zetten in de media
manager."

De vlag was altijd al per track (audio_tracks.fedi_open): hij opent het
bestand en maakt er een echte Audio-bijlage van, met een speler in Mastodon en
consorten. Aanzetten kon alleen per POST, met het vinkje in de editor, dat elke
track in die post opende. Nu staat er in de mediamanager, tab Audio, per track
een wereldbol.

EENRICHTINGS, net als in de editor. Dicht is het een gedimde knop die eerst
vraagt of het zeker is, want terug kan niet; open is het een slotje in de
accentkleur en geen knop meer. Een bestand dat al gefedereerd is staat op andere
servers, en het hier weer afschermen zou schijnveiligheid zijn. Ook de algemene
bijwerk-API van een track neemt fedi_open niet aan; een toets bewaakt dat.

Alleen bij een track met een eigen bestand: een track die alleen een link is,
heeft niets om te openen. Een formulier en geen fetch, dus het werkt zonder
JavaScript, en data-confirm uit shell.ejs stelt de vraag.

EN DE POSTS GAAN OPNIEUW DE DEUR UIT. Het vinkje in de editor werkt zichtbaar
omdat het opslaan van de post een Update stuurt, met de audio erin. Een knop
die alleen de vlag zet, laat elke post die al verstuurd is zonder speler staan
tot iemand hem bewerkt. Dus hier dezelfde Update, voor elke gepubliceerde post
van deze site waar de track in staat: rechtstreeks, via een playlist of via zijn
album. postsEmbeddingTrack in music/index.js zoekt die op; trackHostPosts kiest
per track maar een post, voor de bibliotheek. De bibliotheek zelf hoeft niets,
die wordt opgehaald en een open track staat er meteen in.

Getoetst: openen zet de vlag en stuurt precies de drie posts (geen concept,
geen post zonder de track, geen post van een andere site); nog eens openen doet
niets; een link-track en een track van een andere site blijven dicht; de route;
geen weg terug via de API; de knop alleen waar het kan, met bevestiging; en de
hulpfunctie rechtstreeks. Tegenproef per zaak. Eentje ving eerst niets: het
sitefilter zit dubbel, want openen haalt elke post nog eens op met site_id. De
hulpfunctie is los te gebruiken, dus die wordt nu ook rechtstreeks getoetst.
Volledige suite 1301 groen.

Nagekeken in de gerenderde mediamanager met de echte stylesheet: open in de
accentkleur, dicht gedimd, geen wereldbol bij een link-track, dezelfde maat als
de andere knoppen in de rij.

Co-Authored-By: Claude Opus 5.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 var _aaudFediBtn = t('aaud.fedi_open_btn');
94 var _aaudFediOn = t('aaud.fedi_open_on');
95 var _aaudFediConfirm = t('aaud.fedi_open_confirm');
96 %>
97 <ul class="ax-list">
98 <% tracks.forEach(function(t) { %>
99 <li class="ax-track" data-track-id="<%= t.id %>">
100 <% if (t.cover_url) { %>
101 <img class="ax-track-cover" src="<%= t.cover_url %>" alt="" data-cover-thumb>
102 <% } else { %>
103 <span class="ax-track-cover ax-track-cover-empty" data-cover-thumb>♫</span>
104 <% } %>
105
106 <div class="ax-track-meta">
107 <div class="ax-track-title" data-cell="title"><%= t.title || _aaudUntitled %></div>
108 <div class="ax-track-sub">
109 <span data-cell="artist"><%= t.artist || '—' %></span>
110 <% if (t.album) { %><span class="ax-track-sep">·</span><span data-cell="album"><%= t.album %></span><% } else { %><span data-cell="album" hidden></span><% } %>
111 <span class="ax-track-sep">·</span><span><%= t.size ? Math.round(t.size/1024) + ' KB' : '—' %></span>
112 </div>
113 <code class="ax-embed" data-copy="[[track:<%= t.id %>]]" title="<%= _aaudCopy %>">[[track:<%= t.id %>]]</code>
114 </div>
115
116 <div class="ax-track-actions">
117 <% if (t.stream_url) { %>
118 <button type="button" class="ax-icon-btn ax-track-play"
119 data-stream-url="<%= t.stream_url %>"
120 data-track-id="<%= t.id %>"
121 aria-label="<%= _aaudPlay %>" title="<%= _aaudPlay %>">
122 <span class="ax-track-play-icon" aria-hidden="true">▶</span>
123 </button>
124 <% } %>
125 <button type="button" class="ax-icon-btn" data-track-edit data-id="<%= t.id %>" aria-label="<%= _aaudEdit %>" title="<%= _aaudEdit %>">✎</button>
126 <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
127 <button type="button" class="ax-icon-btn" data-track-dl data-id="<%= t.id %>" data-on="<%= t.downloadable ? '1' : '0' %>"
128 aria-label="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>" title="<%= t.downloadable ? _aaudDlOn : _aaudDlOff %>"
129 style="<%= t.downloadable ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">⬇</button>
130 <% } %>
131 <%# Openbaar op de fediverse, per track (Robin, 30-9). EENRICHTINGS, net
132 als het vinkje in de post-editor: open is een slotje en geen knop meer,
133 want een bestand dat al verspreid is haal je niet terug. Alleen bij een
134 track met een eigen bestand; een link heeft niets om te openen. Een
135 formulier en geen fetch: werkt zonder JS, en data-confirm vraagt het
136 eerst (shell.ejs). %>
137 <% if (t.media_id) { %>
138 <% if (t.fedi_open) { %>
139 <span class="ax-icon-btn ax-fedi-on" role="img" aria-label="<%= _aaudFediOn %>" title="<%= _aaudFediOn %>"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span>
140 <% } else { %>
141 <form action="/admin/audio/<%= t.id %>/fedi-open" method="post" data-confirm="<%= _aaudFediConfirm %>" class="ax-track-fedi">
142 <button type="submit" class="ax-icon-btn ax-fedi-off" aria-label="<%= _aaudFediBtn %>" title="<%= _aaudFediBtn %>"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></button>
143 </form>
144 <% } %>
145 <% } %>
146 <form action="/admin/audio/<%= t.id %>/delete" method="post" data-confirm="<%= _aaudDelConfirm %>" class="ax-track-delete">
147 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _aaudDelete %>" title="<%= _aaudDelete %>">🗑</button>
148 </form>
149 </div>
150 </li>
151 <% }); %>
152 </ul>
153 <% } %>
154 </section>
155
156 <% if ((typeof premiumUnlocked === 'undefined' || premiumUnlocked) && typeof embedUrl !== 'undefined') { %>
157 <section class="ax-section" style="margin-top:1.5rem">
158 <h2 style="margin:0 0 .4rem">🔊 <%= t('aaud.embed_player') %></h2>
159 <p style="opacity:.75;font-size:.9rem;margin:0 0 .6rem"><%= t('aaud.embed_hint') %></p>
160 <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>
161 <p style="margin:.6rem 0 0"><a class="btn" href="<%= embedUrl %>" target="_blank">▶ <%= t('aaud.preview_player') %></a></p>
162 </section>
163 <% } %>
164
165</div>
166
167<style>
168/* ─── Page wrapper ──────────────────────────────────────────────── */
169.ax-page {
170 max-width: 880px;
171 margin: 1.5rem auto 4rem;
172 padding: 0 1rem;
173}
174.ax-card-title {
175 font-family: var(--font-display, serif);
176 font-size: 1.15rem;
177 font-weight: 600;
178 margin-bottom: 1rem;
179 display: flex; align-items: baseline; gap: 0.5rem;
180}
181.ax-count {
182 font-family: var(--font-ui, system-ui);
183 font-size: 0.8rem;
184 color: var(--ink-muted, var(--ink-soft));
185 background: var(--paper-2);
186 padding: 0.1em 0.55em;
187 border-radius: 999px;
188 font-weight: 500;
189}
190
191/* ─── Form fields ──────────────────────────────────────────────── */
192.ax-form {
193 display: flex; flex-direction: column;
194 gap: 0.85rem;
195}
196.ax-row {
197 display: grid; gap: 0.85rem;
198}
199.ax-row-2 { grid-template-columns: 1fr 1fr; }
200@media (max-width: 600px) {
201 .ax-row-2 { grid-template-columns: 1fr; }
202}
203.ax-field {
204 display: flex; flex-direction: column;
205 gap: 0.35rem;
206 min-width: 0;
207}
208.ax-field > span {
209 font-size: 0.8rem;
210 font-weight: 600;
211 color: var(--ink-soft);
212 display: flex; align-items: baseline; gap: 0.4rem;
213 flex-wrap: wrap;
214}
215.ax-field > span small {
216 font-weight: 400; color: var(--ink-muted);
217 font-size: 0.95em;
218}
219.ax-field input[type="text"],
220.ax-field input[type="url"],
221.ax-field input[type="email"],
222.ax-field select,
223.ax-field textarea {
224 width: 100%;
225 box-sizing: border-box;
226 padding: 0.6rem 0.75rem;
227 border: 1px solid var(--rule);
228 border-radius: 6px;
229 background: var(--paper-2);
230 color: var(--ink);
231 font-family: var(--font-ui, system-ui), sans-serif;
232 font-size: 0.95rem;
233 -webkit-appearance: none; appearance: none;
234 transition: border-color 120ms;
235}
236.ax-field input:focus,
237.ax-field select:focus,
238.ax-field textarea:focus {
239 outline: 2px solid var(--accent);
240 outline-offset: -1px;
241 border-color: var(--accent);
242}
243
244/* ─── File inputs (custom-styled wrapper) ─────────────────────── */
245.ax-file-control {
246 display: flex; align-items: center; gap: 0.5rem;
247 flex-wrap: wrap;
248 position: relative;
249 min-height: 40px;
250}
251.ax-file-control input[type="file"] {
252 position: absolute; inset: 0;
253 opacity: 0; cursor: pointer;
254 z-index: 2;
255}
256.ax-file-btn {
257 pointer-events: none; /* let clicks pass through to the file input */
258}
259.ax-file-name {
260 font-size: 0.85rem;
261 color: var(--ink);
262 word-break: break-all;
263 flex: 1;
264 min-width: 0;
265}
266.ax-file-name[data-empty] { color: var(--ink-muted, var(--ink-soft)); }
267/* Openbaar op de fediverse: dicht is gedimd, net als een uitgezette download;
268 open is de accentkleur en geen knop meer, want terug kan niet. */
269.ax-track-fedi { display: inline; }
270.ax-fedi-off { opacity: .5; }
271.ax-fedi-off:hover, .ax-fedi-off:focus-visible { opacity: 1; }
272.ax-fedi-on { color: var(--accent, #6b8f71); cursor: default; }
273
274.ax-form-actions {
275 display: flex; gap: 0.5rem;
276 margin-top: 0.25rem;
277}
278.ax-form-actions[hidden] { display: none; }
279
280.ax-track-play.is-playing {
281 border-color: var(--accent);
282 color: var(--accent);
283 background: color-mix(in srgb, var(--accent) 12%, var(--paper-2));
284}
285.ax-track-play-icon { font-size: 0.85rem; line-height: 1; }
286.ax-track-cover-empty { /* span variant */ }
287.ax-track-delete { display: inline; }
288
289/* ─── Mobile tweaks ───────────────────────────────────────────── */
290@media (max-width: 480px) {
291}
292
293/* ─── Bulk drop-zone ──────────────────────────────────────────── */
294.ax-dropzone {
295 display: flex; flex-direction: column; align-items: center; justify-content: center;
296 gap: 0.4rem;
297 padding: 2rem 1rem;
298 border: 2px dashed var(--rule);
299 border-radius: 12px;
300 background: var(--paper-2);
301 cursor: pointer;
302 text-align: center;
303 transition: border-color 150ms, background 150ms;
304 position: relative;
305 min-height: 160px;
306}
307.ax-dropzone:hover,
308.ax-dropzone:focus-within {
309 border-color: var(--accent);
310}
311.ax-dropzone.is-dragover {
312 border-color: var(--accent);
313 background: color-mix(in srgb, var(--accent) 8%, var(--paper-2));
314}
315.ax-dropzone input[type="file"] {
316 /* Cover the whole dropzone but invisible — clicks bubble through to it. */
317 position: absolute; inset: 0;
318 opacity: 0;
319 cursor: pointer;
320}
321.ax-dropzone-icon {
322 font-size: 2rem;
323 opacity: 0.5;
324 line-height: 1;
325}
326.ax-dropzone-title {
327 font-size: 1rem;
328 font-weight: 600;
329 color: var(--ink);
330}
331.ax-dropzone-sub {
332 font-size: 0.85rem;
333 color: var(--ink-muted, var(--ink-soft));
334 line-height: 1.4;
335}
336.ax-dropzone-sub small {
337 font-size: 0.75rem;
338 opacity: 0.8;
339}
340
341/* ─── Upload queue ────────────────────────────────────────────── */
342.ax-upload-queue {
343 list-style: none;
344 padding: 0; margin: 0.75rem 0 0;
345 display: flex; flex-direction: column;
346 gap: 0.4rem;
347}
348.ax-queue-item {
349 display: grid;
350 grid-template-columns: 1fr auto;
351 gap: 0.75rem;
352 align-items: center;
353 padding: 0.6rem 0.85rem;
354 background: var(--paper);
355 border: 1px solid var(--rule);
356 border-radius: 8px;
357 font-size: 0.875rem;
358 transition: border-color 150ms, opacity 150ms;
359}
360.ax-queue-item--active {
361 border-color: var(--accent);
362}
363.ax-queue-item--done {
364 border-color: rgba(40,160,90,0.4);
365 opacity: 0.85;
366}
367.ax-queue-item--error {
368 border-color: rgba(220,60,60,0.5);
369}
370.ax-queue-name {
371 font-weight: 500;
372 color: var(--ink);
373 overflow: hidden;
374 text-overflow: ellipsis;
375 white-space: nowrap;
376 min-width: 0;
377}
378.ax-queue-size {
379 font-size: 0.75rem;
380 color: var(--ink-muted, var(--ink-soft));
381 margin-left: 0.5rem;
382}
383.ax-queue-status {
384 font-size: 0.75rem;
385 font-weight: 500;
386 padding: 0.15em 0.6em;
387 border-radius: 999px;
388 background: var(--paper-2);
389 color: var(--ink-soft);
390 white-space: nowrap;
391 flex-shrink: 0;
392}
393.ax-queue-status--queued { color: var(--ink-muted, var(--ink-soft)); }
394.ax-queue-status--uploading,
395.ax-queue-status--transcoding {
396 color: var(--accent);
397 background: color-mix(in srgb, var(--accent) 10%, var(--paper-2));
398}
399.ax-queue-status--done {
400 color: #2a9d5e;
401 background: rgba(40,160,90,0.12);
402}
403.ax-queue-status--error {
404 color: #dc2626;
405 background: rgba(220,60,60,0.12);
406}
407</style>
408
409<%# Track editor modal (exposes window.openTrackEditor) %>
410<%- include('../partials/track-editor', { csrfToken: (typeof csrfToken !== 'undefined' ? csrfToken : '') }) %>
411
412<%# Het script staat in assets/js/mod/admin-audio.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
413<%- 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.