Player: shuffle + repeat on the mini-player (parity with modal)

The sidebar mini-player had prev/play/next/stop/expand but not the two
set-and-forget controls you reach for without opening the full view. Added
shuffle + repeat (3-mode, with a repeat-one badge) to the mini-controls.

State stays in sync both ways: handleNpShuffle/handleNpRepeat now call a shared
syncShuffleRepeatUI() that reflects state onto BOTH the modal and mini buttons,
so toggling in either place updates the other. Mini buttons reuse the same
handlers. Accent-active styling via --accent-light-rgb.

JS clean; CSS balance consistent with HEAD.
This commit is contained in:
BoulderBadgeDad 2026-05-30 14:57:04 -07:00
parent 1e514693f1
commit ab2b5c64f4
3 changed files with 76 additions and 6 deletions

View file

@ -8066,6 +8066,9 @@
<div class="artist-name" id="artist-name">Unknown Artist</div>
</div>
<div class="mini-player-controls">
<button class="mini-toggle-btn" id="mini-shuffle-btn" title="Shuffle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="13" height="13"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></svg>
</button>
<button class="mini-nav-btn" id="mini-prev-btn" title="Previous" disabled>
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
</button>
@ -8076,6 +8079,10 @@
<button class="mini-nav-btn" id="mini-next-btn" title="Next" disabled>
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
</button>
<button class="mini-toggle-btn" id="mini-repeat-btn" title="Repeat">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="13" height="13"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
<span class="mini-repeat-one" id="mini-repeat-one-badge" style="display:none">1</span>
</button>
<button class="stop-button" id="stop-button" disabled>
<svg viewBox="0 0 24 24" fill="currentColor" width="10" height="10"><rect x="6" y="6" width="12" height="12" rx="1.5"/></svg>
</button>

View file

@ -61,6 +61,12 @@ function initializeMediaPlayer() {
if (miniPrevBtn) miniPrevBtn.addEventListener('click', (e) => { e.stopPropagation(); playPreviousInQueue(); });
if (miniNextBtn) miniNextBtn.addEventListener('click', (e) => { e.stopPropagation(); playNextInQueue(); });
// Mini shuffle / repeat — share the modal handlers (which now sync both UIs)
const miniShuffleBtn = document.getElementById('mini-shuffle-btn');
const miniRepeatBtn = document.getElementById('mini-repeat-btn');
if (miniShuffleBtn) miniShuffleBtn.addEventListener('click', (e) => { e.stopPropagation(); handleNpShuffle(); });
if (miniRepeatBtn) miniRepeatBtn.addEventListener('click', (e) => { e.stopPropagation(); handleNpRepeat(); });
// Restore a previously-saved queue (does not auto-play)
npRestoreQueue();
}
@ -2064,15 +2070,32 @@ function updateNpMuteIcon() {
if (muteBtn) muteBtn.classList.toggle('muted', npMuted);
}
// Reflect shuffle/repeat state on BOTH the modal and mini-player buttons.
function syncShuffleRepeatUI() {
const npShuffle = document.getElementById('np-shuffle-btn');
const miniShuffle = document.getElementById('mini-shuffle-btn');
if (npShuffle) npShuffle.classList.toggle('active', npShuffleOn);
if (miniShuffle) miniShuffle.classList.toggle('active', npShuffleOn);
const repeatOn = npRepeatMode !== 'off';
const repeatOne = npRepeatMode === 'one';
const npRepeat = document.getElementById('np-repeat-btn');
const miniRepeat = document.getElementById('mini-repeat-btn');
if (npRepeat) npRepeat.classList.toggle('active', repeatOn);
if (miniRepeat) miniRepeat.classList.toggle('active', repeatOn);
const npBadge = document.getElementById('np-repeat-one-badge');
const miniBadge = document.getElementById('mini-repeat-one-badge');
if (npBadge) npBadge.classList.toggle('hidden', !repeatOne);
if (miniBadge) miniBadge.style.display = repeatOne ? '' : 'none';
}
function handleNpShuffle() {
npShuffleOn = !npShuffleOn;
const btn = document.getElementById('np-shuffle-btn');
if (btn) btn.classList.toggle('active', npShuffleOn);
syncShuffleRepeatUI();
updateNpPrevNextButtons();
}
function handleNpRepeat() {
const badge = document.getElementById('np-repeat-one-badge');
if (npRepeatMode === 'off') {
npRepeatMode = 'all';
if (audioPlayer) audioPlayer.loop = false;
@ -2083,9 +2106,7 @@ function handleNpRepeat() {
npRepeatMode = 'off';
if (audioPlayer) audioPlayer.loop = false;
}
const btn = document.getElementById('np-repeat-btn');
if (btn) btn.classList.toggle('active', npRepeatMode !== 'off');
if (badge) badge.classList.toggle('hidden', npRepeatMode !== 'one');
syncShuffleRepeatUI();
updateNpPrevNextButtons();
}

View file

@ -693,6 +693,48 @@ body {
cursor: not-allowed;
}
/* Mini shuffle / repeat toggle buttons (parity with the modal). Smaller than
nav buttons; accent when active; repeat-one shows a tiny badge. */
.mini-toggle-btn {
position: relative;
width: 22px;
height: 22px;
border-radius: 50%;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.38);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.18s ease, background 0.18s ease;
padding: 0;
flex-shrink: 0;
}
.mini-toggle-btn:hover {
color: rgba(255, 255, 255, 0.85);
background: rgba(255, 255, 255, 0.08);
}
.mini-toggle-btn.active {
color: rgb(var(--accent-light-rgb));
}
.mini-repeat-one {
position: absolute;
bottom: -1px;
right: -1px;
font-size: 8px;
font-weight: 800;
line-height: 1;
background: rgb(var(--accent-light-rgb));
color: #06140b;
border-radius: 50%;
width: 10px;
height: 10px;
display: flex;
align-items: center;
justify-content: center;
}
/* Controls cluster in the mini player */
.mini-player-controls {
display: flex;