UI: reorganize bottom controls and improve lyrics panel behavior

This commit is contained in:
Julien Maille 2025-12-27 23:18:53 +01:00
parent 84adbaa811
commit 91da9b887d
5 changed files with 178 additions and 86 deletions

View file

@ -519,29 +519,36 @@
</div>
</div>
<div class="volume-controls">
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Library" style="display: none;">
</button>
<button id="download-current-btn" title="Download current track" class="desktop-only">
<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<button id="cast-btn" title="Cast" class="desktop-only">
<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="M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path>
<path d="M2 12a9 9 0 0 1 9 9"></path>
<path d="M2 17a5 5 0 0 1 5 5"></path>
<path d="M2 22h.01"></path>
</svg>
</button>
<button id="queue-btn" title="Queue">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-icon lucide-list"><path d="M3 5h.01"/><path d="M3 12h.01"/><path d="M3 19h.01"/><path d="M8 5h13"/><path d="M8 12h13"/><path d="M8 19h13"/></svg>
</button>
<button id="volume-btn" title="Mute" class="desktop-only"></button>
<div id="volume-bar" class="volume-bar desktop-only">
<div id="volume-fill" class="volume-fill"></div>
<div class="player-actions-row">
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Library" style="display: none;">
</button>
<button id="toggle-lyrics-btn" title="Lyrics">
<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" class="lucide lucide-mic"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg>
</button>
<button id="download-current-btn" title="Download current track" class="desktop-only">
<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<button id="cast-btn" title="Cast" class="desktop-only">
<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="M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path>
<path d="M2 12a9 9 0 0 1 9 9"></path>
<path d="M2 17a5 5 0 0 1 5 5"></path>
<path d="M2 22h.01"></path>
</svg>
</button>
<button id="queue-btn" title="Queue">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-icon lucide-list"><path d="M3 5h.01"/><path d="M3 12h.01"/><path d="M3 19h.01"/><path d="M8 5h13"/><path d="M8 12h13"/><path d="M8 19h13"/></svg>
</button>
</div>
<div class="volume-slider-row desktop-only">
<button id="volume-btn" title="Mute"></button>
<div id="volume-bar" class="volume-bar">
<div id="volume-fill" class="volume-fill"></div>
</div>
</div>
</div>
</footer>

View file

@ -228,7 +228,7 @@ document.addEventListener('DOMContentLoaded', async () => {
lyricsPanel.classList.toggle('hidden');
if (isHidden) {
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel);
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager);
} else {
clearLyricsPanelSync(audioPlayer, lyricsPanel);
}
@ -257,6 +257,23 @@ document.addEventListener('DOMContentLoaded', async () => {
ui.closeFullscreenCover();
});
document.getElementById('toggle-lyrics-btn')?.addEventListener('click', async (e) => {
e.stopPropagation();
if (!player.currentTrack) {
alert('No track is currently playing');
return;
}
const isHidden = lyricsPanel.classList.contains('hidden');
lyricsPanel.classList.toggle('hidden');
if (isHidden) {
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager);
} else {
clearLyricsPanelSync(audioPlayer, lyricsPanel);
}
});
document.getElementById('close-lyrics-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
lyricsPanel.classList.add('hidden');
@ -308,12 +325,8 @@ document.addEventListener('DOMContentLoaded', async () => {
// Update lyrics panel if it's open
if (!lyricsPanel.classList.contains('hidden')) {
const mode = nowPlayingSettings.getMode();
if (mode === 'lyrics') {
clearLyricsPanelSync(audioPlayer, lyricsPanel);
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel);
}
clearLyricsPanelSync(audioPlayer, lyricsPanel);
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager);
}
// Update Fullscreen/Enlarged Cover if it's open

View file

@ -180,14 +180,24 @@ export function createLyricsPanel() {
return panel;
}
export async function showSyncedLyricsPanel(track, audioPlayer, 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();
// 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;
}
panel.dataset.lastTrackId = String(track.id);
content.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
const lyricsManager = new LyricsManager();
try {
await lyricsManager.ensureComponentLoaded();
await manager.ensureComponentLoaded();
const title = track.title;
const artist = getTrackArtists(track);
@ -203,6 +213,7 @@ export async function showSyncedLyricsPanel(track, audioPlayer, panel) {
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', '');
@ -211,57 +222,9 @@ export async function showSyncedLyricsPanel(track, audioPlayer, panel) {
amLyrics.style.width = '100%';
content.appendChild(amLyrics);
lyricsManager.amLyricsElement = amLyrics;
manager.amLyricsElement = amLyrics;
let baseTimeMs = 0;
let lastTimestamp = performance.now();
const updateTime = () => {
const currentMs = audioPlayer.currentTime * 1000;
baseTimeMs = currentMs;
lastTimestamp = performance.now();
amLyrics.currentTime = currentMs;
};
const tick = () => {
if (!audioPlayer.paused) {
const now = performance.now();
const elapsed = now - lastTimestamp;
const nextMs = baseTimeMs + elapsed;
amLyrics.currentTime = nextMs;
lyricsManager.animationFrameId = requestAnimationFrame(tick);
}
};
audioPlayer.addEventListener('timeupdate', updateTime);
audioPlayer.addEventListener('play', () => {
baseTimeMs = audioPlayer.currentTime * 1000;
lastTimestamp = performance.now();
tick();
});
audioPlayer.addEventListener('pause', () => {
if (lyricsManager.animationFrameId) {
cancelAnimationFrame(lyricsManager.animationFrameId);
}
});
audioPlayer.addEventListener('seeked', updateTime);
amLyrics.addEventListener('line-click', (e) => {
if (e.detail && e.detail.timestamp) {
audioPlayer.currentTime = e.detail.timestamp / 1000;
audioPlayer.play();
}
});
if (!audioPlayer.paused) {
tick();
}
panel.lyricsCleanup = () => {
if (lyricsManager.animationFrameId) {
cancelAnimationFrame(lyricsManager.animationFrameId);
}
};
setupLyricsSync(track, audioPlayer, panel, manager, amLyrics);
} catch (error) {
console.error('Failed to load lyrics:', error);
@ -269,6 +232,82 @@ export async function showSyncedLyricsPanel(track, audioPlayer, panel) {
}
}
function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) {
let baseTimeMs = 0;
let lastTimestamp = performance.now();
const updateTime = () => {
const currentMs = audioPlayer.currentTime * 1000;
baseTimeMs = currentMs;
lastTimestamp = performance.now();
amLyrics.currentTime = currentMs;
};
const tick = () => {
if (!audioPlayer.paused) {
const now = performance.now();
const elapsed = now - lastTimestamp;
const nextMs = baseTimeMs + elapsed;
amLyrics.currentTime = nextMs;
lyricsManager.animationFrameId = requestAnimationFrame(tick);
}
};
const onPlay = () => {
baseTimeMs = audioPlayer.currentTime * 1000;
lastTimestamp = performance.now();
tick();
};
const onPause = () => {
if (lyricsManager.animationFrameId) {
cancelAnimationFrame(lyricsManager.animationFrameId);
}
};
// 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);
audioPlayer.addEventListener('seeked', updateTime);
// Store handlers for removal
panel.lyricsUpdateHandler = updateTime;
panel.lyricsPlayHandler = onPlay;
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;
audioPlayer.play();
}
});
if (!audioPlayer.paused) {
tick();
}
panel.lyricsCleanup = () => {
if (lyricsManager.animationFrameId) {
cancelAnimationFrame(lyricsManager.animationFrameId);
}
// Also remove listeners
audioPlayer.removeEventListener('timeupdate', updateTime);
audioPlayer.removeEventListener('play', onPlay);
audioPlayer.removeEventListener('pause', onPause);
audioPlayer.removeEventListener('seeked', updateTime);
};
}
export function clearLyricsPanelSync(audioPlayer, panel) {
if (panel.lyricsUpdateHandler) {
audioPlayer.removeEventListener('timeupdate', panel.lyricsUpdateHandler);

View file

@ -1276,7 +1276,9 @@ input:checked + .slider::before {
color: var(--muted-foreground);
cursor: pointer;
transition: all var(--transition);
padding: 0.5rem;
padding: 0.25rem;
width: 28px;
height: 28px;
border-radius: var(--radius);
display: flex;
align-items: center;
@ -3205,3 +3207,31 @@ input:checked + .slider::before {
}
}
/* Updated Volume Controls Layout */
.volume-controls {
flex-direction: column !important;
align-items: flex-end !important;
gap: 0.5rem !important;
justify-content: center !important;
}
.player-actions-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.volume-slider-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
/* Ensure buttons in rows behave correctly */
.player-actions-row button,
.volume-slider-row button {
display: flex;
align-items: center;
justify-content: center;
}

5
sw.js
View file

@ -19,9 +19,12 @@ const urlsToCache = [
'/js/lastfm.js',
'/js/lyrics.js',
'/js/downloads.js',
'/js/db.js',
'/js/metadata.js',
'/manifest.json',
'/assets/logo.svg',
'/assets/appicon.png'
'/assets/appicon.png',
'/assets/96.png'
];
self.addEventListener('install', event => {