Merge branch 'main' of github.com:SamidyFR/monochrome
This commit is contained in:
commit
0d8d362ac9
8 changed files with 307 additions and 270 deletions
18
index.html
18
index.html
|
|
@ -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">×</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>
|
||||
|
||||
|
|
|
|||
76
js/app.js
76
js/app.js
|
|
@ -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
|
||||
|
|
|
|||
8
js/db.js
8
js/db.js
|
|
@ -169,7 +169,7 @@ export class MusicDatabase {
|
|||
releaseDate: item.album.releaseDate || null
|
||||
} : null,
|
||||
// Fallback date
|
||||
streamStartDate: item.streamStartDate,
|
||||
streamStartDate: item.streamStartDate || null,
|
||||
// Keep version if exists
|
||||
version: item.version || null
|
||||
};
|
||||
|
|
@ -180,12 +180,12 @@ export class MusicDatabase {
|
|||
...base,
|
||||
title: item.title,
|
||||
cover: item.cover,
|
||||
releaseDate: item.releaseDate,
|
||||
releaseDate: item.releaseDate || null,
|
||||
explicit: item.explicit,
|
||||
// UI uses singular 'artist'
|
||||
artist: item.artist ? { name: item.artist.name, id: item.artist.id } : (item.artists?.[0] ? { name: item.artists[0].name, id: item.artists[0].id } : null),
|
||||
// Keep type and track count for UI labels
|
||||
type: item.type,
|
||||
type: item.type || null,
|
||||
numberOfTracks: item.numberOfTracks
|
||||
};
|
||||
}
|
||||
|
|
@ -194,7 +194,7 @@ export class MusicDatabase {
|
|||
return {
|
||||
...base,
|
||||
name: item.name,
|
||||
picture: item.picture || item.image // Handle both just in case
|
||||
picture: item.picture || item.image || null // Handle both just in case
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ export class SyncManager {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
|
|
|
|||
162
js/lyrics.js
162
js/lyrics.js
|
|
@ -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
55
js/side-panel.js
Normal 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();
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
46
styles.css
46
styles.css
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue