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

View file

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

View file

@ -1,5 +1,6 @@
//js/lyrics.js //js/lyrics.js
import { getTrackTitle, getTrackArtists, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js'; import { getTrackTitle, getTrackArtists, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js';
import { sidePanelManager } from './side-panel.js';
export class LyricsManager { export class LyricsManager {
constructor(api) { constructor(api) {
@ -156,80 +157,86 @@ export class LyricsManager {
} }
} }
export function createLyricsPanel() { export async function openLyricsPanel(track, audioPlayer, lyricsManager) {
const panel = document.createElement('div'); // If no manager provided, create a temp one
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)
const manager = lyricsManager || new LyricsManager(); const manager = lyricsManager || new LyricsManager();
// Check if we are already displaying this track const renderControls = (container) => {
if (panel.dataset.lastTrackId === String(track.id) && content.querySelector('am-lyrics')) { container.innerHTML = `
// Just re-attach listeners <button id="download-lrc-btn" class="btn-icon" title="Download LRC">
setupLyricsSync(track, audioPlayer, panel, manager, content.querySelector('am-lyrics')); ${SVG_DOWNLOAD}
return; </button>
} <button id="close-side-panel-btn" class="btn-icon" title="Close">
${SVG_CLOSE}
</button>
`;
panel.dataset.lastTrackId = String(track.id); container.querySelector('#close-side-panel-btn').addEventListener('click', () => {
content.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>'; sidePanelManager.close();
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
try { });
await manager.ensureComponentLoaded();
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; try {
const artist = getTrackArtists(track); await manager.ensureComponentLoaded();
const album = track.album?.title;
const durationMs = track.duration ? Math.round(track.duration * 1000) : undefined; const title = track.title;
const isrc = track.isrc || ''; const artist = getTrackArtists(track);
const album = track.album?.title;
content.innerHTML = ''; const durationMs = track.duration ? Math.round(track.duration * 1000) : undefined;
const amLyrics = document.createElement('am-lyrics'); const isrc = track.isrc || '';
amLyrics.setAttribute('song-title', title);
amLyrics.setAttribute('song-artist', artist); container.innerHTML = '';
if (album) amLyrics.setAttribute('song-album', album); const amLyrics = document.createElement('am-lyrics');
if (durationMs) amLyrics.setAttribute('song-duration', durationMs); amLyrics.setAttribute('song-title', title);
amLyrics.setAttribute('query', `${title} ${artist}`.trim()); amLyrics.setAttribute('song-artist', artist);
if (isrc) amLyrics.setAttribute('isrc', isrc); if (album) amLyrics.setAttribute('song-album', album);
if (durationMs) amLyrics.setAttribute('song-duration', durationMs);
amLyrics.setAttribute('highlight-color', '#93c5fd'); amLyrics.setAttribute('query', `${title} ${artist}`.trim());
amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)'); if (isrc) amLyrics.setAttribute('isrc', isrc);
amLyrics.setAttribute('autoscroll', '');
amLyrics.setAttribute('interpolate', ''); amLyrics.setAttribute('highlight-color', '#93c5fd');
amLyrics.style.height = '100%'; amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)');
amLyrics.style.width = '100%'; amLyrics.setAttribute('autoscroll', '');
amLyrics.setAttribute('interpolate', '');
content.appendChild(amLyrics); amLyrics.style.height = '100%';
manager.amLyricsElement = amLyrics; amLyrics.style.width = '100%';
setupLyricsSync(track, audioPlayer, panel, manager, amLyrics); container.appendChild(amLyrics);
manager.amLyricsElement = amLyrics;
} catch (error) {
console.error('Failed to load lyrics:', error); // Clean up any previous sync
content.innerHTML = '<div class="lyrics-error">Failed to load lyrics! :(</div>'; 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) { 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('timeupdate', updateTime);
audioPlayer.addEventListener('play', onPlay); audioPlayer.addEventListener('play', onPlay);
audioPlayer.addEventListener('pause', onPause); audioPlayer.addEventListener('pause', onPause);
@ -281,9 +283,6 @@ function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) {
panel.lyricsPauseHandler = onPause; panel.lyricsPauseHandler = onPause;
panel.lyricsSeekHandler = updateTime; panel.lyricsSeekHandler = updateTime;
// We also need to remove these in clearLyricsPanelSync!
// The current clearLyricsPanelSync only removes 'timeupdate'.
amLyrics.addEventListener('line-click', (e) => { amLyrics.addEventListener('line-click', (e) => {
if (e.detail && e.detail.timestamp) { if (e.detail && e.detail.timestamp) {
audioPlayer.currentTime = e.detail.timestamp / 1000; audioPlayer.currentTime = e.detail.timestamp / 1000;
@ -299,7 +298,6 @@ function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) {
if (lyricsManager.animationFrameId) { if (lyricsManager.animationFrameId) {
cancelAnimationFrame(lyricsManager.animationFrameId); cancelAnimationFrame(lyricsManager.animationFrameId);
} }
// Also remove listeners
audioPlayer.removeEventListener('timeupdate', updateTime); audioPlayer.removeEventListener('timeupdate', updateTime);
audioPlayer.removeEventListener('play', onPlay); audioPlayer.removeEventListener('play', onPlay);
audioPlayer.removeEventListener('pause', onPause); audioPlayer.removeEventListener('pause', onPause);
@ -309,11 +307,7 @@ function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) {
export function clearLyricsPanelSync(audioPlayer, panel) { export function clearLyricsPanelSync(audioPlayer, panel) {
if (panel.lyricsUpdateHandler) { if (panel && panel.lyricsCleanup) {
audioPlayer.removeEventListener('timeupdate', panel.lyricsUpdateHandler);
panel.lyricsUpdateHandler = null;
}
if (panel.lyricsCleanup) {
panel.lyricsCleanup(); panel.lyricsCleanup();
panel.lyricsCleanup = null; 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 //js/ui-interactions.js
import { SVG_CLOSE, SVG_BIN, formatTime, trackDataStore, getTrackTitle, getTrackArtists } from './utils.js'; import { SVG_CLOSE, SVG_BIN, formatTime, trackDataStore, getTrackTitle, getTrackArtists } from './utils.js';
import { sidePanelManager } from './side-panel.js';
export function initializeUIInteractions(player, api) { export function initializeUIInteractions(player, api) {
const sidebar = document.querySelector('.sidebar'); const sidebar = document.querySelector('.sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay'); const sidebarOverlay = document.getElementById('sidebar-overlay');
const hamburgerBtn = document.getElementById('hamburger-btn'); const hamburgerBtn = document.getElementById('hamburger-btn');
const queueBtn = document.getElementById('queue-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; let draggedQueueIndex = null;
@ -32,113 +29,122 @@ export function initializeUIInteractions(player, api) {
} }
}); });
// Queue modal // Queue panel
queueBtn.addEventListener('click', () => { const openQueuePanel = () => {
renderQueue(); const renderControls = (container) => {
queueModalOverlay.style.display = 'flex'; const currentQueue = player.getCurrentQueue();
}); const showClearBtn = currentQueue.length > 0;
closeQueueBtn.addEventListener('click', () => { container.innerHTML = `
queueModalOverlay.style.display = 'none'; <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>
if (clearQueueBtn) { <button id="close-side-panel-btn" class="btn-icon" title="Close">
clearQueueBtn.addEventListener('click', () => { ${SVG_CLOSE}
player.clearQueue(); </button>
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>
`; `;
}).join('');
queueList.innerHTML = html; container.querySelector('#close-side-panel-btn').addEventListener('click', () => {
sidePanelManager.close();
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();
}); });
item.addEventListener('dragstart', (e) => { const clearBtn = container.querySelector('#clear-queue-btn');
draggedQueueIndex = index; if (clearBtn) {
item.style.opacity = '0.5'; 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', () => { sidePanelManager.open('queue', 'Queue', renderControls, renderContent);
item.style.opacity = '1'; };
});
item.addEventListener('dragover', (e) => { queueBtn.addEventListener('click', openQueuePanel);
e.preventDefault();
});
item.addEventListener('drop', (e) => { // Expose renderQueue for external updates (e.g. shuffle, add to queue)
e.preventDefault(); window.renderQueueFunction = () => {
if (draggedQueueIndex !== null && draggedQueueIndex !== index) { if (sidePanelManager.isActive('queue')) {
player.moveInQueue(draggedQueueIndex, index); openQueuePanel(); // Re-open acts as update if active
renderQueue(); }
} };
});
});
}
// Make renderQueue available globally for other modules
window.renderQueueFunction = renderQueue;
// Search and Library tabs // Search and Library tabs
document.querySelectorAll('.search-tab').forEach(tab => { 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)); padding-top: max(var(--spacing-md), env(safe-area-inset-top));
} }
} }
/* Lyrics Panel */ /* Side Panels (Lyrics & Queue) */
.lyrics-panel { :root {
--player-bar-height-desktop: 90px;
--player-bar-height-mobile: 130px;
}
.side-panel {
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: var(--player-bar-height-desktop);
width: 400px; width: 400px;
max-width: 90vw; max-width: 100vw;
background: var(--card); background: var(--card);
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
z-index: 3000; z-index: 2000;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transform: translateX(100%); transform: translateX(100%);
@ -2816,12 +2821,12 @@ input:checked + .slider::before {
box-shadow: none; box-shadow: none;
} }
.lyrics-panel:not(.hidden) { .side-panel.active {
transform: translateX(0); transform: translateX(0);
box-shadow: var(--shadow-xl); box-shadow: var(--shadow-xl);
} }
.lyrics-header { .panel-header {
padding: 1rem; padding: 1rem;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
display: flex; display: flex;
@ -2829,15 +2834,24 @@ input:checked + .slider::before {
align-items: center; align-items: center;
} }
.lyrics-header h3 { .panel-header h3 {
margin: 0; margin: 0;
font-size: 1.25rem;
font-weight: 600;
} }
.lyrics-controls { .panel-controls {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
} }
.panel-content {
flex: 1;
overflow-y: auto;
padding: 1rem;
scroll-behavior: smooth;
}
.btn-icon { .btn-icon {
background: transparent; background: transparent;
border: none; border: none;
@ -2856,13 +2870,14 @@ input:checked + .slider::before {
color: var(--foreground); color: var(--foreground);
} }
.lyrics-content { /* Specific Panel Overrides if needed */
flex: 1; .lyrics-panel {
overflow-y: auto; /* Inherits side-panel */
padding: 1rem;
scroll-behavior: smooth;
} }
.queue-panel {
/* Inherits side-panel */
}
/* Synced lyrics styling with Apple Music animations */ /* Synced lyrics styling with Apple Music animations */
@ -2908,8 +2923,9 @@ input:checked + .slider::before {
/* Mobile adjustments */ /* Mobile adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.lyrics-panel { .side-panel {
width: 100vw; width: 100vw;
bottom: var(--player-bar-height-mobile);
} }
.synced-line { .synced-line {