IMP: refactored queue list and lyrics panel in the same ui
This commit is contained in:
parent
16034014a0
commit
82b4afb149
6 changed files with 302 additions and 265 deletions
18
index.html
18
index.html
|
|
@ -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">×</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>
|
||||||
|
|
||||||
|
|
|
||||||
76
js/app.js
76
js/app.js
|
|
@ -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
|
||||||
|
|
|
||||||
162
js/lyrics.js
162
js/lyrics.js
|
|
@ -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
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
|
//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 => {
|
||||||
|
|
|
||||||
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));
|
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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue