a simple web player for subsonic tinysub.devins.page
subsonic navidrome javascript
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: merge layout and components

didnt really make sense to have these separated in my opinion. ive also cleaned up the css heavily and change context menu to use id

intergrav aaf1c5fd 3ac5bb4b

+201 -213
+194 -107
src/css/components.css
··· 1 - /* button base styling (all buttons use flex for icon + text alignment) */ 1 + /* LAYOUT */ 2 + body { 3 + height: 100vh; 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 */ 14 + header { 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 + /* BASE STYLES */ 2 30 button { 3 31 background: none; 4 32 border: none; ··· 9 37 font: inherit; 10 38 } 11 39 12 - /* pulsing animation for currently playing track */ 40 + /* animations */ 13 41 @keyframes pulse { 14 42 0%, 15 43 100% { ··· 20 48 } 21 49 } 22 50 23 - /* sidebar library section headings */ 51 + /* SIDEBAR */ 52 + #sidebar { 53 + grid-area: sidebar; 54 + border-right: 1px solid var(--border); 55 + background: var(--bg-secondary); 56 + display: flex; 57 + flex-direction: column; 58 + min-height: 0; 59 + } 60 + 24 61 #sidebar h2 { 25 62 font-size: 1rem; 26 - margin-top: 0.5rem; 63 + margin-block-start: 0.5rem; 27 64 } 28 65 29 66 #sidebar h2 a { ··· 36 73 } 37 74 38 75 #sidebar li { 39 - margin-top: 0.5rem; 76 + margin-block-start: 0.5rem; 77 + } 78 + 79 + /* sidebar - library */ 80 + #sidebar #library { 81 + flex: 1; 82 + overflow-y: auto; 83 + min-height: 0; 84 + padding-inline: 1rem; 40 85 } 41 86 42 - /* tree item container (expand button + name + actions) */ 43 - .tree-item { 87 + /* sidebar - tree items */ 88 + #sidebar .tree-item { 44 89 display: flex; 45 90 align-items: center; 46 91 gap: 0.5rem; 47 92 } 48 93 49 - /* toggle button and item name (with art and text) */ 50 - .tree-toggle, 51 - .tree-name { 94 + #sidebar .tree-toggle, 95 + #sidebar .tree-name { 52 96 color: inherit; 53 97 text-decoration: none; 54 98 flex: 1; ··· 58 102 min-width: 0; 59 103 } 60 104 61 - /* ellipsis text overflow for item names */ 62 - .tree-toggle span, 63 - .tree-name span { 105 + #sidebar .tree-toggle span, 106 + #sidebar .tree-name span { 64 107 overflow: hidden; 65 108 text-overflow: ellipsis; 66 109 white-space: nowrap; 67 110 } 68 111 69 - /* disabled state for action buttons */ 70 - .tree-action.disabled { 112 + #sidebar .tree-action.disabled { 71 113 opacity: 0.5; 72 114 } 73 115 74 - /* artist-level art icons */ 75 - .tree-toggle img, 76 - .tree-name img { 116 + #sidebar .tree-toggle img, 117 + #sidebar .tree-name img { 77 118 width: var(--art-artist); 78 119 height: var(--art-artist); 79 120 aspect-ratio: 1 / 1; 80 121 object-fit: cover; 81 122 } 82 123 83 - /* album-level art icons (nested under artists) */ 84 - ul.nested > li .tree-toggle img, 85 - ul.nested > li .tree-name img { 124 + #sidebar ul.nested > li .tree-toggle img, 125 + #sidebar ul.nested > li .tree-name img { 86 126 width: var(--art-album); 87 127 height: var(--art-album); 88 128 } 89 129 90 - /* song art in queue table */ 91 - .queue-cover { 92 - width: var(--art-song); 93 - height: var(--art-song); 94 - aspect-ratio: 1 / 1; 95 - object-fit: cover; 96 - flex-shrink: 0; 97 - } 98 - 99 - /* favorite button toggle states */ 100 - .queue-favorite { 101 - opacity: 0.25; 102 - } 103 - 104 - .queue-favorite:hover { 105 - opacity: 0.7; 106 - } 107 - 108 - .queue-favorite.favorited { 109 - opacity: 1; 110 - } 111 - 112 - /* nested tree lists */ 113 - .nested { 130 + /* sidebar - nested tree lists */ 131 + #sidebar .nested { 114 132 list-style: none; 115 133 margin-inline-start: 1rem; 116 134 } 117 135 118 - .nested li { 119 - margin-top: 0.25rem; 136 + #sidebar .nested li { 137 + margin-block-start: 0.25rem; 120 138 } 121 139 122 - /* currently playing track info panel */ 123 - #now-playing { 140 + /* sidebar - now playing */ 141 + #sidebar #now-playing { 124 142 display: flex; 125 143 flex-direction: column; 126 144 align-items: center; ··· 130 148 flex-shrink: 0; 131 149 } 132 150 133 - #now-playing #cover-art { 151 + #sidebar #now-playing #cover-art { 134 152 width: var(--art-now-playing); 135 153 height: var(--art-now-playing); 136 154 object-fit: cover; 137 155 } 138 156 139 - #now-playing #track-info { 157 + #sidebar #now-playing #track-info { 140 158 text-align: center; 141 159 width: 100%; 142 160 } 143 161 144 - #now-playing #track-title { 162 + #sidebar #now-playing #track-title { 145 163 font-weight: bold; 146 164 overflow: hidden; 147 165 text-overflow: ellipsis; 148 166 white-space: nowrap; 149 167 } 150 168 151 - #now-playing #track-artist { 169 + #sidebar #now-playing #track-artist { 152 170 font-size: 0.8rem; 153 171 opacity: 0.75; 154 172 overflow: hidden; ··· 156 174 white-space: nowrap; 157 175 } 158 176 159 - #now-playing #track-lyric { 177 + #sidebar #now-playing #track-lyric { 160 178 font-size: 0.8rem; 161 179 opacity: 0.75; 162 180 } 163 181 164 - /* progress slider and shuffle button alignment */ 165 - #progress { 166 - flex: 1; 182 + /* QUEUE */ 183 + #queue { 184 + grid-area: main; 185 + overflow-y: auto; 186 + padding-inline: 1rem; 167 187 } 168 188 169 - #shuffle-btn { 170 - margin-inline-start: auto; 171 - } 172 - 173 - /* queue table container */ 174 - table { 189 + #queue #queue-table { 175 190 width: 100%; 176 191 border-collapse: collapse; 177 192 table-layout: fixed; 178 - margin-top: 0.5rem; 193 + margin-block-start: 0.5rem; 179 194 } 180 195 181 - th, 182 - td { 196 + #queue #queue-table th, 197 + #queue #queue-table td { 183 198 text-align: left; 184 - vertical-align: middle; 185 199 overflow: hidden; 186 200 text-overflow: ellipsis; 187 201 white-space: nowrap; 188 202 } 189 203 190 - /* actions column (6th) in queue table */ 191 - #queue-table td:nth-child(6) { 204 + /* queue - cover art column */ 205 + #queue #queue-table th:nth-child(1), 206 + #queue #queue-table td:nth-child(1) { 207 + width: calc(var(--art-song) * 2); 208 + } 209 + 210 + /* queue - duration column */ 211 + #queue #queue-table th:nth-child(5), 212 + #queue #queue-table td:nth-child(5) { 213 + width: 6rem; 214 + } 215 + 216 + /* queue - actions column */ 217 + #queue #queue-table th:nth-child(6), 218 + #queue #queue-table td:nth-child(6) { 219 + max-width: 160px; 192 220 overflow: visible; 193 221 white-space: normal; 194 222 } 195 223 196 - #queue-table td:nth-child(6) button { 224 + #queue #queue-table td:nth-child(6) button { 197 225 margin-inline-end: 0.5rem; 198 226 } 199 227 200 - #queue-table td:nth-child(6) button:last-child { 228 + #queue #queue-table td:nth-child(6) button:last-child { 201 229 margin-inline-end: 0; 202 230 } 203 231 204 - /* alternating row backgrounds */ 205 - tbody tr.stripe { 232 + /* queue - rows */ 233 + #queue #queue-table tbody tr.stripe { 206 234 background: var(--bg-secondary); 207 235 } 208 236 209 - /* currently playing track highlighting with pulse animation */ 210 - tbody tr.currently-playing { 237 + #queue #queue-table tbody tr.currently-playing { 211 238 background: var(--playing); 212 239 animation: pulse 4s linear infinite; 213 240 } 214 241 215 - /* cover art column width */ 216 - #queue-table th:nth-child(1), 217 - #queue-table td:nth-child(1) { 218 - width: calc(var(--art-song) * 2); 242 + #queue #queue-table tbody tr.dragging { 243 + opacity: 0.5; 244 + } 245 + 246 + #queue #queue-table tbody tr.selected { 247 + background: Highlight; 248 + color: HighlightText; 249 + } 250 + 251 + #queue #queue-table tbody tr.drag-over-above { 252 + border-block-start: 2px solid currentColor; 219 253 } 220 254 221 - /* duration column width */ 222 - #queue-table th:nth-child(5), 223 - #queue-table td:nth-child(5) { 224 - width: 6rem; 255 + #queue #queue-table tbody tr.drag-over-below { 256 + border-block-end: 2px solid currentColor; 225 257 } 226 258 227 - /* drag and drop states */ 228 - tbody tr.dragging { 229 - opacity: 0.5; 259 + /* queue - row items */ 260 + #queue #queue-table .queue-cover { 261 + width: var(--art-song); 262 + height: var(--art-song); 263 + aspect-ratio: 1 / 1; 264 + object-fit: cover; 265 + flex-shrink: 0; 230 266 } 231 267 232 - tbody tr.selected { 233 - background: var(--selection-color); 234 - color: white; 268 + #queue #queue-table .queue-favorite { 269 + opacity: 0.25; 235 270 } 236 271 237 - tbody tr.drag-over-above { 238 - border-top: 2px solid currentColor; 239 - background: var(--bg-secondary); 272 + #queue #queue-table .queue-favorite:hover { 273 + opacity: 0.7; 240 274 } 241 275 242 - tbody tr.drag-over-below { 243 - border-bottom: 2px solid currentColor; 244 - background: var(--bg-secondary); 276 + #queue #queue-table .queue-favorite.favorited { 277 + opacity: 1; 245 278 } 246 279 247 - /* floating context menu for queue actions */ 248 - .context-menu { 280 + /* ACTIONS */ 281 + #actions { 282 + grid-area: footer; 283 + display: flex; 284 + align-items: center; 285 + gap: 1rem; 286 + padding: 0.5rem 1rem; 287 + border-top: 1px solid var(--border-subtle); 288 + background: var(--bg-tertiary); 289 + } 290 + 291 + #actions #shuffle-btn { 292 + margin-inline-start: auto; 293 + } 294 + 295 + /* CONTEXT MENU */ 296 + #context-menu { 249 297 position: fixed; 250 298 background: var(--bg-context-menu); 251 299 backdrop-filter: blur(16px); ··· 256 304 min-width: 10rem; 257 305 } 258 306 259 - .context-menu-item { 307 + #context-menu .context-menu-item { 260 308 display: block; 261 309 width: 100%; 262 310 padding: 0.25rem 1rem; ··· 265 313 text-align: left; 266 314 } 267 315 268 - .context-menu-item:hover { 316 + #context-menu .context-menu-item:hover { 269 317 background: Highlight; 270 318 color: HighlightText; 271 319 } 272 320 273 - /* error text color */ 274 - .error { 275 - color: var(--error-color); 276 - } 277 - 278 - /* modals */ 321 + /* MODAL */ 279 322 .modal { 280 323 position: fixed; 281 324 inset: 0; ··· 301 344 margin-bottom: 1rem; 302 345 } 303 346 304 - /* form and settings groups */ 347 + /* FORMS & SETTINGS */ 305 348 .form-group, 306 349 .settings-group { 307 350 margin-bottom: 1.5rem; ··· 313 356 margin-bottom: 0.5rem; 314 357 } 315 358 316 - /* famfamfam-silk icons: force pixelated rendering for retina displays */ 359 + /* UTILITIES */ 360 + /* error */ 361 + .error { 362 + color: var(--error-color); 363 + } 364 + 365 + /* famfamfam-silk icons - force pixelated rendering for retina displays */ 317 366 img[src*="famfamfam-silk"] { 318 367 image-rendering: pixelated; 319 368 } 369 + 370 + /* MOBILE */ 371 + @media (max-width: 768px) { 372 + /* layout */ 373 + body { 374 + grid-template-columns: 1fr; 375 + grid-template-rows: 1fr 1fr auto auto; 376 + grid-template-areas: 377 + "main" 378 + "sidebar" 379 + "header" 380 + "footer"; 381 + } 382 + 383 + /* queue - hide album and duration columns */ 384 + th:nth-child(4), 385 + th:nth-child(5), 386 + td:nth-child(4), 387 + td:nth-child(5) { 388 + display: none; 389 + } 390 + 391 + #sidebar { 392 + border-right: none; 393 + border-top: 1px solid var(--border); 394 + overflow: hidden; 395 + } 396 + 397 + /* sidebar - now playing cover art */ 398 + #now-playing #cover-art { 399 + display: none; 400 + } 401 + 402 + /* footer - button labels */ 403 + footer button span { 404 + display: none; 405 + } 406 + }
-98
src/css/layout.css
··· 1 - /* main layout grid (desktop: 3 columns, 3 rows) */ 2 - body { 3 - height: 100vh; 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 - /* playback controls (prev, play, next, progress, time display) */ 14 - header { 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 - /* library browser and track info panel */ 26 - #sidebar { 27 - grid-area: sidebar; 28 - border-right: 1px solid var(--border); 29 - background: var(--bg-secondary); 30 - display: flex; 31 - flex-direction: column; 32 - min-height: 0; 33 - } 34 - 35 - /* container for tree of artists and playlists */ 36 - #library { 37 - flex: 1; 38 - overflow-y: auto; 39 - min-height: 0; 40 - padding-inline: 1rem; 41 - } 42 - 43 - /* song queue table */ 44 - main { 45 - grid-area: main; 46 - overflow-y: auto; 47 - padding-inline: 1rem; 48 - } 49 - 50 - /* action buttons and version info */ 51 - footer { 52 - grid-area: footer; 53 - display: flex; 54 - align-items: center; 55 - gap: 1rem; 56 - padding: 0.5rem 1rem; 57 - border-top: 1px solid var(--border-subtle); 58 - background: var(--bg-tertiary); 59 - } 60 - 61 - /* mobile layout */ 62 - @media (max-width: 768px) { 63 - body { 64 - grid-template-columns: 1fr; 65 - grid-template-rows: 1fr 1fr auto auto; 66 - grid-template-areas: 67 - "main" 68 - "sidebar" 69 - "header" 70 - "footer"; 71 - } 72 - 73 - /* hide less important columns on mobile */ 74 - th:nth-child(3), 75 - th:nth-child(4), 76 - th:nth-child(5), 77 - td:nth-child(3), 78 - td:nth-child(4), 79 - td:nth-child(5) { 80 - display: none; 81 - } 82 - 83 - #sidebar { 84 - border-right: none; 85 - border-top: 1px solid var(--border); 86 - overflow: hidden; 87 - } 88 - 89 - /* hide now playing cover art on mobile */ 90 - #now-playing #cover-art { 91 - display: none; 92 - } 93 - 94 - /* hide button labels on mobile */ 95 - footer button span { 96 - display: none; 97 - } 98 - }
+3 -4
src/index.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 6 <title>tinysub</title> 7 7 <link rel="stylesheet" href="css/base.css" /> 8 - <link rel="stylesheet" href="css/layout.css" /> 9 8 <link rel="stylesheet" href="css/components.css" /> 10 9 </head> 11 10 <body> ··· 123 122 <button id="close-settings-btn">close</button> 124 123 </div> 125 124 </div> 126 - <header> 125 + <header id="playback"> 127 126 <audio id="player" crossorigin="anonymous"></audio> 128 127 <button id="prev-btn" aria-label="previous"> 129 128 <img ··· 170 169 </div> 171 170 </aside> 172 171 173 - <main> 172 + <main id="queue"> 174 173 <table id="queue-table"> 175 174 <thead> 176 175 <tr> ··· 186 185 </table> 187 186 </main> 188 187 189 - <footer> 188 + <footer id="actions"> 190 189 <span 191 190 ><a href="https://tangled.org/devins.page/tinysub">tinysub</a> 192 191 1.5.0</span
+1 -1
src/js/constants.js
··· 6 6 DRAG_OVER_ABOVE: "drag-over-above", 7 7 DRAG_OVER_BELOW: "drag-over-below", 8 8 CURRENTLY_PLAYING: "currently-playing", 9 - CONTEXT_MENU: "context-menu", 10 9 CONTEXT_MENU_ITEM: "context-menu-item", 11 10 TREE_TOGGLE: "tree-toggle", 12 11 TREE_NAME: "tree-name", ··· 32 31 const DOM_IDS = { 33 32 AUTH_MODAL: "auth-modal", 34 33 LOGIN_FORM: "login-form", 34 + CONTEXT_MENU: "context-menu", 35 35 QUEUE_TABLE: "queue-table", 36 36 AUTH_ERROR: "auth-error", 37 37 SERVER_INPUT: "server",
+1 -1
src/js/contextmenu.js
··· 22 22 cleanupContextMenu(); 23 23 24 24 contextMenuEl = createElement("div", { 25 - className: CLASSES.CONTEXT_MENU, 25 + attributes: { id: DOM_IDS.CONTEXT_MENU }, 26 26 }); 27 27 28 28 Object.entries(items).forEach(([label, handler]) => {
+1 -1
src/js/events.js
··· 278 278 document.addEventListener("click", (e) => { 279 279 if ( 280 280 !e.target.closest(`#${DOM_IDS.QUEUE_TABLE}`) && 281 - !e.target.closest(".context-menu") 281 + !e.target.closest(`#${DOM_IDS.CONTEXT_MENU}`) 282 282 ) { 283 283 clearSelection(); 284 284 }
+1 -1
src/js/queue.js
··· 229 229 const coverCell = document.createElement("td"); 230 230 if (song.coverArt && state.settings.artSong > 0) { 231 231 const img = document.createElement("img"); 232 - img.className = "queue-cover"; 232 + img.className = CLASSES.QUEUE_COVER; 233 233 coverCell.appendChild(img); 234 234 loadCachedImage(img, song.coverArt, "artSong"); 235 235 }