Move media player from sidebar to floating bottom-right mini player

The sidebar player was a poor use of vertical real estate and created the
collapsed-state layout issues. The mini player is now a fixed 360px widget
at bottom-right (above the bell/help buttons), matching the convention of
most streaming apps.

Changes:
- Removed media player from sidebar; sidebar spacer now pushes support/version
  section to bottom as before
- New .mini-player-body horizontal layout: album art | track info | controls
- Added prev/next skip buttons (mini-nav-btn) with same skip logic as the
  Now Playing modal; updateNpPrevNextButtons() now syncs both sets
- .media-player.idle now display:none (widget hides entirely when no track)
- Progress bar is a flush full-width line at the top of the widget
- Volume slider kept as hidden DOM element for JS compatibility; volume is
  set via the Now Playing modal
- Toast container moved up to bottom:174px to stay above the mini player
- expand-hint button updated to four-corner expand icon, opens NP modal
- All volumeSlider and click-exclusion references updated for null-safety
This commit is contained in:
Broque Thomas 2026-04-14 19:50:18 -07:00
parent 276f70ab9a
commit 76ff98261b
3 changed files with 141 additions and 117 deletions

View file

@ -255,73 +255,6 @@
<!-- Spacer -->
<div class="sidebar-spacer"></div>
<!-- Media Player Section -->
<div class="media-player" id="media-player">
<!-- Progress bar at top of player -->
<div class="player-top-progress">
<div class="progress-bar-container">
<div class="progress-track">
<div class="progress-fill" id="progress-fill"></div>
</div>
<input type="range" class="progress-bar" id="progress-bar" min="0" max="100" value="0" step="0.1">
</div>
<div class="time-display">
<span class="current-time" id="current-time">0:00</span>
<span class="total-time" id="total-time">0:00</span>
</div>
</div>
<!-- Loading Animation -->
<div class="loading-animation hidden" id="loading-animation">
<div class="loading-bar">
<div class="loading-progress"></div>
</div>
<div class="loading-text">0%</div>
</div>
<!-- Track info row -->
<div class="media-header">
<img class="sidebar-album-art" id="sidebar-album-art" src="/static/trans2.png" alt="" onerror="this.src='/static/trans2.png'">
<div class="media-info">
<div class="track-title" id="track-title">No track</div>
<div class="artist-name" id="artist-name">Unknown Artist</div>
<div class="album-name header-album" id="album-name">Unknown Album</div>
</div>
</div>
<!-- Controls row (always visible) -->
<div class="media-controls-row">
<button class="play-button" id="play-button" disabled>
<svg class="play-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
<svg class="pause-icon" viewBox="0 0 24 24" fill="currentColor" style="display:none"><path d="M6 4h4v16H6zm8 0h4v16h-4z"/></svg>
</button>
<div class="volume-control">
<span class="volume-icon">
<svg viewBox="0 0 24 24" fill="currentColor" width="14" height="14"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.16c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.16c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
</span>
<input type="range" class="volume-slider" id="volume-slider" min="0" max="100" value="70">
</div>
<button class="stop-button" id="stop-button" disabled>
<svg viewBox="0 0 24 24" fill="currentColor" width="12" height="12"><rect x="6" y="6" width="12" height="12" rx="1.5"/></svg>
</button>
<button class="expand-hint" aria-label="Expand player">
<svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12"><path d="M3 10l5 4 5-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<!-- Expanded Content (kept for backward compat, hidden) -->
<div class="media-expanded hidden" id="media-expanded"></div>
<!-- No Track Message -->
<div class="no-track-message" id="no-track-message">
<svg class="no-track-icon" viewBox="0 0 48 48" fill="none" width="28" height="28">
<circle cx="24" cy="24" r="22" stroke="currentColor" stroke-width="1.5" opacity="0.25"/>
<path d="M20 16l12 8-12 8z" fill="currentColor" opacity="0.2"/>
</svg>
<span>Play a track to get started</span>
</div>
</div>
<!-- Support Button -->
<div class="support-section">
<button class="support-button" onclick="showSupportModal()">Support SoulSync</button>
@ -7455,6 +7388,63 @@
</div>
<div class="gsearch-results" id="gsearch-results"></div>
<!-- Floating Mini Media Player — fixed bottom-right, above bell/help -->
<div class="media-player idle" id="media-player">
<!-- Thin interactive progress bar spanning full width at top -->
<div class="player-top-progress">
<div class="progress-bar-container">
<div class="progress-track">
<div class="progress-fill" id="progress-fill"></div>
</div>
<input type="range" class="progress-bar" id="progress-bar" min="0" max="100" value="0" step="0.1">
</div>
</div>
<!-- Loading animation (shown while buffering/downloading) -->
<div class="loading-animation hidden" id="loading-animation">
<div class="loading-bar"><div class="loading-progress"></div></div>
<div class="loading-text">0%</div>
</div>
<!-- Main body: album art + track info + controls -->
<div class="mini-player-body">
<img class="sidebar-album-art" id="sidebar-album-art" src="/static/trans2.png" alt="" onerror="this.src='/static/trans2.png'">
<div class="media-info">
<div class="track-title" id="track-title"><span class="title-text">No track</span></div>
<div class="artist-name" id="artist-name">Unknown Artist</div>
</div>
<div class="mini-player-controls">
<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>
<button class="play-button" id="play-button" disabled>
<svg class="play-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
<svg class="pause-icon" viewBox="0 0 24 24" fill="currentColor" style="display:none"><path d="M6 4h4v16H6zm8 0h4v16h-4z"/></svg>
</button>
<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="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>
<button class="expand-hint" aria-label="Expand player" title="Now Playing">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="13" height="13"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>
</button>
</div>
</div>
<!-- Elements kept in DOM for JS compatibility — not visible in mini player -->
<div class="volume-control" style="display:none">
<span class="volume-icon"></span>
<input type="range" class="volume-slider" id="volume-slider" min="0" max="100" value="70">
</div>
<span id="current-time" style="display:none">0:00</span>
<span id="total-time" style="display:none">0:00</span>
<div id="album-name" style="display:none"></div>
<div class="media-expanded hidden" id="media-expanded"></div>
<div class="no-track-message hidden" id="no-track-message"><span>Play a track to get started</span></div>
</div>
<button class="notif-bell-btn" id="notif-bell-btn" onclick="toggleNotifPanel()" title="Notifications">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
<span class="notif-bell-badge" id="notif-bell-badge" style="display:none">0</span>

