feat: move shuffle button from player to queue (#693)

- Removes shuffle button from PlaybackControls
- Adds shuffle button to Queue header next to clear button
- Same functionality, just relocated to reduce player clutter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

authored by zzstoatzz.io Claude Opus 4.5 and committed by GitHub e2b53004 8c2cf0cc

Changed files
+55 -75
frontend
src
lib
+54 -6
frontend/src/lib/components/Queue.svelte
··· 114 114 <div class="queue"> 115 115 <div class="queue-header"> 116 116 <h2>queue</h2> 117 - {#if upcoming.length > 0} 117 + <div class="queue-actions"> 118 118 <button 119 - class="clear-btn" 120 - onclick={() => queue.clearUpNext()} 121 - title="clear upcoming tracks" 119 + class="shuffle-btn" 120 + class:active={queue.shuffle} 121 + onclick={() => queue.toggleShuffle()} 122 + title={queue.shuffle ? 'disable shuffle' : 'enable shuffle'} 122 123 > 123 - clear queue 124 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 125 + <polyline points="16 3 21 3 21 8"></polyline> 126 + <line x1="4" y1="20" x2="21" y2="3"></line> 127 + <polyline points="21 16 21 21 16 21"></polyline> 128 + <line x1="15" y1="15" x2="21" y2="21"></line> 129 + <line x1="4" y1="4" x2="9" y2="9"></line> 130 + </svg> 124 131 </button> 125 - {/if} 132 + {#if upcoming.length > 0} 133 + <button 134 + class="clear-btn" 135 + onclick={() => queue.clearUpNext()} 136 + title="clear upcoming tracks" 137 + > 138 + clear 139 + </button> 140 + {/if} 141 + </div> 126 142 </div> 127 143 128 144 <div class="queue-body"> ··· 259 275 display: flex; 260 276 justify-content: space-between; 261 277 align-items: center; 278 + } 279 + 280 + .queue-actions { 281 + display: flex; 282 + align-items: center; 283 + gap: 0.5rem; 284 + } 285 + 286 + .shuffle-btn { 287 + display: flex; 288 + align-items: center; 289 + justify-content: center; 290 + width: 32px; 291 + height: 32px; 292 + padding: 0; 293 + background: transparent; 294 + border: 1px solid var(--border-subtle); 295 + color: var(--text-tertiary); 296 + border-radius: var(--radius-sm); 297 + cursor: pointer; 298 + transition: all 0.15s ease; 299 + } 300 + 301 + .shuffle-btn:hover { 302 + color: var(--text-secondary); 303 + border-color: var(--border-default); 304 + background: var(--bg-secondary); 305 + } 306 + 307 + .shuffle-btn.active { 308 + color: var(--accent); 309 + border-color: var(--accent); 262 310 } 263 311 264 312 .clear-btn {
+1 -69
frontend/src/lib/components/player/PlaybackControls.svelte
··· 151 151 </svg> 152 152 </button> 153 153 154 - <div class="playback-options"> 155 - <button 156 - class="option-btn" 157 - class:active={queue.shuffle} 158 - onclick={() => queue.toggleShuffle()} 159 - title={queue.shuffle ? 'disable shuffle' : 'enable shuffle'} 160 - > 161 - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 162 - <polyline points="16 3 21 3 21 8"></polyline> 163 - <line x1="4" y1="20" x2="21" y2="3"></line> 164 - <polyline points="21 16 21 21 16 21"></polyline> 165 - <line x1="15" y1="15" x2="21" y2="21"></line> 166 - <line x1="4" y1="4" x2="9" y2="9"></line> 167 - </svg> 168 - </button> 169 - </div> 170 - 171 154 <div class="time-control"> 172 155 <span class="time">{formattedCurrentTime}</span> 173 156 <input ··· 264 247 .control-btn.disabled { 265 248 opacity: 0.4; 266 249 pointer-events: none; 267 - } 268 - 269 - .playback-options { 270 - display: flex; 271 - align-items: center; 272 - gap: 0.5rem; 273 - } 274 - 275 - .option-btn { 276 - background: transparent; 277 - border: 1px solid var(--border-default); 278 - color: var(--text-secondary); 279 - cursor: pointer; 280 - width: 40px; 281 - height: 40px; 282 - display: flex; 283 - align-items: center; 284 - justify-content: center; 285 - border-radius: var(--radius-base); 286 - transition: all 0.2s; 287 - position: relative; 288 - } 289 - 290 - .option-btn svg { 291 - width: 20px; 292 - height: 20px; 293 - } 294 - 295 - .option-btn:hover { 296 - color: var(--text-primary); 297 - border-color: var(--accent); 298 - } 299 - 300 - .option-btn.active { 301 - color: var(--accent); 302 - border-color: var(--accent); 303 250 } 304 251 305 252 .time-control { ··· 472 419 height: 32px; 473 420 } 474 421 475 - .playback-options { 476 - grid-row: 2; 477 - grid-column: 1; 478 - } 479 - 480 - .option-btn { 481 - width: 36px; 482 - height: 36px; 483 - } 484 - 485 - .option-btn svg { 486 - width: 18px; 487 - height: 18px; 488 - } 489 - 490 422 .time-control { 491 423 grid-row: 2; 492 - grid-column: 2 / 7; 424 + grid-column: 1 / 7; 493 425 } 494 426 495 427 .time {