source: Klonkt/src/views/partials/playlist-editor.ejs@ d8f7d9c

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

Playlists: uitgavedatum en MusicBrainz release-id op een album

Stap 1 van shaer-756s. Het album bestaat al als ding -- een playlist met
kind='album', met eigen id, titel, artiest, jaar, hoes en een eigen
AP-collectie. Wat ontbrak om het als Album uit te geven zijn twee velden.

release_date en niet year: die kolom blijft en wordt gewoon getoond,
maar hun AlbumSerializer leest released als een DateField. Een jaartal
als 2024-01-01 versturen is een dag verzinnen, en dat is precies wat we
bij artiesten en albums niet doen. Volledige datum of niets -- ook
2024-02-31 valt af, want Date rolt die stil door naar 2 maart en dan
bewaren we iets anders dan er ingetypt is.

mb_release_id is de tegenhanger van sites.mb_artist_id: dezelfde
verwijzing naar MusicBrainz, een niveau lager.

ALLEEN BIJ kind='album', en dat wordt in PlaylistService afgedwongen en
niet alleen in het scherm. De API ligt open -- de post-editor gebruikt
hem ook -- en een scherm is geen bewaking. Omschakelen naar afspeellijst
maakt de velden ALTIJD leeg, ook als de aanroeper er niets over zei:
anders houdt een album dat je tot mixtape ombouwt zijn uitgavedatum, en
duikt die weer op zodra iemand hem terugzet. Het leespad geeft ze ook
niet door bij een afspeellijst, zodat een oude rij niet alsnog lekt.

In de editor staan de velden onder Type en verdwijnen ze als je naar
playlist schakelt. Daar hoorde een CSS-regel bij: .pl-field is display:
flex en dat wint van het hidden-attribuut -- zonder .pl-field[hidden]
{ display: none } bleven ze gewoon staan.

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

  • Property mode set to 100644