View file

@ -3059,7 +3059,7 @@ function initializeMediaPlayer() {
// Set initial volume
audioPlayer.volume = 0.7; // 70%
volumeSlider.value = 70;
if (volumeSlider) volumeSlider.value = 70;
}
// Track title click handled by initExpandedPlayer's media-player click handler
@ -3067,7 +3067,7 @@ function initializeMediaPlayer() {
// Media controls
playButton.addEventListener('click', handlePlayPause);
stopButton.addEventListener('click', handleStop);
volumeSlider.addEventListener('input', handleVolumeChange);
if (volumeSlider) volumeSlider.addEventListener('input', handleVolumeChange);
// Progress bar controls
const progressBar = document.getElementById('progress-bar');
@ -3083,7 +3083,13 @@ function initializeMediaPlayer() {
}
// Update volume slider styling
volumeSlider.addEventListener('input', updateVolumeSliderAppearance);
if (volumeSlider) volumeSlider.addEventListener('input', updateVolumeSliderAppearance);
// Mini player prev / next buttons
const miniPrevBtn = document.getElementById('mini-prev-btn');
const miniNextBtn = document.getElementById('mini-next-btn');
if (miniPrevBtn) miniPrevBtn.addEventListener('click', (e) => { e.stopPropagation(); playPreviousInQueue(); });
if (miniNextBtn) miniNextBtn.addEventListener('click', (e) => { e.stopPropagation(); playNextInQueue(); });
}
function toggleMediaPlayerExpansion() {
@ -3325,6 +3331,7 @@ function handleProgressBarChange(event) {
function updateVolumeSliderAppearance() {
const slider = document.getElementById('volume-slider');
if (!slider) return;
const value = slider.value;
slider.style.setProperty('--volume-percent', `${value}%`);
}
@ -4169,7 +4176,7 @@ function initExpandedPlayer() {
mediaPlayer.style.cursor = 'pointer';
mediaPlayer.addEventListener('click', (e) => {
// Don't open modal when clicking controls (let expand-hint through)
if (e.target.closest('.play-button, .stop-button, .volume-slider, .volume-control, .progress-bar, .volume-icon') && !e.target.closest('.expand-hint')) return;
if (e.target.closest('.play-button, .stop-button, .volume-slider, .volume-control, .progress-bar, .volume-icon, .mini-nav-btn') && !e.target.closest('.expand-hint')) return;
if (currentTrack) openNowPlayingModal();
});
}
@ -4821,16 +4828,20 @@ function renderNpQueue() {
}
function updateNpPrevNextButtons() {
const canPrev = npQueueIndex > 0 || (audioPlayer && audioPlayer.currentTime > 3);
const canNext = npQueue.length > 0 && (npShuffleOn ? npQueue.length > 1 : (npQueueIndex < npQueue.length - 1 || npRepeatMode === 'all'));
// Full Now Playing modal buttons
const prevBtn = document.getElementById('np-prev-btn');
const nextBtn = document.getElementById('np-next-btn');
if (prevBtn) {
const canPrev = npQueueIndex > 0 || (audioPlayer && audioPlayer.currentTime > 3);
prevBtn.disabled = !canPrev;
}
if (nextBtn) {
const canNext = npQueue.length > 0 && (npShuffleOn ? npQueue.length > 1 : (npQueueIndex < npQueue.length - 1 || npRepeatMode === 'all'));
nextBtn.disabled = !canNext;
}
if (prevBtn) prevBtn.disabled = !canPrev;
if (nextBtn) nextBtn.disabled = !canNext;
// Mini player buttons
const miniPrevBtn = document.getElementById('mini-prev-btn');
const miniNextBtn = document.getElementById('mini-next-btn');
if (miniPrevBtn) miniPrevBtn.disabled = !canPrev;
if (miniNextBtn) miniNextBtn.disabled = !canNext;
}
function handlePlayerKeyboardShortcuts(event) {

View file

@ -506,67 +506,90 @@ body {
);
}
/* Media Player Section - Premium Compact Design */
/* ── Floating Mini Media Player — fixed bottom-right ─────────────────────── */
.media-player {
position: fixed;
bottom: 88px;
right: 16px;
width: 360px;
background: linear-gradient(180deg,
rgba(24, 24, 24, 0.97) 0%,
rgba(16, 16, 16, 0.99) 50%,
rgba(10, 10, 10, 1.0) 100%);
backdrop-filter: blur(16px) saturate(1.2);
border: 1px solid rgba(255, 255, 255, 0.06);
border-top: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px) saturate(1.3);
border: 1px solid rgba(255, 255, 255, 0.07);
border-top: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
margin: 8px 14px;
padding: 0;
overflow: hidden;
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s ease;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.45),
0 2px 8px rgba(0, 0, 0, 0.3),
0 12px 40px rgba(0, 0, 0, 0.55),
0 4px 12px rgba(0, 0, 0, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.07);
position: relative;
min-height: 120px;
flex-shrink: 0;
z-index: 99998;
}
.media-player:hover {
border-color: rgba(var(--accent-rgb), 0.18);
border-top-color: rgba(var(--accent-rgb), 0.22);
border-color: rgba(var(--accent-rgb), 0.2);
border-top-color: rgba(var(--accent-rgb), 0.25);
box-shadow:
0 10px 28px rgba(0, 0, 0, 0.5),
0 4px 14px rgba(var(--accent-rgb), 0.1),
0 16px 48px rgba(0, 0, 0, 0.6),
0 4px 16px rgba(var(--accent-rgb), 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* Compact idle state — collapsed when nothing is playing */
/* Hidden when nothing is playing — widget disappears entirely */
.media-player.idle {
min-height: 0;
padding: 0;
margin: 4px 14px;
}
.media-player.idle .player-top-progress,
.media-player.idle .media-header,
.media-player.idle .media-controls-row,
.media-player.idle .media-expanded {
display: none;
}
.media-player.idle .no-track-message {
padding: 10px 14px;
font-size: 0.75rem;
gap: 8px;
/* Mini player body — horizontal: art | info | controls */
.mini-player-body {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px 10px;
cursor: pointer;
}
.media-player.idle .no-track-message svg {
width: 18px;
height: 18px;
/* Compact prev/next nav buttons */
.mini-nav-btn {
width: 26px;
height: 26px;
border-radius: 50%;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.45);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.18s ease, background 0.18s ease;
padding: 0;
flex-shrink: 0;
}
/* Top progress bar - thin line across full width */
.mini-nav-btn:hover:not(:disabled) {
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.08);
}
.mini-nav-btn:disabled {
opacity: 0.22;
cursor: not-allowed;
}
/* Controls cluster in the mini player */
.mini-player-controls {
display: flex;
align-items: center;
gap: 3px;
flex-shrink: 0;
}
/* Top progress bar - flush full-width line at the very top of the mini player */
.player-top-progress {
padding: 0 14px;
padding-top: 10px;
padding: 0;
}
.player-top-progress .progress-bar-container {
@ -5034,10 +5057,10 @@ body.helper-mode-active #dashboard-activity-feed:hover {
NOTIFICATION SYSTEM Compact toasts + bell button + notification panel
================================================================================== */
/* Toast container — bottom-right, above the bell/helper buttons */
/* Toast container — bottom-right, above the mini player */
.toast-container {
position: fixed;
bottom: 84px;
bottom: 174px;
right: 24px;
z-index: 99999;
pointer-events: none;