a simple web player for subsonic tinysub.devins.page
subsonic navidrome javascript
at main 518 lines 9.2 kB view raw
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}