File size: 9.3 KB
Line 
1<%
2// Playlist editor modal — included once per page (in admin-playlists, and in
3// post-edit when present). Exposes `window.openPlaylistEditor({ mode, id?, onSaved? })`.
4//
5// mode: 'create' or 'edit'
6// id : (edit only) playlist id to load
7// onSaved: callback receiving { id, playlist } — used by post-editor
8// to insert [[playlist:id]] at the cursor.
9//
10// The modal itself is appended on first call; subsequent calls reuse the
11// same DOM. CSS is included once. JS lives below.
12
13const _csrf = (typeof csrfToken !== 'undefined' && csrfToken) || '';
14%>
15
16<style>
17/* ─── Editor modal ─────────────────────────────────────────────── */
18.pl-modal-backdrop {
19 position: fixed; inset: 0; z-index: 9000;
20 background: color-mix(in srgb, #000 50%, transparent);
21 display: flex; align-items: center; justify-content: center;
22 padding: 1rem;
23 -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
24}
25.pl-modal {
26 background: var(--paper);
27 border: 1px solid var(--rule); border-radius: 16px;
28 width: 100%; max-width: 960px;
29 max-height: 92vh;
30 display: flex; flex-direction: column;
31 box-shadow: 0 24px 60px rgba(0, 0, 0, .35);
32 overflow: hidden;
33}
34.pl-modal-header {
35 padding: 1rem 1.25rem;
36 border-bottom: 1px solid var(--rule);
37 display: flex; align-items: center; justify-content: space-between;
38}
39.pl-modal-header h3 { margin: 0; font-size: 1.15rem; }
40.pl-modal-close {
41 background: transparent; border: 0;
42 font-size: 1.5rem; line-height: 1; cursor: pointer;
43 width: 36px; height: 36px; border-radius: 8px;
44 display: inline-flex; align-items: center; justify-content: center;
45 color: var(--ink-soft);
46}
47.pl-modal-close:hover { background: var(--paper-2); color: var(--ink); }
48.pl-modal-body {
49 flex: 1 1 auto; min-height: 0;
50 overflow: hidden;
51 padding: 1.25rem;
52 /* Become a flex container so .pl-editor-cols (grid) can flex into the
53 full available height. Without this, .pl-editor-cols had no bounded
54 parent height and inner overflow:auto never fired. */
55 display: flex; flex-direction: column;
56}
57.pl-modal-footer {
58 padding: .75rem 1.25rem;
59 border-top: 1px solid var(--rule);
60 display: flex; justify-content: flex-end; gap: .5rem;
61}
62
63.pl-editor-cols {
64 flex: 1 1 auto; min-height: 0;
65 display: grid;
66 grid-template-columns: 1fr 1fr;
67 gap: 1.5rem;
68 overflow: hidden;
69}
70@media (max-width: 720px) {
71 /* On mobile, abandon the inner-scroll-container model entirely. Two
72 scrolling boxes side-by-side don't fit, and stacked scroll boxes inside
73 a fixed modal create a scroll-trap (touch scroll easily lands on the
74 wrong layer). Instead: let the modal-body scroll the whole content,
75 and let lists flow at their natural height. */
76 .pl-editor-cols {
77 grid-template-columns: 1fr;
78 gap: 1rem;
79 height: auto;
80 overflow: visible;
81 }
82 .pl-modal-body { overflow: auto; -webkit-overflow-scrolling: touch; }
83 .pl-editor-left, .pl-editor-right { overflow: visible; min-height: 0; }
84 .pl-selected-list, .pl-available-list { overflow: visible; flex: 0 0 auto; }
85}
86
87.pl-editor-left,
88.pl-editor-right {
89 display: flex; flex-direction: column;
90 min-height: 0;
91 overflow: hidden;
92}
93
94/* Meta grid (title/artist/year/kind/cover) */
95.pl-meta-grid {
96 display: grid; gap: .5rem .75rem;
97 grid-template-columns: 1fr 1fr;
98 margin-bottom: 1rem;
99}
100.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; }
105.pl-field-full { grid-column: 1 / -1; }
106.pl-field > span {
107 font-size: .8rem; color: var(--ink-soft); font-weight: 500;
108}
109.pl-field input,
110.pl-field select {
111 width: 100%;
112 padding: .5rem .75rem;
113 border: 1px solid var(--rule); border-radius: 8px;
114 background: var(--paper); color: var(--ink);
115 font-family: var(--font-ui); font-size: .95rem;
116}
117.pl-field input:focus,
118.pl-field select:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
119
120.pl-cover-row {
121 display: flex; align-items: center; gap: .5rem;
122}
123.pl-cover-thumb {
124 width: 48px; height: 48px; border-radius: 8px;
125 background: var(--paper-2);
126 border: 1px solid var(--rule);
127 display: flex; align-items: center; justify-content: center;
128 cursor: pointer; padding: 0; overflow: hidden;
129}
130.pl-cover-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
131.pl-cover-empty { font-size: 1.25rem; color: var(--ink-muted); }
132
133/* File upload row + status — only renders in edit mode */
134.pl-cover-upload-row {
135 display: flex; align-items: center; gap: .5rem;
136 margin-top: .35rem; flex-wrap: wrap;
137}
138.pl-btn-small {
139 background: var(--paper-2); color: var(--ink);
140 border: 1px solid var(--rule); border-radius: 999px;
141 padding: .35rem .75rem; font-size: .8rem;
142 cursor: pointer; transition: border-color 120ms;
143 -webkit-tap-highlight-color: transparent;
144}
145.pl-btn-small:hover { border-color: var(--accent); }
146.pl-cover-status { font-size: .8rem; color: var(--ink-muted); }
147.pl-cover-status.is-error { color: #dc2626; }
148.pl-cover-status.is-ok { color: #2a9d5e; }
149.pl-cover-note { font-size: .75rem; color: var(--ink-muted); margin-top: .25rem; display: block; }
150
151/* Section titles */
152.pl-section-title {
153 display: flex; align-items: baseline; gap: .5rem;
154 font-size: .8rem; font-weight: 600;
155 text-transform: uppercase; letter-spacing: .05em;
156 color: var(--ink-soft);
157 padding: .5rem 0;
158 margin-bottom: .5rem;
159 border-bottom: 1px solid var(--rule);
160 flex-shrink: 0;
161}
162.pl-section-title small {
163 font-size: .8em; font-weight: 400; text-transform: none; letter-spacing: 0;
164 color: var(--ink-muted);
165}
166.pl-track-count {
167 background: var(--accent); color: #fff;
168 padding: .1em .55em; border-radius: 999px; font-size: .75rem;
169 font-weight: 600; margin-left: .25rem;
170}
171
172/* Selected + available lists */
173.pl-selected-list,
174.pl-available-list {
175 flex: 1; overflow-y: auto; min-height: 0;
176 padding-right: .25rem;
177}
178.pl-empty {
179 color: var(--ink-muted); font-style: italic;
180 padding: 1.5rem; text-align: center; font-size: .9rem;
181}
182
183/* Search input on right pane */
184.pl-search-input {
185 width: 100%;
186 padding: .5rem .75rem;
187 border: 1px solid var(--rule); border-radius: 8px;
188 background: var(--paper); color: var(--ink);
189 font-family: var(--font-ui); font-size: .9rem;
190 margin-bottom: .5rem;
191 flex-shrink: 0;
192}
193
194/* Selected row (sortable) */
195.pl-row {
196 display: flex; align-items: center; gap: .5rem;
197 padding: .5rem;
198 border: 1px solid transparent;
199 border-radius: 8px;
200 margin-bottom: .25rem;
201 background: var(--paper-2);
202 user-select: none;
203}
204.pl-row.is-dragging { opacity: 0.5; }
205.pl-row.drop-above { box-shadow: 0 -2px 0 var(--accent); }
206.pl-row.drop-below { box-shadow: 0 2px 0 var(--accent); }
207.pl-row-handle {
208 cursor: grab; padding: .25rem .5rem;
209 color: var(--ink-muted);
210 touch-action: none;
211}
212.pl-row-handle:active { cursor: grabbing; }
213.pl-row-num {
214 width: 22px; text-align: right;
215 font-family: var(--font-mono, monospace);
216 font-size: .8rem; color: var(--ink-muted);
217}
218.pl-row-cover {
219 width: 32px; height: 32px; border-radius: 4px;
220 flex-shrink: 0;
221 background: var(--paper); border: 1px solid var(--rule);
222 display: inline-flex; align-items: center; justify-content: center;
223 overflow: hidden;
224}
225.pl-row-cover img { width: 100%; height: 100%; object-fit: cover; }
226.pl-row-cover-empty { color: var(--ink-muted); font-size: .8rem; }
227.pl-row-info {
228 flex: 1; min-width: 0;
229 display: flex; flex-direction: column;
230 font-size: .85rem;
231}
232.pl-row-title {
233 font-weight: 500;
234 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
235}
236.pl-row-artist {
237 font-size: .8em; color: var(--ink-soft);
238 white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
239}
240.pl-row-x {
241 background: transparent; border: 0;
242 width: 28px; height: 28px;
243 border-radius: 6px;
244 cursor: pointer;
245 color: var(--ink-muted);
246 font-size: 1.1rem;
247}
248.pl-row-x:hover { background: var(--paper); color: #dc2626; }
249
250/* Available row (clickable) */
251.pl-avail-row {
252 display: flex; align-items: center; gap: .5rem;
253 padding: .5rem; border-radius: 8px;
254 cursor: pointer; margin-bottom: .25rem;
255 transition: background 120ms ease;
256}
257.pl-avail-row:hover { background: var(--paper-2); }
258.pl-avail-row.is-added { opacity: .55; }
259.pl-avail-row.is-unplayable { opacity: .4; cursor: not-allowed; }
260.pl-avail-action {
261 width: 22px; height: 22px;
262 border-radius: 50%; display: flex; align-items: center; justify-content: center;
263 font-size: .9rem; font-weight: 600;
264 background: var(--paper-2); color: var(--ink-soft);
265 flex-shrink: 0;
266}
267.pl-avail-row.is-added .pl-avail-action {
268 background: var(--accent); color: #fff;
269}
270
271/* Cover-picker grid (sub-modal) */
272.pl-cover-picker-grid {
273 display: grid;
274 grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
275 gap: .5rem; max-height: 60vh; overflow-y: auto;
276}
277.pl-cover-picker-grid button {
278 border: 1px solid var(--rule); border-radius: 8px;
279 background: var(--paper-2); padding: 0; cursor: pointer;
280 aspect-ratio: 1; overflow: hidden;
281}
282.pl-cover-picker-grid button:hover { border-color: var(--accent); }
283.pl-cover-picker-grid img { width: 100%; height: 100%; object-fit: cover; display: block; }
284</style>
285
286<%# Het script staat in assets/js/mod/playlist-editor.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
Note: See TracBrowser for help on using the repository browser.