diff --git a/webui/static/mobile.css b/webui/static/mobile.css
index 5dc75e67..3612725c 100644
--- a/webui/static/mobile.css
+++ b/webui/static/mobile.css
@@ -2214,4 +2214,33 @@
.enhanced-bulk-modal {
width: calc(100vw - 32px);
}
+ /* Now Playing Modal */
+ .np-modal {
+ width: 100vw;
+ max-width: none;
+ max-height: none;
+ height: 100vh;
+ border-radius: 0;
+ }
+ .np-body {
+ flex-direction: column;
+ padding: 48px 24px 24px;
+ gap: 24px;
+ overflow-y: auto;
+ max-height: 100vh;
+ }
+ .np-album-art-container {
+ width: 220px;
+ height: 220px;
+ margin: 0 auto;
+ }
+ .np-track-info {
+ width: 100%;
+ }
+ .np-track-title {
+ font-size: 18px;
+ }
+ .np-volume-slider-container {
+ width: 120px;
+ }
}
\ No newline at end of file
diff --git a/webui/static/script.js b/webui/static/script.js
index 674de1b7..5d80db55 100644
--- a/webui/static/script.js
+++ b/webui/static/script.js
@@ -1270,6 +1270,7 @@ function initApp() {
initializeNavigation();
initializeMobileNavigation();
initializeMediaPlayer();
+ initExpandedPlayer();
initializeSyncPage();
initializeWatchlist();
initializeDownloadManagerToggle();
@@ -1621,8 +1622,7 @@ function initializeMediaPlayer() {
volumeSlider.value = 70;
}
- // Track title click - toggle expansion
- trackTitle.addEventListener('click', toggleMediaPlayerExpansion);
+ // Track title click handled by initExpandedPlayer's media-player click handler
// Media controls
playButton.addEventListener('click', handlePlayPause);
@@ -1647,22 +1647,8 @@ function initializeMediaPlayer() {
}
function toggleMediaPlayerExpansion() {
- if (!currentTrack) return;
-
- const mediaPlayer = document.getElementById('media-player');
- const expandedContent = document.getElementById('media-expanded');
- const noTrackMessage = document.getElementById('no-track-message');
-
- mediaPlayerExpanded = !mediaPlayerExpanded;
-
- if (mediaPlayerExpanded) {
- mediaPlayer.style.minHeight = '145px';
- expandedContent.classList.remove('hidden');
- noTrackMessage.classList.add('hidden');
- } else {
- mediaPlayer.style.minHeight = '85px';
- expandedContent.classList.add('hidden');
- }
+ // No-op: controls are always visible in the new layout.
+ // Kept for backward compatibility with any callers.
}
function extractTrackTitle(filename) {
@@ -1710,10 +1696,10 @@ function setTrackInfo(track) {
// Hide no track message
document.getElementById('no-track-message').classList.add('hidden');
- // Auto-expand if collapsed
- if (!mediaPlayerExpanded) {
- toggleMediaPlayerExpansion();
- }
+ // Sync expanded player and media session
+ updateNpTrackInfo();
+ updateMediaSessionMetadata();
+ updateMediaSessionPlaybackState();
}
function checkAndEnableScrolling(element, text) {
@@ -1744,18 +1730,7 @@ function checkAndEnableScrolling(element, text) {
function clearTrack() {
- // Force collapse the media player BEFORE clearing currentTrack
- if (mediaPlayerExpanded) {
- // Manually collapse since toggleMediaPlayerExpansion() needs currentTrack
- mediaPlayerExpanded = false;
- const mediaPlayer = document.getElementById('media-player');
- const expandedContent = document.getElementById('media-expanded');
-
- if (mediaPlayer) mediaPlayer.style.minHeight = '85px';
- if (expandedContent) expandedContent.classList.add('hidden');
- }
-
- // Now clear track state
+ // Clear track state
currentTrack = null;
isPlaying = false;
@@ -1766,8 +1741,13 @@ function clearTrack() {
document.getElementById('artist-name').textContent = 'Unknown Artist';
document.getElementById('album-name').textContent = 'Unknown Album';
- document.getElementById('play-button').textContent = 'β·';
- document.getElementById('play-button').disabled = true;
+ // Reset play button SVGs (don't use textContent β it destroys SVG children)
+ const clearPlayBtn = document.getElementById('play-button');
+ const clearPlayIcon = clearPlayBtn.querySelector('.play-icon');
+ const clearPauseIcon = clearPlayBtn.querySelector('.pause-icon');
+ if (clearPlayIcon) clearPlayIcon.style.display = '';
+ if (clearPauseIcon) clearPauseIcon.style.display = 'none';
+ clearPlayBtn.disabled = true;
document.getElementById('stop-button').disabled = true;
// Reset progress bar and time displays
@@ -1792,13 +1772,32 @@ function clearTrack() {
// Show no track message
document.getElementById('no-track-message').classList.remove('hidden');
+ // Reset queue state
+ npQueue = [];
+ npQueueIndex = -1;
+
+ // Sync expanded player and media session
+ updateNpTrackInfo();
+ updateNpPlayButton();
+ updateNpProgress();
+ renderNpQueue();
+ updateNpPrevNextButtons();
+ updateMediaSessionPlaybackState();
+ if (npModalOpen) closeNowPlayingModal();
+
console.log('π§Ή Track cleared and media player reset');
}
function setPlayingState(playing) {
isPlaying = playing;
const playButton = document.getElementById('play-button');
- playButton.textContent = playing ? 'βΈοΈ' : 'β·';
+ // Toggle SVG icons (don't use textContent β it destroys SVG children)
+ const playIcon = playButton.querySelector('.play-icon');
+ const pauseIcon = playButton.querySelector('.pause-icon');
+ if (playIcon) playIcon.style.display = playing ? 'none' : '';
+ if (pauseIcon) pauseIcon.style.display = playing ? '' : 'none';
+ updateNpPlayButton();
+ updateMediaSessionPlaybackState();
}
async function handlePlayPause() {
@@ -1820,6 +1819,13 @@ function handleVolumeChange(event) {
if (audioPlayer) {
audioPlayer.volume = volume / 100;
}
+
+ // Sync modal volume and clear mute state
+ npMuted = false;
+ const npVol = document.getElementById('np-volume-slider');
+ const npFill = document.getElementById('np-volume-fill');
+ if (npVol) npVol.value = volume;
+ if (npFill) npFill.style.width = volume + '%';
}
function handleProgressBarChange(event) {
@@ -1845,6 +1851,14 @@ function handleProgressBarChange(event) {
if (currentTimeElement) {
currentTimeElement.textContent = formatTime(newTime);
}
+
+ // Sync modal progress
+ const npBar = document.getElementById('np-progress-bar');
+ const npFill = document.getElementById('np-progress-fill');
+ const npTime = document.getElementById('np-current-time');
+ if (npBar) npBar.value = progress;
+ if (npFill) npFill.style.width = progress + '%';
+ if (npTime) npTime.textContent = formatTime(newTime);
} catch (error) {
console.warn('β οΈ Seek failed:', error.message);
// Reset progress bar to current position
@@ -1917,7 +1931,8 @@ async function startStream(searchResult) {
artist: searchResult.artist || searchResult.username || 'Unknown Artist',
album: searchResult.album || 'Unknown Album',
username: searchResult.username,
- filename: searchResult.filename
+ filename: searchResult.filename,
+ image_url: searchResult.image_url || searchResult.album_cover_url || null
});
showLoadingAnimation();
@@ -2262,11 +2277,6 @@ async function startAudioPlayback() {
noTrackMessage.classList.add('hidden');
}
- // Ensure media player is expanded when playback starts
- if (!mediaPlayerExpanded) {
- toggleMediaPlayerExpansion();
- }
-
// Update volume to current slider value
const volumeSlider = document.getElementById('volume-slider');
if (volumeSlider) {
@@ -2410,6 +2420,9 @@ function updateAudioProgress() {
if (totalTimeElement) {
totalTimeElement.textContent = formatTime(audioPlayer.duration);
}
+
+ // Sync expanded player modal
+ if (npModalOpen) updateNpProgress();
}
function onAudioEnded() {
@@ -2432,7 +2445,9 @@ function onAudioEnded() {
currentTimeElement.textContent = '0:00';
}
- // TODO: Auto-advance to next track if queue exists
+ // Repeat-one is handled by audioPlayer.loop (set in handleNpRepeat)
+ // Auto-advance to next track if queue exists (guard against race conditions)
+ if (npQueue.length > 0 && !npLoadingQueueItem) { playNextInQueue(); return; }
}
function onAudioError(event) {
@@ -2593,6 +2608,664 @@ function canPlayAudioFormat(extension) {
return isSupported;
}
+// ===============================
+// EXPANDED NOW PLAYING MODAL
+// ===============================
+
+let npModalOpen = false;
+let npRepeatMode = 'off'; // 'off' | 'one'
+let npShuffleOn = false;
+let npQueue = [];
+let npQueueIndex = -1;
+let npMuted = false;
+let npPreMuteVolume = 70;
+let npMediaSessionThrottle = 0;
+let npLoadingQueueItem = false;
+
+function initExpandedPlayer() {
+ const closeBtn = document.getElementById('np-close-btn');
+ const overlay = document.getElementById('np-modal-overlay');
+ const playBtn = document.getElementById('np-play-btn');
+ const stopBtn = document.getElementById('np-stop-btn');
+ const shuffleBtn = document.getElementById('np-shuffle-btn');
+ const repeatBtn = document.getElementById('np-repeat-btn');
+ const muteBtn = document.getElementById('np-mute-btn');
+ const npProgressBar = document.getElementById('np-progress-bar');
+ const npVolumeSlider = document.getElementById('np-volume-slider');
+
+ if (!overlay) return;
+
+ // Close handlers
+ closeBtn.addEventListener('click', closeNowPlayingModal);
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) closeNowPlayingModal(); });
+
+ // Control handlers
+ playBtn.addEventListener('click', () => { togglePlayback(); });
+ stopBtn.addEventListener('click', async () => { await handleStop(); closeNowPlayingModal(); });
+ shuffleBtn.addEventListener('click', handleNpShuffle);
+ repeatBtn.addEventListener('click', handleNpRepeat);
+ muteBtn.addEventListener('click', handleNpMuteToggle);
+
+ // Progress bar
+ npProgressBar.addEventListener('input', handleNpProgressBarChange);
+ npProgressBar.addEventListener('mousedown', () => { npProgressBar.dataset.seeking = 'true'; });
+ npProgressBar.addEventListener('mouseup', () => { delete npProgressBar.dataset.seeking; });
+
+ // Volume slider
+ npVolumeSlider.addEventListener('input', handleNpVolumeChange);
+
+ // Keyboard shortcuts (global)
+ document.addEventListener('keydown', handlePlayerKeyboardShortcuts);
+
+ // Make sidebar media player clickable to open modal
+ const mediaPlayer = document.getElementById('media-player');
+ if (mediaPlayer) {
+ 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 (currentTrack) openNowPlayingModal();
+ });
+ }
+
+ // Prev / Next buttons
+ const prevBtn = document.getElementById('np-prev-btn');
+ const nextBtn = document.getElementById('np-next-btn');
+ if (prevBtn) prevBtn.addEventListener('click', () => { playPreviousInQueue(); });
+ if (nextBtn) nextBtn.addEventListener('click', () => { playNextInQueue(); });
+
+ // Queue panel toggle + clear
+ const queueToggle = document.getElementById('np-queue-toggle');
+ if (queueToggle) {
+ queueToggle.addEventListener('click', () => {
+ const body = document.getElementById('np-queue-body');
+ if (body) body.classList.toggle('hidden');
+ queueToggle.classList.toggle('active');
+ });
+ }
+ const queueClearBtn = document.getElementById('np-queue-clear');
+ if (queueClearBtn) queueClearBtn.addEventListener('click', () => { clearQueue(); });
+
+ // Init Media Session API
+ initMediaSession();
+}
+
+function openNowPlayingModal() {
+ const overlay = document.getElementById('np-modal-overlay');
+ if (!overlay) return;
+ npModalOpen = true;
+ overlay.classList.remove('hidden');
+ document.body.style.overflow = 'hidden';
+ syncExpandedPlayerUI();
+}
+
+function closeNowPlayingModal() {
+ const overlay = document.getElementById('np-modal-overlay');
+ if (!overlay) return;
+ npModalOpen = false;
+ overlay.classList.add('hidden');
+ document.body.style.overflow = '';
+}
+
+function syncExpandedPlayerUI() {
+ if (!npModalOpen) return;
+
+ // Track info
+ updateNpTrackInfo();
+
+ // Play state
+ updateNpPlayButton();
+
+ // Progress
+ updateNpProgress();
+
+ // Volume
+ const sidebarVol = document.getElementById('volume-slider');
+ const npVol = document.getElementById('np-volume-slider');
+ const npVolFill = document.getElementById('np-volume-fill');
+ if (sidebarVol && npVol) {
+ npVol.value = sidebarVol.value;
+ if (npVolFill) npVolFill.style.width = sidebarVol.value + '%';
+ }
+
+ // Visualizer
+ const viz = document.getElementById('np-visualizer');
+ if (viz) viz.classList.toggle('playing', isPlaying);
+
+ // Queue
+ renderNpQueue();
+ updateNpPrevNextButtons();
+}
+
+function updateNpTrackInfo() {
+ const titleEl = document.getElementById('np-track-title');
+ const artistEl = document.getElementById('np-artist-name');
+ const albumEl = document.getElementById('np-album-name');
+ const artImg = document.getElementById('np-album-art');
+ const artPlaceholder = document.getElementById('np-album-art-placeholder');
+ const badgesEl = document.getElementById('np-format-badges');
+
+ if (!titleEl) return;
+
+ // Sidebar album art
+ const sidebarArt = document.getElementById('sidebar-album-art');
+
+ if (currentTrack) {
+ titleEl.textContent = currentTrack.title || 'Unknown Track';
+ artistEl.textContent = currentTrack.artist || 'Unknown Artist';
+ albumEl.textContent = currentTrack.album || 'Unknown Album';
+
+ // Album art (modal + sidebar)
+ const artUrl = getNpAlbumArtUrl();
+ if (artUrl && artImg) {
+ artImg.src = artUrl;
+ artImg.classList.remove('hidden');
+ artImg.onerror = () => { artImg.classList.add('hidden'); };
+ } else if (artImg) {
+ artImg.classList.add('hidden');
+ }
+ if (sidebarArt) {
+ if (artUrl) {
+ sidebarArt.src = artUrl;
+ sidebarArt.style.display = '';
+ sidebarArt.onerror = () => { sidebarArt.src = ''; };
+ } else {
+ sidebarArt.src = '';
+ }
+ }
+
+ // Format badges
+ if (badgesEl) {
+ badgesEl.innerHTML = '';
+ const filename = currentTrack.filename || '';
+ if (filename) {
+ const ext = getFileExtension(filename);
+ if (ext) {
+ const badge = document.createElement('span');
+ badge.className = 'np-format-badge' + (ext === 'flac' ? ' flac' : '');
+ badge.textContent = ext.toUpperCase();
+ badgesEl.appendChild(badge);
+ }
+ }
+ }
+ } else {
+ titleEl.textContent = 'No track';
+ artistEl.textContent = 'Unknown Artist';
+ albumEl.textContent = 'Unknown Album';
+ if (artImg) artImg.classList.add('hidden');
+ if (sidebarArt) sidebarArt.src = '';
+ if (badgesEl) badgesEl.innerHTML = '';
+ }
+}
+
+function updateNpPlayButton() {
+ const playIcon = document.querySelector('.np-icon-play');
+ const pauseIcon = document.querySelector('.np-icon-pause');
+ if (playIcon && pauseIcon) {
+ playIcon.classList.toggle('hidden', isPlaying);
+ pauseIcon.classList.toggle('hidden', !isPlaying);
+ }
+
+ const viz = document.getElementById('np-visualizer');
+ if (viz) viz.classList.toggle('playing', isPlaying);
+}
+
+function updateNpProgress() {
+ if (!npModalOpen || !audioPlayer) return;
+
+ const npProgressBar = document.getElementById('np-progress-bar');
+ const npProgressFill = document.getElementById('np-progress-fill');
+ const npCurrentTime = document.getElementById('np-current-time');
+ const npTotalTime = document.getElementById('np-total-time');
+
+ if (audioPlayer.duration) {
+ const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100;
+ if (npProgressBar && !npProgressBar.dataset.seeking) {
+ npProgressBar.value = progress;
+ }
+ if (npProgressFill) npProgressFill.style.width = progress + '%';
+ if (npCurrentTime) npCurrentTime.textContent = formatTime(audioPlayer.currentTime);
+ if (npTotalTime) npTotalTime.textContent = formatTime(audioPlayer.duration);
+ } else {
+ if (npProgressBar) npProgressBar.value = 0;
+ if (npProgressFill) npProgressFill.style.width = '0%';
+ if (npCurrentTime) npCurrentTime.textContent = '0:00';
+ if (npTotalTime) npTotalTime.textContent = '0:00';
+ }
+}
+
+function handleNpProgressBarChange(event) {
+ if (!audioPlayer || !audioPlayer.duration) return;
+ const progress = parseFloat(event.target.value);
+ const newTime = (progress / 100) * audioPlayer.duration;
+
+ try {
+ audioPlayer.currentTime = newTime;
+
+ // Sync sidebar progress
+ const sidebarBar = document.getElementById('progress-bar');
+ const sidebarFill = document.getElementById('progress-fill');
+ if (sidebarBar) sidebarBar.value = progress;
+ if (sidebarFill) sidebarFill.style.width = progress + '%';
+
+ // Sync modal progress fill
+ const npFill = document.getElementById('np-progress-fill');
+ if (npFill) npFill.style.width = progress + '%';
+
+ // Update time displays
+ const sidebarTime = document.getElementById('current-time');
+ const npTime = document.getElementById('np-current-time');
+ if (sidebarTime) sidebarTime.textContent = formatTime(newTime);
+ if (npTime) npTime.textContent = formatTime(newTime);
+ } catch (error) {
+ console.warn('Seek failed:', error.message);
+ }
+}
+
+function handleNpVolumeChange(event) {
+ const volume = parseInt(event.target.value);
+ if (audioPlayer) audioPlayer.volume = volume / 100;
+
+ // Sync sidebar volume slider
+ const sidebarVol = document.getElementById('volume-slider');
+ if (sidebarVol) {
+ sidebarVol.value = volume;
+ sidebarVol.style.setProperty('--volume-percent', volume + '%');
+ }
+
+ // Update modal volume fill
+ const npFill = document.getElementById('np-volume-fill');
+ if (npFill) npFill.style.width = volume + '%';
+
+ // Update mute state
+ npMuted = volume === 0;
+ updateNpMuteIcon();
+}
+
+function handleNpMuteToggle() {
+ const npVol = document.getElementById('np-volume-slider');
+ if (!npVol) return;
+
+ if (npMuted) {
+ // Unmute β restore previous volume
+ npVol.value = npPreMuteVolume;
+ npVol.dispatchEvent(new Event('input'));
+ npMuted = false;
+ } else {
+ // Mute β save current volume, set to 0
+ npPreMuteVolume = parseInt(npVol.value) || 70;
+ npVol.value = 0;
+ npVol.dispatchEvent(new Event('input'));
+ npMuted = true;
+ }
+ updateNpMuteIcon();
+}
+
+function updateNpMuteIcon() {
+ const muteBtn = document.getElementById('np-mute-btn');
+ const volIcon = muteBtn ? muteBtn.querySelector('.np-icon-vol') : null;
+ const mutedIcon = muteBtn ? muteBtn.querySelector('.np-icon-muted') : null;
+ if (volIcon && mutedIcon) {
+ volIcon.classList.toggle('hidden', npMuted);
+ mutedIcon.classList.toggle('hidden', !npMuted);
+ }
+ if (muteBtn) muteBtn.classList.toggle('muted', npMuted);
+}
+
+function handleNpShuffle() {
+ npShuffleOn = !npShuffleOn;
+ const btn = document.getElementById('np-shuffle-btn');
+ if (btn) btn.classList.toggle('active', npShuffleOn);
+}
+
+function handleNpRepeat() {
+ if (npRepeatMode === 'off') {
+ npRepeatMode = 'one';
+ if (audioPlayer) audioPlayer.loop = true;
+ } else {
+ npRepeatMode = 'off';
+ if (audioPlayer) audioPlayer.loop = false;
+ }
+ const btn = document.getElementById('np-repeat-btn');
+ if (btn) btn.classList.toggle('active', npRepeatMode !== 'off');
+}
+
+// ===============================
+// QUEUE MANAGEMENT
+// ===============================
+
+function addToQueue(track) {
+ npQueue.push(track);
+ showToast('Added to queue', 'success');
+ renderNpQueue();
+ updateNpPrevNextButtons();
+ // If nothing is currently playing, auto-play the first queued track
+ if (!currentTrack || (!isPlaying && audioPlayer && audioPlayer.paused && audioPlayer.currentTime === 0)) {
+ playQueueItem(npQueue.length - 1);
+ }
+}
+
+function removeFromQueue(index) {
+ if (index < 0 || index >= npQueue.length) return;
+ const wasCurrentTrack = (index === npQueueIndex);
+ npQueue.splice(index, 1);
+ // Adjust current index
+ if (npQueue.length === 0) {
+ npQueueIndex = -1;
+ } else if (index < npQueueIndex) {
+ npQueueIndex--;
+ } else if (wasCurrentTrack) {
+ // Removed the currently playing item
+ if (npQueueIndex >= npQueue.length) {
+ npQueueIndex = npQueue.length - 1;
+ }
+ // Play the next track at the adjusted index
+ playQueueItem(npQueueIndex);
+ }
+ renderNpQueue();
+ updateNpPrevNextButtons();
+}
+
+function clearQueue() {
+ npQueue = [];
+ npQueueIndex = -1;
+ renderNpQueue();
+ updateNpPrevNextButtons();
+}
+
+function playNextInQueue() {
+ if (npQueue.length === 0) return;
+ if (npShuffleOn) {
+ // Pick a random index that is not the current one
+ const candidates = [];
+ for (let i = 0; i < npQueue.length; i++) {
+ if (i !== npQueueIndex) candidates.push(i);
+ }
+ if (candidates.length === 0) return;
+ const next = candidates[Math.floor(Math.random() * candidates.length)];
+ playQueueItem(next);
+ } else {
+ const next = npQueueIndex + 1;
+ if (next >= npQueue.length) return; // End of queue
+ playQueueItem(next);
+ }
+}
+
+function playPreviousInQueue() {
+ // If more than 3 seconds in, restart current track
+ if (audioPlayer && audioPlayer.currentTime > 3) {
+ audioPlayer.currentTime = 0;
+ if (audioPlayer.paused) audioPlayer.play();
+ return;
+ }
+ if (npQueue.length === 0) return;
+ const prev = npQueueIndex - 1;
+ if (prev < 0) {
+ // At start β restart current track
+ if (audioPlayer) {
+ audioPlayer.currentTime = 0;
+ if (audioPlayer.paused) audioPlayer.play();
+ }
+ return;
+ }
+ playQueueItem(prev);
+}
+
+async function playQueueItem(index) {
+ if (index < 0 || index >= npQueue.length) return;
+ if (npLoadingQueueItem) return; // Prevent race condition from double-advance
+ npLoadingQueueItem = true;
+ npQueueIndex = index;
+ const track = npQueue[index];
+
+ try {
+ if (track.is_library) {
+ // Library track playback flow
+ await stopStream();
+ setTrackInfo({
+ title: track.title,
+ artist: track.artist,
+ album: track.album,
+ filename: track.file_path,
+ is_library: true,
+ image_url: track.image_url
+ });
+ showLoadingAnimation();
+
+ const response = await fetch('/api/library/play', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ file_path: track.file_path,
+ title: track.title || '',
+ artist: track.artist || '',
+ album: track.album || ''
+ })
+ });
+ const result = await response.json();
+ if (!result.success) throw new Error(result.error || 'Failed to start playback');
+ // Re-apply repeat-one loop property
+ if (audioPlayer) audioPlayer.loop = (npRepeatMode === 'one');
+ await startAudioPlayback();
+ } else {
+ // Non-library (stream) tracks cannot be queued for auto-advance
+ // Just show track info β the stream flow handles its own playback
+ setTrackInfo({
+ title: track.title,
+ artist: track.artist,
+ album: track.album,
+ filename: track.filename || track.file_path,
+ is_library: false,
+ image_url: track.image_url
+ });
+ }
+ } catch (error) {
+ console.error('Queue playback error:', error);
+ showToast(`Playback error: ${error.message}`, 'error');
+ hideLoadingAnimation();
+ } finally {
+ npLoadingQueueItem = false;
+ }
+
+ renderNpQueue();
+ updateNpPrevNextButtons();
+}
+
+function renderNpQueue() {
+ const listEl = document.getElementById('np-queue-list');
+ const emptyEl = document.getElementById('np-queue-empty');
+ const countEl = document.getElementById('np-queue-count');
+ if (!listEl) return;
+
+ if (countEl) countEl.textContent = npQueue.length > 0 ? `(${npQueue.length})` : '';
+
+ if (npQueue.length === 0) {
+ listEl.innerHTML = '';
+ if (emptyEl) emptyEl.classList.remove('hidden');
+ return;
+ }
+
+ if (emptyEl) emptyEl.classList.add('hidden');
+ listEl.innerHTML = '';
+
+ npQueue.forEach((track, i) => {
+ const item = document.createElement('div');
+ item.className = 'np-queue-item' + (i === npQueueIndex ? ' active' : '');
+ item.onclick = () => playQueueItem(i);
+
+ const info = document.createElement('div');
+ info.className = 'np-queue-item-info';
+
+ const title = document.createElement('div');
+ title.className = 'np-queue-item-title';
+ title.textContent = track.title || 'Unknown Track';
+
+ const artist = document.createElement('div');
+ artist.className = 'np-queue-item-artist';
+ artist.textContent = track.artist || 'Unknown Artist';
+
+ info.appendChild(title);
+ info.appendChild(artist);
+ item.appendChild(info);
+
+ const removeBtn = document.createElement('button');
+ removeBtn.className = 'np-queue-item-remove';
+ removeBtn.innerHTML = '✕';
+ removeBtn.title = 'Remove from queue';
+ removeBtn.onclick = (e) => {
+ e.stopPropagation();
+ removeFromQueue(i);
+ };
+ item.appendChild(removeBtn);
+
+ listEl.appendChild(item);
+ });
+}
+
+function updateNpPrevNextButtons() {
+ 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);
+ nextBtn.disabled = !canNext;
+ }
+}
+
+function handlePlayerKeyboardShortcuts(event) {
+ // Don't intercept when typing in inputs or when non-player modals are open
+ const tag = document.activeElement.tagName.toLowerCase();
+ if (tag === 'input' || tag === 'textarea' || tag === 'select' || document.activeElement.isContentEditable) return;
+
+ // Only handle when player modal is open OR when no other modal is visible
+ const otherModals = document.querySelectorAll('.modal-overlay:not(.hidden):not(#np-modal-overlay)');
+ if (otherModals.length > 0 && !npModalOpen) return;
+
+ switch (event.key) {
+ case ' ':
+ if (!currentTrack) return;
+ event.preventDefault();
+ togglePlayback();
+ break;
+ case 'ArrowLeft':
+ if (!audioPlayer || !audioPlayer.duration) return;
+ event.preventDefault();
+ audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 5);
+ break;
+ case 'ArrowRight':
+ if (!audioPlayer || !audioPlayer.duration) return;
+ event.preventDefault();
+ audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 5);
+ break;
+ case 'ArrowUp':
+ event.preventDefault();
+ if (audioPlayer) {
+ const newVol = Math.min(1, audioPlayer.volume + 0.05);
+ audioPlayer.volume = newVol;
+ syncVolumeUI(Math.round(newVol * 100));
+ }
+ break;
+ case 'ArrowDown':
+ event.preventDefault();
+ if (audioPlayer) {
+ const newVol = Math.max(0, audioPlayer.volume - 0.05);
+ audioPlayer.volume = newVol;
+ syncVolumeUI(Math.round(newVol * 100));
+ }
+ break;
+ case 'm':
+ case 'M':
+ if (npModalOpen) handleNpMuteToggle();
+ break;
+ case 'Escape':
+ if (npModalOpen) closeNowPlayingModal();
+ break;
+ default:
+ return; // Don't prevent default for unhandled keys
+ }
+}
+
+function syncVolumeUI(volumePercent) {
+ // Sync both sidebar and modal volume UIs
+ const sidebarVol = document.getElementById('volume-slider');
+ const npVol = document.getElementById('np-volume-slider');
+ const npFill = document.getElementById('np-volume-fill');
+
+ if (sidebarVol) {
+ sidebarVol.value = volumePercent;
+ sidebarVol.style.setProperty('--volume-percent', volumePercent + '%');
+ }
+ if (npVol) npVol.value = volumePercent;
+ if (npFill) npFill.style.width = volumePercent + '%';
+}
+
+function getNpAlbumArtUrl() {
+ if (!currentTrack) return null;
+ return currentTrack.image_url || currentTrack.album_cover_url || currentTrack.thumb_url || null;
+}
+
+// Media Session API
+function initMediaSession() {
+ if (!('mediaSession' in navigator)) return;
+
+ navigator.mediaSession.setActionHandler('play', () => {
+ if (audioPlayer && currentTrack) {
+ audioPlayer.play().then(() => setPlayingState(true));
+ }
+ });
+ navigator.mediaSession.setActionHandler('pause', () => {
+ if (audioPlayer) {
+ audioPlayer.pause();
+ setPlayingState(false);
+ }
+ });
+ navigator.mediaSession.setActionHandler('stop', () => {
+ handleStop();
+ });
+ navigator.mediaSession.setActionHandler('seekbackward', () => {
+ if (audioPlayer && audioPlayer.duration) {
+ audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 10);
+ }
+ });
+ navigator.mediaSession.setActionHandler('seekforward', () => {
+ if (audioPlayer && audioPlayer.duration) {
+ audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 10);
+ }
+ });
+ navigator.mediaSession.setActionHandler('previoustrack', () => {
+ if (npQueue.length > 0) playPreviousInQueue();
+ });
+ navigator.mediaSession.setActionHandler('nexttrack', () => {
+ if (npQueue.length > 0) playNextInQueue();
+ });
+}
+
+function updateMediaSessionMetadata() {
+ if (!('mediaSession' in navigator) || !currentTrack) return;
+ const artwork = [];
+ const artUrl = getNpAlbumArtUrl();
+ if (artUrl) artwork.push({ src: artUrl, sizes: '512x512', type: 'image/jpeg' });
+
+ navigator.mediaSession.metadata = new MediaMetadata({
+ title: currentTrack.title || 'Unknown Track',
+ artist: currentTrack.artist || 'Unknown Artist',
+ album: currentTrack.album || 'Unknown Album',
+ artwork: artwork
+ });
+}
+
+function updateMediaSessionPlaybackState() {
+ if (!('mediaSession' in navigator)) return;
+ if (!currentTrack) {
+ navigator.mediaSession.playbackState = 'none';
+ } else {
+ navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
+ }
+}
+
// ===============================
// SUPPORT MODAL
// ===============================
@@ -32814,6 +33487,7 @@ function renderTrackTable(album) {
{ label: 'BPM', cls: 'col-bpm', sortField: 'bpm' },
{ label: 'File', cls: 'col-path' },
{ label: 'Match', cls: 'col-match' },
+ { label: '', cls: 'col-queue' },
{ label: '', cls: 'col-delete' },
];
columns.forEach(col => {
@@ -32980,6 +33654,35 @@ function renderTrackTable(album) {
matchTd.appendChild(matchCell);
tr.appendChild(matchTd);
+ // Add to Queue button
+ const queueTd = document.createElement('td');
+ queueTd.className = 'col-queue';
+ if (track.file_path) {
+ const queueBtn = document.createElement('button');
+ queueBtn.className = 'enhanced-queue-btn';
+ queueBtn.innerHTML = '+';
+ queueBtn.title = 'Add to queue';
+ queueBtn.onclick = (e) => {
+ e.stopPropagation();
+ const artistName = artistDetailPageState.enhancedData ? artistDetailPageState.enhancedData.artist.name : '';
+ let albumArt = album.thumb_url || null;
+ if (!albumArt && artistDetailPageState.enhancedData) {
+ albumArt = artistDetailPageState.enhancedData.artist?.thumb_url;
+ }
+ addToQueue({
+ title: track.title || 'Unknown Track',
+ artist: artistName || 'Unknown Artist',
+ album: album.title || 'Unknown Album',
+ file_path: track.file_path,
+ filename: track.file_path,
+ is_library: true,
+ image_url: albumArt
+ });
+ };
+ queueTd.appendChild(queueBtn);
+ }
+ tr.appendChild(queueTd);
+
// Delete button
const delTd = document.createElement('td');
delTd.className = 'col-delete';
@@ -33807,13 +34510,27 @@ async function playLibraryTrack(track, albumTitle, artistName) {
audioPlayer.pause();
}
+ // Get album art from enhanced data if available
+ let albumArt = null;
+ if (artistDetailPageState.enhancedData) {
+ const albums = artistDetailPageState.enhancedData.albums || [];
+ for (const a of albums) {
+ if ((a.tracks || []).some(t => t.id === track.id)) {
+ albumArt = a.thumb_url;
+ break;
+ }
+ }
+ if (!albumArt) albumArt = artistDetailPageState.enhancedData.artist?.thumb_url;
+ }
+
// Set track info in the media player UI
setTrackInfo({
title: track.title || 'Unknown Track',
artist: artistName || 'Unknown Artist',
album: albumTitle || 'Unknown Album',
filename: track.file_path,
- is_library: true
+ is_library: true,
+ image_url: albumArt
});
// Show loading state
diff --git a/webui/static/style.css b/webui/static/style.css
index 87d3c673..fbbce482 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -333,50 +333,182 @@ body {
);
}
-/* Media Player Section - Premium Glassmorphic Design */
+/* Media Player Section - Premium Compact Design */
.media-player {
background: linear-gradient(180deg,
- rgba(28, 28, 28, 0.95) 0%,
- rgba(20, 20, 20, 0.98) 50%,
- rgba(14, 14, 14, 1.0) 100%);
- backdrop-filter: blur(12px) saturate(1.1);
+ 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);
- border-radius: 16px;
+ border-radius: 14px;
margin: 8px 14px;
- padding: 18px;
- min-height: 150px;
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ padding: 0;
+ overflow: hidden;
+ transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
- 0 8px 24px rgba(0, 0, 0, 0.4),
- 0 4px 12px rgba(0, 0, 0, 0.3),
- inset 0 1px 0 rgba(255, 255, 255, 0.08);
+ 0 8px 24px rgba(0, 0, 0, 0.45),
+ 0 2px 8px rgba(0, 0, 0, 0.3),
+ inset 0 1px 0 rgba(255, 255, 255, 0.07);
+ position: relative;
}
.media-player:hover {
- border: 1px solid rgba(var(--accent-rgb), 0.2);
- border-top: 1px solid rgba(var(--accent-rgb), 0.25);
- background: linear-gradient(180deg,
- rgba(var(--accent-rgb), 0.08) 0%,
- rgba(28, 28, 28, 0.95) 40%,
- rgba(20, 20, 20, 0.98) 70%,
- rgba(14, 14, 14, 1.0) 100%);
+ border-color: rgba(var(--accent-rgb), 0.18);
+ border-top-color: rgba(var(--accent-rgb), 0.22);
box-shadow:
- 0 12px 32px rgba(0, 0, 0, 0.5),
- 0 6px 16px rgba(var(--accent-rgb), 0.15),
- 0 4px 12px rgba(0, 0, 0, 0.3),
- inset 0 1px 0 rgba(255, 255, 255, 0.12);
+ 0 10px 28px rgba(0, 0, 0, 0.5),
+ 0 4px 14px rgba(var(--accent-rgb), 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
+/* Top progress bar - thin line across full width */
+.player-top-progress {
+ padding: 0 14px;
+ padding-top: 10px;
+}
+
+.player-top-progress .progress-bar-container {
+ position: relative;
+ width: 100%;
+ height: 14px;
+}
+
+.player-top-progress .progress-track {
+ position: absolute;
+ top: 50%;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 2px;
+ transform: translateY(-50%);
+ z-index: 1;
+ transition: height 0.15s ease;
+}
+
+.player-top-progress:hover .progress-track {
+ height: 4px;
+}
+
+.progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, rgb(var(--accent-rgb)), rgb(var(--accent-light-rgb)));
+ border-radius: 2px;
+ width: 0%;
+ transition: width 0.1s linear;
+}
+
+.progress-bar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 14px;
+ background: transparent;
+ outline: none;
+ cursor: pointer;
+ -webkit-appearance: none;
+ appearance: none;
+ z-index: 2;
+ margin: 0;
+}
+
+.progress-bar::-webkit-slider-track {
+ width: 100%;
+ height: 3px;
+ background: transparent;
+ border-radius: 2px;
+}
+
+.progress-bar::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 10px;
+ height: 10px;
+ background: #fff;
+ border-radius: 50%;
+ cursor: pointer;
+ border: none;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
+ transition: all 0.15s ease;
+ opacity: 0;
+}
+
+.player-top-progress:hover .progress-bar::-webkit-slider-thumb {
+ opacity: 1;
+}
+
+.progress-bar::-webkit-slider-thumb:hover {
+ transform: scale(1.2);
+ box-shadow: 0 0 0 4px rgba(var(--accent-rgb), 0.2);
+}
+
+.progress-bar::-webkit-slider-thumb:active {
+ transform: scale(1.3);
+ box-shadow: 0 0 0 6px rgba(var(--accent-rgb), 0.25);
+}
+
+/* Firefox progress bar styling */
+.progress-bar::-moz-range-track {
+ width: 100%;
+ height: 3px;
+ background: transparent;
+ border-radius: 2px;
+ border: none;
+}
+
+.progress-bar::-moz-range-thumb {
+ width: 10px;
+ height: 10px;
+ background: #fff;
+ border-radius: 50%;
+ cursor: pointer;
+ border: none;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
+ opacity: 0;
+ transition: opacity 0.15s ease;
+}
+
+.player-top-progress:hover .progress-bar::-moz-range-thumb {
+ opacity: 1;
+}
+
+/* Time display - left/right under progress */
+.player-top-progress .time-display {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
+ font-size: 9px;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.35);
+ letter-spacing: 0.3px;
+ padding: 0 1px;
+ margin-top: -1px;
+}
+
+.current-time {
+ min-width: 28px;
+ text-align: left;
+}
+
+.total-time {
+ min-width: 28px;
+ text-align: right;
+}
+
+/* Loading Animation */
.loading-animation {
height: 12px;
- margin-bottom: 12px;
+ margin: 8px 14px 4px;
position: relative;
}
.loading-bar {
- height: 4px;
- background: #2a2a2a;
+ height: 3px;
+ background: rgba(255, 255, 255, 0.06);
border-radius: 2px;
overflow: hidden;
position: relative;
@@ -395,18 +527,35 @@ body {
top: 0;
left: 50%;
transform: translateX(-50%);
- font-family: 'Segoe UI', sans-serif;
+ font-family: 'SF Mono', 'Consolas', monospace;
font-size: 7px;
font-weight: 500;
- color: #b4b4b4;
+ color: rgba(255, 255, 255, 0.4);
line-height: 8px;
}
+/* Track info row β album art + text only, no controls competing for space */
.media-header {
display: flex;
- align-items: flex-start;
- gap: 16px;
- margin-bottom: 10px;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 14px 4px;
+ cursor: pointer;
+}
+
+.sidebar-album-art {
+ width: 38px;
+ height: 38px;
+ min-width: 38px;
+ border-radius: 6px;
+ object-fit: cover;
+ background: rgba(255, 255, 255, 0.05);
+ display: block;
+}
+
+.sidebar-album-art[src=""],
+.sidebar-album-art:not([src]) {
+ background: linear-gradient(135deg, rgba(var(--accent-rgb), 0.15) 0%, rgba(255, 255, 255, 0.03) 100%);
}
.media-info {
@@ -414,20 +563,20 @@ body {
min-width: 0;
display: flex;
flex-direction: column;
- gap: 4px;
- padding-top: 2px;
+ gap: 1px;
}
.track-title {
- font-family: 'Spotify Circular', 'SF Pro Display', -apple-system, sans-serif;
- font-size: 15px;
- font-weight: 700;
+ font-family: -apple-system, 'Segoe UI', sans-serif;
+ font-size: 12.5px;
+ font-weight: 600;
color: #ffffff;
- letter-spacing: -0.2px;
+ letter-spacing: -0.1px;
line-height: 1.3;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
+ text-overflow: ellipsis;
margin: 0;
transition: color 0.2s ease;
position: relative;
@@ -449,21 +598,10 @@ body {
}
@keyframes marquee-scroll {
- 0% {
- transform: translateX(0px);
- }
-
- 20% {
- transform: translateX(0px);
- }
-
- 80% {
- transform: translateX(var(--scroll-distance));
- }
-
- 100% {
- transform: translateX(var(--scroll-distance));
- }
+ 0% { transform: translateX(0px); }
+ 20% { transform: translateX(0px); }
+ 80% { transform: translateX(var(--scroll-distance)); }
+ 100%{ transform: translateX(var(--scroll-distance)); }
}
.track-title:hover {
@@ -471,281 +609,243 @@ body {
}
.artist-name {
- font-family: 'Spotify Circular', -apple-system, sans-serif;
- font-size: 12px;
- font-weight: 500;
- color: #a7a7a7;
+ font-family: -apple-system, 'Segoe UI', sans-serif;
+ font-size: 11px;
+ font-weight: 400;
+ color: rgba(255, 255, 255, 0.55);
letter-spacing: 0.1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
- opacity: 0.9;
}
+.album-name.header-album {
+ font-family: -apple-system, 'Segoe UI', sans-serif;
+ font-size: 10px;
+ font-weight: 400;
+ color: rgba(255, 255, 255, 0.3);
+ letter-spacing: 0.1px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin: 0;
+ font-style: normal;
+}
+
+/* Controls row β always visible below track info */
+.media-controls-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 14px 10px;
+}
+
+/* Play Button */
.play-button {
- width: 40px;
- height: 40px;
- border-radius: 20px;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
background: rgb(var(--accent-light-rgb));
border: none;
color: #000000;
- font-size: 16px;
- font-weight: 900;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
- transition: all 0.2s ease;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ padding: 0;
+ flex-shrink: 0;
+}
+
+.play-button svg {
+ width: 14px;
+ height: 14px;
}
.play-button:hover:not(:disabled) {
background: #1fdf64;
- transform: scale(1.05);
+ transform: scale(1.08);
+ box-shadow: 0 4px 12px rgba(var(--accent-rgb), 0.35);
}
.play-button:active:not(:disabled) {
- background: #1ca851;
transform: scale(0.95);
}
.play-button:disabled {
- background: #535353;
- color: #b3b3b3;
+ background: rgba(255, 255, 255, 0.08);
+ color: rgba(255, 255, 255, 0.25);
cursor: not-allowed;
+ box-shadow: none;
}
-.media-expanded {
- margin-top: 8px;
- padding-top: 8px;
- border-top: 1px solid rgba(255, 255, 255, 0.08);
-}
-
-.album-name {
- font-family: 'Spotify Circular', -apple-system, sans-serif;
- font-size: 11px;
- font-weight: 500;
- color: #999999;
- letter-spacing: 0.1px;
- margin-bottom: 12px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- font-style: italic;
- opacity: 0.8;
-}
-
-/* Progress Section Styling */
-.progress-section {
- margin-bottom: 12px;
-}
-
-.time-display {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 6px;
- font-family: 'Spotify Circular', -apple-system, monospace;
- font-size: 10px;
- color: #b3b3b3;
- font-weight: 400;
-}
-
-.current-time,
-.total-time {
- min-width: 32px;
- text-align: center;
-}
-
-.time-separator {
- color: #666666;
-}
-
-.progress-bar-container {
- position: relative;
- width: 100%;
- height: 20px;
-}
-
-.progress-track {
- position: absolute;
- top: 50%;
- left: 0;
- right: 0;
- height: 3px;
- background: #4f4f4f;
- border-radius: 2px;
- transform: translateY(-50%);
- z-index: 1;
-}
-
-.progress-fill {
- height: 100%;
- background: rgb(var(--accent-rgb));
- /* Green accent color */
- border-radius: 2px;
- width: 0%;
- transition: width 0.1s ease;
-}
-
-.progress-bar {
-
- top: 0;
- left: 0;
- width: 100%;
- height: 20px;
- background: transparent;
- outline: none;
- cursor: pointer;
- -webkit-appearance: none;
- appearance: none;
- z-index: 2;
-}
-
-.progress-bar::-webkit-slider-track {
- width: 100%;
- height: 3px;
- background: transparent;
- border-radius: 2px;
-}
-
-.progress-bar::-webkit-slider-thumb {
- -webkit-appearance: none;
- appearance: none;
- width: 12px;
- height: 12px;
- background: rgb(var(--accent-rgb));
- border-radius: 50%;
- cursor: pointer;
- border: none;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
- transition: all 0.15s ease;
-}
-
-.progress-bar::-webkit-slider-thumb:hover {
- background: rgb(var(--accent-light-rgb));
- transform: scale(1.1);
-}
-
-.progress-bar::-webkit-slider-thumb:active {
- transform: scale(1.15);
- box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.3);
-}
-
-/* Firefox progress bar styling */
-.progress-bar::-moz-range-track {
- width: 100%;
- height: 3px;
- background: transparent;
- border-radius: 2px;
- border: none;
-}
-
-.progress-bar::-moz-range-thumb {
- width: 12px;
- height: 12px;
- background: rgb(var(--accent-rgb));
- border-radius: 50%;
- cursor: pointer;
- border: none;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
-}
-
-.progress-bar::-moz-range-thumb:hover {
- background: rgb(var(--accent-light-rgb));
-}
-
-.media-controls {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 0px;
-}
-
+/* Volume control in controls row */
.volume-control {
display: flex;
align-items: center;
- gap: 10px;
+ gap: 5px;
+ flex: 1;
+ min-width: 0;
}
.volume-icon {
- color: #b3b3b3;
- font-size: 13px;
+ color: rgba(255, 255, 255, 0.4);
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+}
+
+.volume-icon svg {
+ width: 13px;
+ height: 13px;
}
.volume-slider {
- width: 80px;
- height: 20px;
+ width: 100%;
+ height: 16px;
background: transparent;
outline: none;
cursor: pointer;
-}
-
-.volume-slider::-webkit-slider-track {
- width: 100%;
- height: 3px;
- background: #4f4f4f;
- border-radius: 1px;
-}
-
-.volume-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
appearance: none;
- width: 12px;
- height: 12px;
- background: #ffffff;
- border-radius: 50%;
- cursor: pointer;
- margin-top: -4px;
-}
-
-.volume-slider::-webkit-slider-thumb:hover {
- background: rgb(var(--accent-light-rgb));
}
.volume-slider::-webkit-slider-runnable-track {
- background: linear-gradient(to right, rgb(var(--accent-light-rgb)) 0%, rgb(var(--accent-light-rgb)) var(--volume-percent, 70%), #4f4f4f var(--volume-percent, 70%), #4f4f4f 100%);
+ width: 100%;
+ height: 3px;
+ border-radius: 2px;
+ background: linear-gradient(to right,
+ rgba(255, 255, 255, 0.6) 0%,
+ rgba(255, 255, 255, 0.6) var(--volume-percent, 70%),
+ rgba(255, 255, 255, 0.08) var(--volume-percent, 70%),
+ rgba(255, 255, 255, 0.08) 100%);
}
-.stop-button {
- width: 32px;
- height: 32px;
- border-radius: 16px;
+.volume-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 10px;
+ height: 10px;
+ background: #ffffff;
+ border-radius: 50%;
+ cursor: pointer;
+ margin-top: -3.5px;
+ border: none;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+ transition: all 0.15s ease;
+ opacity: 0;
+}
+
+.volume-control:hover .volume-slider::-webkit-slider-thumb {
+ opacity: 1;
+}
+
+.volume-slider::-webkit-slider-thumb:hover {
+ transform: scale(1.15);
+}
+
+/* Firefox volume slider */
+.volume-slider::-moz-range-track {
+ width: 100%;
+ height: 3px;
+ border-radius: 2px;
background: rgba(255, 255, 255, 0.08);
- border: 1px solid #b3b3b3;
- color: #ffffff;
- font-size: 12px;
+ border: none;
+}
+
+.volume-slider::-moz-range-thumb {
+ width: 10px;
+ height: 10px;
+ background: #ffffff;
+ border-radius: 50%;
+ cursor: pointer;
+ border: none;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+}
+
+/* Stop button */
+.stop-button {
+ width: 24px;
+ height: 24px;
+ border-radius: 5px;
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ color: rgba(255, 255, 255, 0.5);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
+ padding: 0;
+ flex-shrink: 0;
+}
+
+.stop-button svg {
+ width: 10px;
+ height: 10px;
}
.stop-button:hover:not(:disabled) {
- background: rgba(255, 255, 255, 0.15);
- border: 1px solid #ffffff;
+ background: rgba(255, 255, 255, 0.12);
+ border-color: rgba(255, 255, 255, 0.15);
+ color: #ffffff;
}
.stop-button:active:not(:disabled) {
- background: rgba(255, 255, 255, 0.25);
+ background: rgba(255, 255, 255, 0.18);
}
.stop-button:disabled {
background: transparent;
- border: 1px solid #2a2a2a;
- color: #535353;
+ border-color: rgba(255, 255, 255, 0.03);
+ color: rgba(255, 255, 255, 0.12);
cursor: not-allowed;
}
+/* Expand hint chevron β subtle indicator to open NP modal */
+.expand-hint {
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.15);
+ cursor: pointer;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: color 0.2s ease;
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.expand-hint:hover {
+ color: rgba(255, 255, 255, 0.45);
+}
+
+/* Expanded section (kept for backward compat) */
+.media-expanded {
+ display: none;
+}
+
+/* No track empty state */
.no-track-message {
- color: rgba(255, 255, 255, 0.25);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+ color: rgba(255, 255, 255, 0.18);
font-size: 11px;
- font-weight: 500;
+ font-weight: 400;
text-align: center;
- padding: 24px 20px;
- letter-spacing: 0.3px;
- font-family: 'SF Pro Text', -apple-system, sans-serif;
- line-height: 1.5;
+ padding: 20px 16px;
+ letter-spacing: 0.2px;
+ font-family: -apple-system, 'Segoe UI', sans-serif;
+ line-height: 1.4;
+}
+
+.no-track-icon {
+ opacity: 0.6;
}
/* Crypto Donation Section */
@@ -30963,4 +31063,620 @@ textarea.enhanced-meta-field-input {
/* Sortable column headers */
.enhanced-track-table th[style*="cursor: pointer"]:hover {
color: rgb(var(--accent-rgb));
+}
+
+/* ========== Expanded Now Playing Modal ========== */
+.np-modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(4, 4, 4, 0.92);
+ backdrop-filter: blur(32px);
+ -webkit-backdrop-filter: blur(32px);
+ z-index: 10001;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 1;
+ transition: opacity 0.3s ease;
+}
+.np-modal-overlay.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+.np-modal {
+ width: 90vw;
+ max-width: 880px;
+ max-height: 85vh;
+ background: linear-gradient(180deg, rgba(24, 24, 24, 0.97) 0%, rgba(10, 10, 10, 1) 100%);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 20px;
+ box-shadow: 0 32px 100px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.03);
+ position: relative;
+ overflow: hidden;
+ transform: scale(1);
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
+}
+.np-modal-overlay.hidden .np-modal {
+ transform: scale(0.92);
+}
+.np-close-btn {
+ position: absolute;
+ top: 16px;
+ right: 20px;
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.3);
+ font-size: 28px;
+ cursor: pointer;
+ z-index: 2;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ transition: all 0.15s ease;
+}
+.np-close-btn:hover {
+ color: rgba(255, 255, 255, 0.7);
+ background: rgba(255, 255, 255, 0.06);
+}
+
+/* Layout */
+.np-body {
+ display: flex;
+ flex-direction: row;
+ padding: 48px 40px 36px;
+ gap: 48px;
+ align-items: flex-start;
+}
+.np-left {
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+}
+.np-right {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ min-width: 0;
+ justify-content: center;
+}
+
+/* Album Art */
+.np-album-art-container {
+ width: 300px;
+ height: 300px;
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5), 0 2px 12px rgba(0, 0, 0, 0.3);
+ position: relative;
+ background: rgba(255, 255, 255, 0.03);
+ flex-shrink: 0;
+}
+.np-album-art {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+.np-album-art.hidden {
+ display: none;
+}
+.np-album-art-placeholder {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(135deg, rgba(30, 30, 30, 1) 0%, rgba(18, 18, 18, 1) 100%);
+ color: rgba(255, 255, 255, 0.12);
+}
+.np-album-art:not(.hidden) + .np-album-art-placeholder {
+ display: none;
+}
+
+/* Track Info */
+.np-track-info {
+ text-align: center;
+ width: 300px;
+}
+.np-track-title {
+ font-size: 22px;
+ font-weight: 700;
+ color: #fff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ letter-spacing: -0.3px;
+}
+.np-artist-name {
+ font-size: 15px;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.55);
+ margin-top: 4px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.np-album-name {
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.35);
+ font-style: italic;
+ margin-top: 3px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.np-format-badges {
+ display: flex;
+ gap: 6px;
+ justify-content: center;
+ margin-top: 10px;
+ flex-wrap: wrap;
+}
+.np-format-badge {
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ background: rgba(255, 255, 255, 0.06);
+ color: rgba(255, 255, 255, 0.45);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+}
+.np-format-badge.flac {
+ color: rgba(var(--accent-rgb), 0.9);
+ background: rgba(var(--accent-rgb), 0.08);
+ border-color: rgba(var(--accent-rgb), 0.15);
+}
+
+/* Progress Section */
+.np-progress-section {
+ width: 100%;
+}
+.np-progress-bar-container {
+ position: relative;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+}
+.np-progress-track {
+ position: absolute;
+ width: 100%;
+ height: 4px;
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 2px;
+ overflow: hidden;
+}
+.np-progress-bar-container:hover .np-progress-track {
+ height: 6px;
+}
+.np-progress-fill {
+ height: 100%;
+ background: rgb(var(--accent-rgb));
+ border-radius: 2px;
+ width: 0%;
+ transition: width 0.1s linear;
+}
+.np-progress-bar {
+ position: absolute;
+ width: 100%;
+ height: 20px;
+ -webkit-appearance: none;
+ appearance: none;
+ background: transparent;
+ cursor: pointer;
+ margin: 0;
+ z-index: 1;
+}
+.np-progress-bar::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+.np-progress-bar-container:hover .np-progress-bar::-webkit-slider-thumb {
+ opacity: 1;
+}
+.np-progress-bar::-moz-range-thumb {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
+ cursor: pointer;
+ border: none;
+}
+.np-progress-bar::-moz-range-track {
+ background: transparent;
+ border: none;
+ height: 4px;
+}
+.np-time-display {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 6px;
+}
+.np-current-time,
+.np-total-time {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.4);
+ font-variant-numeric: tabular-nums;
+ min-width: 36px;
+}
+.np-total-time {
+ text-align: right;
+}
+
+/* Controls Row */
+.np-controls-row {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 20px;
+}
+.np-btn {
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.6);
+ cursor: pointer;
+ padding: 8px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.15s ease;
+}
+.np-btn:hover {
+ color: #fff;
+ transform: scale(1.08);
+}
+.np-btn:active {
+ transform: scale(0.95);
+}
+.np-btn:disabled {
+ color: rgba(255, 255, 255, 0.15);
+ cursor: default;
+ transform: none;
+}
+.np-btn-play {
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ background: rgb(var(--accent-rgb));
+ color: #000;
+ box-shadow: 0 4px 20px rgba(var(--accent-rgb), 0.3);
+}
+.np-btn-play:hover {
+ background: rgb(calc(var(--accent-rgb) + 20));
+ filter: brightness(1.15);
+ transform: scale(1.05);
+ box-shadow: 0 6px 28px rgba(var(--accent-rgb), 0.4);
+}
+.np-btn-play:active {
+ filter: brightness(0.9);
+ transform: scale(0.97);
+}
+.np-btn-shuffle.active,
+.np-btn-repeat.active {
+ color: rgb(var(--accent-rgb));
+}
+.np-btn-shuffle.active::after,
+.np-btn-repeat.active::after {
+ content: '';
+ position: absolute;
+ bottom: 2px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: rgb(var(--accent-rgb));
+}
+.np-btn-shuffle,
+.np-btn-repeat {
+ position: relative;
+}
+
+/* Volume Row */
+.np-volume-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ justify-content: center;
+}
+.np-mute-btn {
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.5);
+ cursor: pointer;
+ padding: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: color 0.15s;
+}
+.np-mute-btn:hover {
+ color: #fff;
+}
+.np-mute-btn.muted {
+ color: rgba(255, 80, 80, 0.7);
+}
+.np-volume-slider-container {
+ position: relative;
+ width: 160px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+}
+.np-volume-track {
+ position: absolute;
+ width: 100%;
+ height: 4px;
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 2px;
+ overflow: hidden;
+}
+.np-volume-fill {
+ height: 100%;
+ background: rgb(var(--accent-rgb));
+ border-radius: 2px;
+ width: 70%;
+ transition: width 0.05s linear;
+}
+.np-volume-slider {
+ position: absolute;
+ width: 100%;
+ height: 20px;
+ -webkit-appearance: none;
+ appearance: none;
+ background: transparent;
+ cursor: pointer;
+ margin: 0;
+ z-index: 1;
+}
+.np-volume-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
+ cursor: pointer;
+}
+.np-volume-slider::-moz-range-thumb {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
+ cursor: pointer;
+ border: none;
+}
+.np-volume-slider::-moz-range-track {
+ background: transparent;
+ border: none;
+ height: 4px;
+}
+
+/* Stop Row */
+.np-stop-row {
+ display: flex;
+ justify-content: center;
+}
+.np-btn-stop {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 20px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ color: rgba(255, 255, 255, 0.4);
+ font-size: 12px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+.np-btn-stop:hover {
+ background: rgba(255, 60, 60, 0.08);
+ border-color: rgba(255, 60, 60, 0.2);
+ color: rgba(255, 80, 80, 0.8);
+}
+
+/* Visualizer */
+.np-visualizer {
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ gap: 3px;
+ height: 24px;
+ margin-top: 8px;
+ opacity: 0.4;
+}
+.np-viz-bar {
+ width: 3px;
+ background: rgb(var(--accent-rgb));
+ border-radius: 1.5px;
+ height: 3px;
+ transition: height 0.1s ease;
+}
+.np-visualizer.playing .np-viz-bar {
+ animation: npVizPulse 0.8s ease-in-out infinite alternate;
+}
+.np-visualizer.playing .np-viz-bar:nth-child(1) { animation-delay: 0s; }
+.np-visualizer.playing .np-viz-bar:nth-child(2) { animation-delay: 0.1s; }
+.np-visualizer.playing .np-viz-bar:nth-child(3) { animation-delay: 0.2s; }
+.np-visualizer.playing .np-viz-bar:nth-child(4) { animation-delay: 0.15s; }
+.np-visualizer.playing .np-viz-bar:nth-child(5) { animation-delay: 0.25s; }
+.np-visualizer.playing .np-viz-bar:nth-child(6) { animation-delay: 0.05s; }
+.np-visualizer.playing .np-viz-bar:nth-child(7) { animation-delay: 0.18s; }
+@keyframes npVizPulse {
+ 0% { height: 3px; }
+ 100% { height: 20px; }
+}
+
+/* Queue Panel */
+.np-queue-panel {
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
+ padding: 0 40px 20px;
+}
+.np-queue-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 0 8px;
+}
+.np-queue-toggle {
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.5);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ font-weight: 500;
+ padding: 4px 0;
+ transition: color 0.15s ease;
+}
+.np-queue-toggle:hover,
+.np-queue-toggle.active {
+ color: rgba(255, 255, 255, 0.85);
+}
+.np-queue-count {
+ color: rgba(255, 255, 255, 0.3);
+ font-size: 12px;
+}
+.np-queue-clear-btn {
+ background: none;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.35);
+ font-size: 11px;
+ padding: 3px 10px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+.np-queue-clear-btn:hover {
+ color: rgba(255, 255, 255, 0.7);
+ border-color: rgba(255, 255, 255, 0.2);
+}
+.np-queue-body {
+ max-height: 200px;
+ overflow-y: auto;
+}
+.np-queue-body.hidden {
+ display: none;
+}
+.np-queue-empty {
+ color: rgba(255, 255, 255, 0.25);
+ font-size: 13px;
+ text-align: center;
+ padding: 16px 0;
+}
+.np-queue-list {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.np-queue-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+.np-queue-item:hover {
+ background: rgba(255, 255, 255, 0.05);
+}
+.np-queue-item.active {
+ background: rgba(29, 185, 84, 0.12);
+ border-left: 3px solid #1db954;
+}
+.np-queue-item-info {
+ min-width: 0;
+ flex: 1;
+}
+.np-queue-item-title {
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.85);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.np-queue-item.active .np-queue-item-title {
+ color: #1db954;
+}
+.np-queue-item-artist {
+ font-size: 11px;
+ color: rgba(255, 255, 255, 0.35);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.np-queue-item-remove {
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.2);
+ cursor: pointer;
+ font-size: 12px;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ flex-shrink: 0;
+ transition: all 0.15s ease;
+}
+.np-queue-item-remove:hover {
+ color: rgba(255, 80, 80, 0.8);
+ background: rgba(255, 80, 80, 0.1);
+}
+
+/* Queue button in enhanced track table */
+.col-queue {
+ width: 32px;
+ text-align: center;
+}
+.enhanced-queue-btn {
+ background: none;
+ border: 1px solid rgba(29, 185, 84, 0.2);
+ color: rgba(29, 185, 84, 0.5);
+ border-radius: 50%;
+ width: 24px;
+ height: 24px;
+ font-size: 14px;
+ font-weight: bold;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.15s ease;
+ padding: 0;
+}
+.enhanced-queue-btn:hover {
+ background: rgba(29, 185, 84, 0.12);
+ color: rgba(29, 185, 84, 0.9);
+ border-color: rgba(29, 185, 84, 0.4);
+ transform: scale(1.1);
}
\ No newline at end of file