IMP: refactored queue list and lyrics panel in the same ui

This commit is contained in:
Julien Maille 2025-12-30 12:05:50 +01:00
parent 16034014a0
commit 82b4afb149
6 changed files with 302 additions and 265 deletions

View file

@ -28,16 +28,16 @@
<li data-action="download">Download</li>
</ul>
</div>
<div id="queue-modal-overlay" style="display: none;">
<div id="queue-modal">
<div id="queue-modal-header">
<h3>Queue</h3>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button id="clear-queue-btn" class="btn-secondary">Clear All</button>
<button id="close-queue-btn">&times;</button>
</div>
<div id="side-panel" class="side-panel">
<div class="panel-header">
<h3 id="side-panel-title">Panel</h3>
<div class="panel-controls" id="side-panel-controls">
<!-- Controls injected dynamically -->
</div>
<div id="queue-list"></div>
</div>
<div id="side-panel-content" class="panel-content">
<!-- Content injected dynamically -->
</div>
</div>

View file

@ -5,13 +5,14 @@ import { apiSettings, themeManager, nowPlayingSettings, trackListSettings } from
import { UIRenderer } from './ui.js';
import { Player } from './player.js';
import { LastFMScrobbler } from './lastfm.js';
import { LyricsManager, createLyricsPanel, showSyncedLyricsPanel, clearLyricsPanelSync } from './lyrics.js';
import { LyricsManager, openLyricsPanel, clearLyricsPanelSync } from './lyrics.js';
import { createRouter, updateTabTitle } from './router.js';
import { initializeSettings } from './settings.js';
import { initializePlayerEvents, initializeTrackInteractions, handleTrackAction } from './events.js';
import { initializeUIInteractions } from './ui-interactions.js';
import { downloadAlbumAsZip, downloadDiscography, downloadPlaylistAsZip } from './downloads.js';
import { debounce, SVG_PLAY } from './utils.js';
import { sidePanelManager } from './side-panel.js';
function initializeCasting(audioPlayer, castBtn) {
if (!castBtn) return;
@ -86,7 +87,7 @@ function initializeCasting(audioPlayer, castBtn) {
}
}
function initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel) {
function initializeKeyboardShortcuts(player, audioPlayer) {
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea')) return;
@ -138,11 +139,8 @@ function initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel) {
break;
case 'escape':
document.getElementById('search-input')?.blur();
document.getElementById('queue-modal-overlay').style.display = 'none';
if (lyricsPanel) {
lyricsPanel.classList.add('hidden');
clearLyricsPanelSync(audioPlayer, lyricsPanel);
}
sidePanelManager.close();
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
break;
case 'l':
document.querySelector('.now-playing-bar .cover')?.click();
@ -188,7 +186,6 @@ document.addEventListener('DOMContentLoaded', async () => {
const ui = new UIRenderer(api, player);
const scrobbler = new LastFMScrobbler();
const lyricsManager = new LyricsManager(api);
const lyricsPanel = createLyricsPanel();
const currentTheme = themeManager.getTheme();
themeManager.setTheme(currentTheme);
@ -198,7 +195,7 @@ document.addEventListener('DOMContentLoaded', async () => {
initializePlayerEvents(player, audioPlayer, scrobbler, ui);
initializeTrackInteractions(player, api, document.querySelector('.main-content'), document.getElementById('context-menu'), lyricsManager, ui, scrobbler);
initializeUIInteractions(player, api);
initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel);
initializeKeyboardShortcuts(player, audioPlayer);
const castBtn = document.getElementById('cast-btn');
initializeCasting(audioPlayer, castBtn);
@ -217,13 +214,13 @@ document.addEventListener('DOMContentLoaded', async () => {
const mode = nowPlayingSettings.getMode();
if (mode === 'lyrics') {
const isHidden = lyricsPanel.classList.contains('hidden');
lyricsPanel.classList.toggle('hidden');
if (isHidden) {
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager);
const isActive = sidePanelManager.isActive('lyrics');
if (isActive) {
sidePanelManager.close();
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
} else {
clearLyricsPanelSync(audioPlayer, lyricsPanel);
openLyricsPanel(player.currentTrack, audioPlayer, lyricsManager);
}
} else if (mode === 'cover') {
@ -257,47 +254,16 @@ document.addEventListener('DOMContentLoaded', async () => {
return;
}
const isHidden = lyricsPanel.classList.contains('hidden');
lyricsPanel.classList.toggle('hidden');
if (isHidden) {
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager);
const isActive = sidePanelManager.isActive('lyrics');
if (isActive) {
sidePanelManager.close();
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
} else {
clearLyricsPanelSync(audioPlayer, lyricsPanel);
openLyricsPanel(player.currentTrack, audioPlayer, lyricsManager);
}
});
document.getElementById('close-lyrics-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
lyricsPanel.classList.add('hidden');
clearLyricsPanelSync(audioPlayer, lyricsPanel);
});
document.getElementById('download-lrc-btn')?.addEventListener('click', async (e) => {
e.stopPropagation();
if (!player.currentTrack) {
alert('No track is currently playing');
return;
}
const btn = e.target.closest('#download-lrc-btn');
btn.disabled = true;
try {
const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id, player.currentTrack);
if (lyricsData) {
lyricsManager.downloadLRC(lyricsData, player.currentTrack);
} else {
alert('No synced lyrics available for download');
}
} catch (error) {
console.error('Failed to download lyrics!', error);
alert('Failed to Download Lyrics! check the console for more information.')
} finally {
btn.disabled = false;
}
});
document.getElementById('download-current-btn')?.addEventListener('click', () => {
if (player.currentTrack) {
handleTrackAction('download', player.currentTrack, player, api, lyricsManager, 'track', ui);
@ -317,9 +283,9 @@ document.addEventListener('DOMContentLoaded', async () => {
previousTrackId = currentTrackId;
// Update lyrics panel if it's open
if (!lyricsPanel.classList.contains('hidden')) {
clearLyricsPanelSync(audioPlayer, lyricsPanel);
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager);
if (sidePanelManager.isActive('lyrics')) {
// Re-open forces update/refresh of content and sync
openLyricsPanel(player.currentTrack, audioPlayer, lyricsManager);
}
// Update Fullscreen/Enlarged Cover if it's open

View file

@ -1,5 +1,6 @@
//js/lyrics.js
import { getTrackTitle, getTrackArtists, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js';
import { sidePanelManager } from './side-panel.js';
export class LyricsManager {
constructor(api) {
@ -156,80 +157,86 @@ export class LyricsManager {
}
}
export function createLyricsPanel() {
const panel = document.createElement('div');
panel.id = 'lyrics-panel';
panel.className = 'lyrics-panel hidden';
panel.innerHTML = `
<div class="lyrics-header">
<h3>Lyrics</h3>
<div class="lyrics-controls">
<button id="download-lrc-btn" class="btn-icon" title="Download LRC">
${SVG_DOWNLOAD}
</button>
<button id="close-lyrics-btn" class="btn-icon" title="Close">
${SVG_CLOSE}
</button>
</div>
</div>
<div class="lyrics-content">
<div class="lyrics-loading">Loading lyrics...</div>
</div>
`;
document.body.appendChild(panel);
return panel;
}
export async function showSyncedLyricsPanel(track, audioPlayer, panel, lyricsManager = null) {
const content = panel.querySelector('.lyrics-content');
// If no manager provided, create a temp one (though caching won't persist across calls if this happens)
export async function openLyricsPanel(track, audioPlayer, lyricsManager) {
// If no manager provided, create a temp one
const manager = lyricsManager || new LyricsManager();
// Check if we are already displaying this track
if (panel.dataset.lastTrackId === String(track.id) && content.querySelector('am-lyrics')) {
// Just re-attach listeners
setupLyricsSync(track, audioPlayer, panel, manager, content.querySelector('am-lyrics'));
return;
}
const renderControls = (container) => {
container.innerHTML = `
<button id="download-lrc-btn" class="btn-icon" title="Download LRC">
${SVG_DOWNLOAD}
</button>
<button id="close-side-panel-btn" class="btn-icon" title="Close">
${SVG_CLOSE}
</button>
`;
panel.dataset.lastTrackId = String(track.id);
content.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
try {
await manager.ensureComponentLoaded();
container.querySelector('#close-side-panel-btn').addEventListener('click', () => {
sidePanelManager.close();
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
});
container.querySelector('#download-lrc-btn').addEventListener('click', async (e) => {
const btn = e.currentTarget;
btn.disabled = true;
try {
const lyricsData = await manager.fetchLyrics(track.id, track);
if (lyricsData) {
manager.downloadLRC(lyricsData, track);
} else {
alert('No synced lyrics available for download');
}
} catch (error) {
console.error('Failed to download lyrics!', error);
alert('Failed to Download Lyrics! check the console for more information.')
} finally {
btn.disabled = false;
}
});
};
const renderContent = async (container) => {
container.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
const title = track.title;
const artist = getTrackArtists(track);
const album = track.album?.title;
const durationMs = track.duration ? Math.round(track.duration * 1000) : undefined;
const isrc = track.isrc || '';
content.innerHTML = '';
const amLyrics = document.createElement('am-lyrics');
amLyrics.setAttribute('song-title', title);
amLyrics.setAttribute('song-artist', artist);
if (album) amLyrics.setAttribute('song-album', album);
if (durationMs) amLyrics.setAttribute('song-duration', durationMs);
amLyrics.setAttribute('query', `${title} ${artist}`.trim());
if (isrc) amLyrics.setAttribute('isrc', isrc);
amLyrics.setAttribute('highlight-color', '#93c5fd');
amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)');
amLyrics.setAttribute('autoscroll', '');
amLyrics.setAttribute('interpolate', '');
amLyrics.style.height = '100%';
amLyrics.style.width = '100%';
content.appendChild(amLyrics);
manager.amLyricsElement = amLyrics;
setupLyricsSync(track, audioPlayer, panel, manager, amLyrics);
} catch (error) {
console.error('Failed to load lyrics:', error);
content.innerHTML = '<div class="lyrics-error">Failed to load lyrics! :(</div>';
}
try {
await manager.ensureComponentLoaded();
const title = track.title;
const artist = getTrackArtists(track);
const album = track.album?.title;
const durationMs = track.duration ? Math.round(track.duration * 1000) : undefined;
const isrc = track.isrc || '';
container.innerHTML = '';
const amLyrics = document.createElement('am-lyrics');
amLyrics.setAttribute('song-title', title);
amLyrics.setAttribute('song-artist', artist);
if (album) amLyrics.setAttribute('song-album', album);
if (durationMs) amLyrics.setAttribute('song-duration', durationMs);
amLyrics.setAttribute('query', `${title} ${artist}`.trim());
if (isrc) amLyrics.setAttribute('isrc', isrc);
amLyrics.setAttribute('highlight-color', '#93c5fd');
amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)');
amLyrics.setAttribute('autoscroll', '');
amLyrics.setAttribute('interpolate', '');
amLyrics.style.height = '100%';
amLyrics.style.width = '100%';
container.appendChild(amLyrics);
manager.amLyricsElement = amLyrics;
// Clean up any previous sync
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
setupLyricsSync(track, audioPlayer, sidePanelManager.panel, manager, amLyrics);
} catch (error) {
console.error('Failed to load lyrics:', error);
container.innerHTML = '<div class="lyrics-error">Failed to load lyrics! :(</div>';
}
};
sidePanelManager.open('lyrics', 'Lyrics', renderControls, renderContent);
}
function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) {
@ -265,11 +272,6 @@ function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) {
}
};
// Remove old listeners if any (though clearLyricsPanelSync handles this,
// we might be calling this from the "same track" branch where clear wasn't called?
// No, clearLyricsPanelSync IS called when hiding.
// But when SHOWING, we need to add them.
audioPlayer.addEventListener('timeupdate', updateTime);
audioPlayer.addEventListener('play', onPlay);
audioPlayer.addEventListener('pause', onPause);
@ -281,9 +283,6 @@ function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) {
panel.lyricsPauseHandler = onPause;
panel.lyricsSeekHandler = updateTime;
// We also need to remove these in clearLyricsPanelSync!
// The current clearLyricsPanelSync only removes 'timeupdate'.
amLyrics.addEventListener('line-click', (e) => {
if (e.detail && e.detail.timestamp) {
audioPlayer.currentTime = e.detail.timestamp / 1000;
@ -299,7 +298,6 @@ function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) {
if (lyricsManager.animationFrameId) {
cancelAnimationFrame(lyricsManager.animationFrameId);
}
// Also remove listeners
audioPlayer.removeEventListener('timeupdate', updateTime);
audioPlayer.removeEventListener('play', onPlay);
audioPlayer.removeEventListener('pause', onPause);
@ -309,11 +307,7 @@ function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) {
export function clearLyricsPanelSync(audioPlayer, panel) {
if (panel.lyricsUpdateHandler) {
audioPlayer.removeEventListener('timeupdate', panel.lyricsUpdateHandler);
panel.lyricsUpdateHandler = null;
}
if (panel.lyricsCleanup) {
if (panel && panel.lyricsCleanup) {
panel.lyricsCleanup();
panel.lyricsCleanup = null;
}

55
js/side-panel.js Normal file
View file

@ -0,0 +1,55 @@
export class SidePanelManager {
constructor() {
this.panel = document.getElementById('side-panel');
this.titleElement = document.getElementById('side-panel-title');
this.controlsElement = document.getElementById('side-panel-controls');
this.contentElement = document.getElementById('side-panel-content');
this.currentView = null; // 'queue' or 'lyrics'
}
open(view, title, renderControlsCallback, renderContentCallback) {
// If clicking the same view that is already open, close it
if (this.currentView === view && this.panel.classList.contains('active')) {
this.close();
return;
}
this.currentView = view;
this.titleElement.textContent = title;
// Clear previous content
this.controlsElement.innerHTML = '';
this.contentElement.innerHTML = '';
// Render new content
if (renderControlsCallback) renderControlsCallback(this.controlsElement);
if (renderContentCallback) renderContentCallback(this.contentElement);
this.panel.classList.add('active');
}
close() {
this.panel.classList.remove('active');
this.currentView = null;
// Optionally clear content after transition
setTimeout(() => {
if (!this.panel.classList.contains('active')) {
this.controlsElement.innerHTML = '';
this.contentElement.innerHTML = '';
}
}, 300);
}
isActive(view) {
return this.currentView === view && this.panel.classList.contains('active');
}
updateContent(view, renderContentCallback) {
if (this.isActive(view)) {
this.contentElement.innerHTML = '';
renderContentCallback(this.contentElement);
}
}
}
export const sidePanelManager = new SidePanelManager();

View file

@ -1,15 +1,12 @@
//js/ui-interactions.js
import { SVG_CLOSE, SVG_BIN, formatTime, trackDataStore, getTrackTitle, getTrackArtists } from './utils.js';
import { sidePanelManager } from './side-panel.js';
export function initializeUIInteractions(player, api) {
const sidebar = document.querySelector('.sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const hamburgerBtn = document.getElementById('hamburger-btn');
const queueBtn = document.getElementById('queue-btn');
const queueModalOverlay = document.getElementById('queue-modal-overlay');
const closeQueueBtn = document.getElementById('close-queue-btn');
const clearQueueBtn = document.getElementById('clear-queue-btn');
const queueList = document.getElementById('queue-list');
let draggedQueueIndex = null;
@ -32,113 +29,122 @@ export function initializeUIInteractions(player, api) {
}
});
// Queue modal
queueBtn.addEventListener('click', () => {
renderQueue();
queueModalOverlay.style.display = 'flex';
});
// Queue panel
const openQueuePanel = () => {
const renderControls = (container) => {
const currentQueue = player.getCurrentQueue();
const showClearBtn = currentQueue.length > 0;
closeQueueBtn.addEventListener('click', () => {
queueModalOverlay.style.display = 'none';
});
if (clearQueueBtn) {
clearQueueBtn.addEventListener('click', () => {
player.clearQueue();
renderQueue();
});
}
queueModalOverlay.addEventListener('click', e => {
if (e.target === queueModalOverlay) {
queueModalOverlay.style.display = 'none';
}
});
function renderQueue() {
const currentQueue = player.getCurrentQueue();
if (clearQueueBtn) {
clearQueueBtn.style.display = currentQueue.length > 0 ? 'block' : 'none';
}
if (currentQueue.length === 0) {
queueList.innerHTML = '<div class="placeholder-text">Queue is empty.</div>';
return;
}
const html = currentQueue.map((track, index) => {
const isPlaying = index === player.currentQueueIndex;
const trackTitle = getTrackTitle(track);
const trackArtists = getTrackArtists(track, { fallback: "Unknown" });
return `
<div class="queue-track-item ${isPlaying ? 'playing' : ''}" data-queue-index="${index}" data-track-id="${track.id}" draggable="true">
<div class="drag-handle">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="8" x2="19" y2="8"></line>
<line x1="5" y1="16" x2="19" y2="16"></line>
</svg>
</div>
<div class="track-item-info">
<img src="${api.getCoverUrl(track.album?.cover, '80')}"
class="track-item-cover" loading="lazy">
<div class="track-item-details">
<div class="title">${trackTitle}</div>
<div class="artist">${trackArtists}</div>
</div>
</div>
<div class="track-item-duration">${formatTime(track.duration)}</div>
<button class="queue-remove-btn" data-track-index="${index}" title="Remove from queue">
${SVG_BIN}
</button>
</div>
container.innerHTML = `
<button id="clear-queue-btn" class="btn-icon" title="Clear Queue" style="display: ${showClearBtn ? 'flex' : 'none'}">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
</button>
<button id="close-side-panel-btn" class="btn-icon" title="Close">
${SVG_CLOSE}
</button>
`;
}).join('');
queueList.innerHTML = html;
queueList.querySelectorAll('.queue-track-item').forEach((item) => {
const index = parseInt(item.dataset.queueIndex);
item.addEventListener('click', (e) => {
const removeBtn = e.target.closest('.queue-remove-btn');
if (removeBtn) {
e.stopPropagation();
player.removeFromQueue(index);
renderQueue();
return;
}
player.playAtIndex(index);
renderQueue();
container.querySelector('#close-side-panel-btn').addEventListener('click', () => {
sidePanelManager.close();
});
item.addEventListener('dragstart', (e) => {
draggedQueueIndex = index;
item.style.opacity = '0.5';
const clearBtn = container.querySelector('#clear-queue-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
player.clearQueue();
openQueuePanel(); // Re-render to update state
});
}
};
const renderContent = (container) => {
const currentQueue = player.getCurrentQueue();
if (currentQueue.length === 0) {
container.innerHTML = '<div class="placeholder-text">Queue is empty.</div>';
return;
}
const html = currentQueue.map((track, index) => {
const isPlaying = index === player.currentQueueIndex;
const trackTitle = getTrackTitle(track);
const trackArtists = getTrackArtists(track, { fallback: "Unknown" });
return `
<div class="queue-track-item ${isPlaying ? 'playing' : ''}" data-queue-index="${index}" data-track-id="${track.id}" draggable="true">
<div class="drag-handle">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="5" y1="8" x2="19" y2="8"></line>
<line x1="5" y1="16" x2="19" y2="16"></line>
</svg>
</div>
<div class="track-item-info">
<img src="${api.getCoverUrl(track.album?.cover, '80')}"
class="track-item-cover" loading="lazy">
<div class="track-item-details">
<div class="title">${trackTitle}</div>
<div class="artist">${trackArtists}</div>
</div>
</div>
<div class="track-item-duration">${formatTime(track.duration)}</div>
<button class="queue-remove-btn" data-track-index="${index}" title="Remove from queue">
${SVG_BIN}
</button>
</div>
`;
}).join('');
container.innerHTML = html;
container.querySelectorAll('.queue-track-item').forEach((item) => {
const index = parseInt(item.dataset.queueIndex);
item.addEventListener('click', (e) => {
const removeBtn = e.target.closest('.queue-remove-btn');
if (removeBtn) {
e.stopPropagation();
player.removeFromQueue(index);
openQueuePanel(); // Re-render
return;
}
player.playAtIndex(index);
openQueuePanel(); // Re-render to update playing state
});
item.addEventListener('dragstart', (e) => {
draggedQueueIndex = index;
item.style.opacity = '0.5';
});
item.addEventListener('dragend', () => {
item.style.opacity = '1';
});
item.addEventListener('dragover', (e) => {
e.preventDefault();
});
item.addEventListener('drop', (e) => {
e.preventDefault();
if (draggedQueueIndex !== null && draggedQueueIndex !== index) {
player.moveInQueue(draggedQueueIndex, index);
openQueuePanel(); // Re-render
}
});
});
};
item.addEventListener('dragend', () => {
item.style.opacity = '1';
});
sidePanelManager.open('queue', 'Queue', renderControls, renderContent);
};
item.addEventListener('dragover', (e) => {
e.preventDefault();
});
queueBtn.addEventListener('click', openQueuePanel);
item.addEventListener('drop', (e) => {
e.preventDefault();
if (draggedQueueIndex !== null && draggedQueueIndex !== index) {
player.moveInQueue(draggedQueueIndex, index);
renderQueue();
}
});
});
}
// Make renderQueue available globally for other modules
window.renderQueueFunction = renderQueue;
// Expose renderQueue for external updates (e.g. shuffle, add to queue)
window.renderQueueFunction = () => {
if (sidePanelManager.isActive('queue')) {
openQueuePanel(); // Re-open acts as update if active
}
};
// Search and Library tabs
document.querySelectorAll('.search-tab').forEach(tab => {

View file

@ -2798,17 +2798,22 @@ input:checked + .slider::before {
padding-top: max(var(--spacing-md), env(safe-area-inset-top));
}
}
/* Lyrics Panel */
.lyrics-panel {
/* Side Panels (Lyrics & Queue) */
:root {
--player-bar-height-desktop: 90px;
--player-bar-height-mobile: 130px;
}
.side-panel {
position: fixed;
right: 0;
top: 0;
bottom: 0;
bottom: var(--player-bar-height-desktop);
width: 400px;
max-width: 90vw;
max-width: 100vw;
background: var(--card);
border-left: 1px solid var(--border);
z-index: 3000;
z-index: 2000;
display: flex;
flex-direction: column;
transform: translateX(100%);
@ -2816,12 +2821,12 @@ input:checked + .slider::before {
box-shadow: none;
}
.lyrics-panel:not(.hidden) {
.side-panel.active {
transform: translateX(0);
box-shadow: var(--shadow-xl);
}
.lyrics-header {
.panel-header {
padding: 1rem;
border-bottom: 1px solid var(--border);
display: flex;
@ -2829,15 +2834,24 @@ input:checked + .slider::before {
align-items: center;
}
.lyrics-header h3 {
.panel-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.lyrics-controls {
.panel-controls {
display: flex;
gap: 0.5rem;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 1rem;
scroll-behavior: smooth;
}
.btn-icon {
background: transparent;
border: none;
@ -2856,13 +2870,14 @@ input:checked + .slider::before {
color: var(--foreground);
}
.lyrics-content {
flex: 1;
overflow-y: auto;
padding: 1rem;
scroll-behavior: smooth;
/* Specific Panel Overrides if needed */
.lyrics-panel {
/* Inherits side-panel */
}
.queue-panel {
/* Inherits side-panel */
}
/* Synced lyrics styling with Apple Music animations */
@ -2908,8 +2923,9 @@ input:checked + .slider::before {
/* Mobile adjustments */
@media (max-width: 768px) {
.lyrics-panel {
.side-panel {
width: 100vw;
bottom: var(--player-bar-height-mobile);
}
.synced-line {