a simple web player for subsonic
tinysub.devins.page
subsonic
navidrome
javascript
1/* LAYOUT */
2body {
3 height: 100dvh;
4 display: grid;
5 grid-template-columns: 20rem 1fr;
6 grid-template-rows: 1fr auto auto;
7 grid-template-areas:
8 "sidebar main"
9 "header header"
10 "footer footer";
11}
12
13/* HEADER */
14header {
15 grid-area: header;
16 display: flex;
17 align-items: center;
18 justify-content: center;
19 gap: 0.5rem;
20 padding: 0.5rem;
21 border-top: 1px solid var(--border);
22 background: var(--bg-tertiary);
23}
24
25#progress {
26 flex: 1;
27}
28
29#playback #loop-btn {
30 opacity: 0.5;
31}
32
33#playback #loop-btn.active {
34 opacity: 1;
35}
36
37/* BASE STYLES */
38button {
39 background: none;
40 border: none;
41 display: inline-flex;
42 align-items: center;
43 gap: 0.5rem;
44 color: inherit;
45 font: inherit;
46}
47
48@keyframes pulse {
49 0%,
50 100% {
51 background-color: var(--playing);
52 }
53 50% {
54 background-color: var(--playing-pulse);
55 }
56}
57
58/* SIDEBAR */
59#sidebar {
60 grid-area: sidebar;
61 border-right: 1px solid var(--border);
62 background: var(--bg-secondary);
63 display: flex;
64 flex-direction: column;
65 min-height: 0;
66}
67
68#sidebar h2 {
69 font-size: 1rem;
70 margin-block-start: 0.5rem;
71}
72
73#sidebar h2 a {
74 color: inherit;
75 text-decoration: none;
76}
77
78#sidebar ul {
79 list-style: none;
80}
81
82#sidebar li {
83 margin-block-start: 0.5rem;
84}
85
86/* sidebar - library */
87#sidebar #library {
88 flex: 1;
89 overflow-y: auto;
90 min-height: 0;
91 padding-inline: 1rem;
92}
93
94/* sidebar - library - tree items */
95#sidebar #library .tree-item {
96 display: flex;
97 align-items: center;
98 gap: 0.5rem;
99}
100
101#sidebar #library .tree-toggle,
102#sidebar #library .tree-name {
103 color: inherit;
104 text-decoration: none;
105 flex: 1;
106 display: flex;
107 gap: 0.5rem;
108 align-items: center;
109 min-width: 0;
110}
111
112#sidebar #library .tree-toggle span,
113#sidebar #library .tree-name span {
114 overflow: hidden;
115 text-overflow: ellipsis;
116 white-space: nowrap;
117}
118
119#sidebar #library .tree-action.disabled {
120 opacity: 0.5;
121}
122
123#sidebar #library .tree-toggle img,
124#sidebar #library .tree-name img {
125 width: var(--art-artist);
126 height: var(--art-artist);
127 aspect-ratio: 1 / 1;
128 object-fit: cover;
129 flex-shrink: 0;
130}
131
132#sidebar #library ul.nested > li .tree-toggle img,
133#sidebar #library ul.nested > li .tree-name img {
134 width: var(--art-album);
135 height: var(--art-album);
136}
137
138#sidebar #library .nested,
139#sidebar #library .nested-songs {
140 list-style: none;
141 margin-inline-start: 1rem;
142}
143
144#sidebar #library .nested li {
145 margin-block-start: 0.25rem;
146}
147
148#sidebar #library #library-search {
149 width: 100%;
150 margin-block-start: 0.5rem;
151}
152
153#sidebar #library .tree-toggle.focused,
154#sidebar #library .tree-name.focused,
155#sidebar #library .section-toggle.focused {
156 background: Highlight;
157 color: HighlightText;
158}
159
160#sidebar #library:not(:focus-within) .tree-toggle.focused,
161#sidebar #library:not(:focus-within) .tree-name.focused,
162#sidebar #library:not(:focus-within) .section-toggle.focused {
163 background: GrayText;
164}
165
166/* sidebar - now playing */
167#sidebar #now-playing {
168 display: flex;
169 flex-direction: column;
170 align-items: center;
171 gap: 0.75rem;
172 padding: 1rem;
173 border-top: 1px solid var(--border-subtle);
174 flex-shrink: 0;
175}
176
177#sidebar #now-playing #cover-art {
178 width: var(--art-now-playing);
179 height: var(--art-now-playing);
180 object-fit: cover;
181}
182
183#sidebar #now-playing #track-info {
184 text-align: center;
185 width: 100%;
186}
187
188#sidebar #now-playing #track-title {
189 font-weight: bold;
190 overflow: hidden;
191 text-overflow: ellipsis;
192 white-space: nowrap;
193}
194
195#sidebar #now-playing #track-artist {
196 font-size: 0.8rem;
197 opacity: 0.75;
198 overflow: hidden;
199 text-overflow: ellipsis;
200 white-space: nowrap;
201}
202
203#sidebar #now-playing #track-lyric {
204 font-size: 0.8rem;
205 opacity: 0.75;
206}
207
208/* QUEUE */
209#queue {
210 grid-area: main;
211 overflow-y: auto;
212 padding-inline: 1rem;
213}
214
215#queue #queue-table {
216 width: 100%;
217 border-collapse: collapse;
218 table-layout: fixed;
219 margin-block-start: 0.5rem;
220}
221
222#queue #queue-table th,
223#queue #queue-table td {
224 text-align: left;
225 overflow: hidden;
226 text-overflow: ellipsis;
227 white-space: nowrap;
228}
229
230/* queue - cover art column */
231#queue #queue-table th:nth-child(1),
232#queue #queue-table td:nth-child(1) {
233 width: calc(var(--art-song) * 1.5);
234}
235
236/* queue - duration column */
237#queue #queue-table th:nth-child(5),
238#queue #queue-table td:nth-child(5) {
239 width: 6rem;
240}
241
242/* queue - actions column */
243#queue #queue-table th:nth-child(6),
244#queue #queue-table td:nth-child(6) {
245 text-align: right;
246 overflow: visible;
247 white-space: normal;
248}
249
250#queue #queue-table td:nth-child(6) button {
251 margin-inline-start: 0.5rem;
252}
253
254/* queue - rows */
255#queue #queue-table tbody tr.stripe {
256 background: var(--bg-secondary);
257}
258
259#queue #queue-table tbody tr.currently-playing {
260 background: var(--playing);
261 animation: pulse 4s linear infinite;
262}
263
264#queue #queue-table tbody tr.dragging {
265 opacity: 0.5;
266}
267
268#queue #queue-table tbody tr.selected {
269 background: Highlight;
270 color: HighlightText;
271}
272
273#queue:not(:focus-within) #queue-table tbody tr.selected {
274 background: GrayText;
275}
276
277#queue #queue-table tbody tr.drag-over-above {
278 border-block-start: 2px solid currentColor;
279}
280
281#queue #queue-table tbody tr.drag-over-below {
282 border-block-end: 2px solid currentColor;
283}
284
285/* queue - row items */
286#queue #queue-table .queue-cover {
287 width: var(--art-song);
288 height: var(--art-song);
289 aspect-ratio: 1 / 1;
290 object-fit: cover;
291 flex-shrink: 0;
292}
293
294#queue #queue-table .queue-favorite {
295 opacity: 0.25;
296}
297
298#queue #queue-table .queue-favorite:hover {
299 opacity: 0.5;
300}
301
302#queue #queue-table .queue-favorite.favorited {
303 opacity: 1;
304}
305
306#queue #queue-table .queue-rating-star {
307 opacity: 0.25;
308}
309
310#queue #queue-table .queue-rating-star:hover {
311 opacity: 0.5;
312}
313
314#queue #queue-table .queue-rating-star.rated {
315 opacity: 1;
316}
317
318#queue #queue-table .queue-play,
319#queue #queue-table .queue-play-next,
320#queue #queue-table .queue-move-up,
321#queue #queue-table .queue-move-down {
322 display: none;
323}
324
325/* FOOTER */
326#footer {
327 grid-area: footer;
328 display: flex;
329 align-items: center;
330 justify-content: space-between;
331 gap: 1rem;
332 padding: 0.5rem 1rem;
333 border-top: 1px solid var(--border-subtle);
334 background: var(--bg-tertiary);
335}
336
337#footer #actions {
338 display: flex;
339 align-items: center;
340 gap: 1rem;
341}
342
343/* CONTEXT MENU */
344#context-menu {
345 position: fixed;
346 background: var(--bg-context-menu);
347 backdrop-filter: blur(16px);
348 border: 1px solid var(--border);
349 border-radius: 0.25rem;
350 box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
351 z-index: 1000;
352 min-width: 10rem;
353}
354
355#context-menu .context-menu-item {
356 display: block;
357 width: 100%;
358 padding: 0.25rem 1rem;
359 background: none;
360 border: none;
361 text-align: left;
362}
363
364#context-menu .context-menu-item:hover {
365 background: Highlight;
366 color: HighlightText;
367}
368
369#context-menu .context-menu-item.focused {
370 background: Highlight;
371 color: HighlightText;
372}
373
374/* MODAL */
375.modal {
376 position: fixed;
377 inset: 0;
378 background: rgba(0, 0, 0, 0.75);
379 display: flex;
380 align-items: center;
381 justify-content: center;
382 z-index: 2000;
383}
384
385.modal.hidden {
386 display: none;
387}
388
389.modal-content {
390 background: Menu;
391 border: 1px solid var(--border);
392 padding: 1rem;
393 max-height: 100%;
394 min-width: 24rem;
395 overflow-y: auto;
396 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
397 display: flex;
398 flex-direction: column;
399 gap: 1rem;
400}
401
402.modal-actions {
403 display: flex;
404 gap: 0.5rem;
405 justify-content: end;
406}
407
408.modal-actions button {
409 background: ButtonFace;
410 padding: 0.5rem 1rem;
411}
412
413#keyboard-help-modal .shortcuts-grid {
414 display: grid;
415 grid-template-columns: 1fr 1fr;
416 gap: 1rem 2rem;
417}
418
419#keyboard-help-modal .shortcut-section-group {
420 display: flex;
421 flex-direction: column;
422 gap: 0.5rem;
423}
424
425#keyboard-help-modal .shortcuts-items-grid {
426 display: grid;
427 grid-template-columns: 10rem 1fr;
428 gap: 0.5rem 1rem;
429}
430
431#keyboard-help-modal kbd {
432 background: ButtonFace;
433 padding: 0.25rem 0.5rem;
434 font-family: ui-monospace, monospace;
435}
436
437/* form groups */
438.form-group {
439 display: flex;
440 flex-direction: column;
441 gap: 1rem;
442}
443
444.modal-content form {
445 display: flex;
446 flex-direction: column;
447 gap: 1rem;
448}
449
450/* UTILITIES */
451/* danger */
452.danger {
453 color: var(--error-color);
454}
455
456button.danger {
457 background-color: var(--error-color);
458 color: white;
459}
460
461/* famfamfam-silk icons - force pixelated rendering for retina displays */
462img[src*="famfamfam-silk"] {
463 image-rendering: pixelated;
464}
465
466/* MEDIA QUERIES */
467@media (max-width: 768px) {
468 /* layout */
469 body {
470 grid-template-columns: 1fr;
471 grid-template-rows: 1fr 1fr auto auto;
472 grid-template-areas:
473 "main"
474 "sidebar"
475 "header"
476 "footer";
477 }
478
479 /* queue - hide album and duration columns */
480 #queue-table th:nth-child(4),
481 #queue-table th:nth-child(5),
482 #queue-table td:nth-child(4),
483 #queue-table td:nth-child(5) {
484 display: none;
485 }
486
487 #sidebar {
488 border-right: none;
489 border-top: 1px solid var(--border);
490 overflow: hidden;
491 }
492
493 /* sidebar - now playing cover art */
494 #now-playing #cover-art {
495 display: none;
496 }
497
498 /* footer - button labels */
499 footer button span {
500 display: none;
501 }
502}
503
504@media (pointer: coarse) {
505 /* queue - show action buttons */
506 #queue #queue-table .queue-play,
507 #queue #queue-table .queue-play-next,
508 #queue #queue-table .queue-move-up,
509 #queue #queue-table .queue-move-down {
510 display: inline-block;
511 }
512
513 /* queue - hide artist column */
514 #queue-table th:nth-child(3),
515 #queue-table td:nth-child(3) {
516 display: none;
517 }
518}