IMP: fullscreen lyrics, various css fixes

This commit is contained in:
Julien Maille 2026-01-01 23:50:06 +01:00
parent b828322a72
commit 801fa264cd
4 changed files with 63 additions and 236 deletions

View file

@ -65,8 +65,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="fullscreen-lyrics-container" id="fullscreen-lyrics-container" style="display: none;">
</div>
</div> </div>
</div> </div>

View file

@ -193,125 +193,16 @@ export async function openLyricsPanel(track, audioPlayer, lyricsManager) {
}; };
const renderContent = async (container) => { const renderContent = async (container) => {
container.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>'; // Clean up any previous sync (though sidePanelManager might handle cleanup, we ensure it here)
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
try { await renderLyricsComponent(container, track, audioPlayer, manager);
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); sidePanelManager.open('lyrics', 'Lyrics', renderControls, renderContent);
} }
function setupLyricsSync(track, audioPlayer, panel, lyricsManager, amLyrics) { async function renderLyricsComponent(container, track, audioPlayer, lyricsManager) {
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);
}
};
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;
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);
}
audioPlayer.removeEventListener('timeupdate', updateTime);
audioPlayer.removeEventListener('play', onPlay);
audioPlayer.removeEventListener('pause', onPause);
audioPlayer.removeEventListener('seeked', updateTime);
};
}
export function clearLyricsPanelSync(audioPlayer, panel) {
if (panel && panel.lyricsCleanup) {
panel.lyricsCleanup();
panel.lyricsCleanup = null;
}
}
export async function renderLyricsInFullscreen(track, audioPlayer, lyricsManager, container) {
container.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>'; container.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
try { try {
@ -340,21 +231,25 @@ export async function renderLyricsInFullscreen(track, audioPlayer, lyricsManager
amLyrics.style.width = '100%'; amLyrics.style.width = '100%';
container.appendChild(amLyrics); container.appendChild(amLyrics);
setupFullscreenLyricsSync(track, audioPlayer, container, lyricsManager, amLyrics); const cleanup = setupSync(track, audioPlayer, amLyrics);
// Attach cleanup to container for easy access
container.lyricsCleanup = cleanup;
return amLyrics; return amLyrics;
} catch (error) { } catch (error) {
console.error('Failed to load lyrics in fullscreen:', error); console.error('Failed to load lyrics:', error);
container.innerHTML = '<div class="lyrics-error">Failed to load lyrics</div>'; container.innerHTML = '<div class="lyrics-error">Failed to load lyrics</div>';
return null; return null;
} }
} }
function setupFullscreenLyricsSync(track, audioPlayer, container, lyricsManager, amLyrics) { function setupSync(track, audioPlayer, amLyrics) {
let baseTimeMs = 0; let baseTimeMs = 0;
let lastTimestamp = performance.now(); let lastTimestamp = performance.now();
let animationFrameId = null;
const updateTime = () => { const updateTime = () => {
const currentMs = audioPlayer.currentTime * 1000; const currentMs = audioPlayer.currentTime * 1000;
baseTimeMs = currentMs; baseTimeMs = currentMs;
@ -368,7 +263,7 @@ function setupFullscreenLyricsSync(track, audioPlayer, container, lyricsManager,
const elapsed = now - lastTimestamp; const elapsed = now - lastTimestamp;
const nextMs = baseTimeMs + elapsed; const nextMs = baseTimeMs + elapsed;
amLyrics.currentTime = nextMs; amLyrics.currentTime = nextMs;
lyricsManager.fullscreenAnimationFrameId = requestAnimationFrame(tick); animationFrameId = requestAnimationFrame(tick);
} }
}; };
@ -379,9 +274,16 @@ function setupFullscreenLyricsSync(track, audioPlayer, container, lyricsManager,
}; };
const onPause = () => { const onPause = () => {
if (lyricsManager.fullscreenAnimationFrameId) { if (animationFrameId) {
cancelAnimationFrame(lyricsManager.fullscreenAnimationFrameId); cancelAnimationFrame(animationFrameId);
lyricsManager.fullscreenAnimationFrameId = null; animationFrameId = null;
}
};
const onLineClick = (e) => {
if (e.detail && e.detail.timestamp) {
audioPlayer.currentTime = e.detail.timestamp / 1000;
audioPlayer.play();
} }
}; };
@ -389,33 +291,26 @@ function setupFullscreenLyricsSync(track, audioPlayer, container, lyricsManager,
audioPlayer.addEventListener('play', onPlay); audioPlayer.addEventListener('play', onPlay);
audioPlayer.addEventListener('pause', onPause); audioPlayer.addEventListener('pause', onPause);
audioPlayer.addEventListener('seeked', updateTime); audioPlayer.addEventListener('seeked', updateTime);
amLyrics.addEventListener('line-click', onLineClick);
container.lyricsUpdateHandler = updateTime;
container.lyricsPlayHandler = onPlay; if (!audioPlayer.paused) {
container.lyricsPauseHandler = onPause; tick();
container.lyricsSeekHandler = updateTime; }
container.lyricsCleanup = () => { return () => {
if (lyricsManager.fullscreenAnimationFrameId) { if (animationFrameId) {
cancelAnimationFrame(lyricsManager.fullscreenAnimationFrameId); cancelAnimationFrame(animationFrameId);
lyricsManager.fullscreenAnimationFrameId = null;
} }
audioPlayer.removeEventListener('timeupdate', updateTime); audioPlayer.removeEventListener('timeupdate', updateTime);
audioPlayer.removeEventListener('play', onPlay); audioPlayer.removeEventListener('play', onPlay);
audioPlayer.removeEventListener('pause', onPause); audioPlayer.removeEventListener('pause', onPause);
audioPlayer.removeEventListener('seeked', updateTime); audioPlayer.removeEventListener('seeked', updateTime);
amLyrics.removeEventListener('line-click', onLineClick);
}; };
}
amLyrics.addEventListener('line-click', (e) => { export async function renderLyricsInFullscreen(track, audioPlayer, lyricsManager, container) {
if (e.detail && e.detail.timestamp) { return renderLyricsComponent(container, track, audioPlayer, lyricsManager);
audioPlayer.currentTime = e.detail.timestamp / 1000;
audioPlayer.play();
}
});
if (!audioPlayer.paused) {
tick();
}
} }
export function clearFullscreenLyricsSync(container) { export function clearFullscreenLyricsSync(container) {
@ -424,3 +319,10 @@ export function clearFullscreenLyricsSync(container) {
container.lyricsCleanup = null; container.lyricsCleanup = null;
} }
} }
export function clearLyricsPanelSync(audioPlayer, panel) {
if (panel && panel.lyricsCleanup) {
panel.lyricsCleanup();
panel.lyricsCleanup = null;
}
}

View file

@ -1,6 +1,6 @@
//js/ui.js //js/ui.js
import { SVG_PLAY, SVG_DOWNLOAD, SVG_MENU, SVG_HEART, formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackArtists, getTrackTitle, calculateTotalDuration, formatDuration } from './utils.js'; import { SVG_PLAY, SVG_DOWNLOAD, SVG_MENU, SVG_HEART, formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackArtists, getTrackTitle, calculateTotalDuration, formatDuration } from './utils.js';
import { renderLyricsInFullscreen, clearFullscreenLyricsSync } from './lyrics.js'; import { openLyricsPanel } from './lyrics.js';
import { recentActivityManager, backgroundSettings, trackListSettings } from './storage.js'; import { recentActivityManager, backgroundSettings, trackListSettings } from './storage.js';
import { db } from './db.js'; import { db } from './db.js';
@ -491,25 +491,11 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
if (lyricsManager && audioPlayer) { if (lyricsManager && audioPlayer) {
lyricsToggleBtn.style.display = 'flex'; lyricsToggleBtn.style.display = 'flex';
lyricsContainer.style.display = 'none';
lyricsContainer.classList.remove('active');
lyricsToggleBtn.classList.remove('active'); lyricsToggleBtn.classList.remove('active');
const toggleLyrics = async () => { const toggleLyrics = () => {
const isActive = lyricsContainer.classList.contains('active'); openLyricsPanel(track, audioPlayer, lyricsManager);
if (isActive) { lyricsToggleBtn.classList.toggle('active');
lyricsContainer.classList.remove('active');
lyricsToggleBtn.classList.remove('active');
setTimeout(() => {
lyricsContainer.style.display = 'none';
clearFullscreenLyricsSync(lyricsContainer);
}, 300);
} else {
lyricsContainer.style.display = 'block';
setTimeout(() => lyricsContainer.classList.add('active'), 10);
lyricsToggleBtn.classList.add('active');
await renderLyricsInFullscreen(track, audioPlayer, lyricsManager, lyricsContainer);
}
}; };
const newToggleBtn = lyricsToggleBtn.cloneNode(true); const newToggleBtn = lyricsToggleBtn.cloneNode(true);
@ -524,13 +510,6 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
closeFullscreenCover() { closeFullscreenCover() {
const overlay = document.getElementById('fullscreen-cover-overlay'); const overlay = document.getElementById('fullscreen-cover-overlay');
const lyricsContainer = document.getElementById('fullscreen-lyrics-container');
clearFullscreenLyricsSync(lyricsContainer);
lyricsContainer.style.display = 'none';
lyricsContainer.classList.remove('active');
lyricsContainer.innderHTML = '';
overlay.style.display = 'none'; overlay.style.display = 'none';
} }
@ -1198,7 +1177,8 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
// Set play button action // Set play button action
playBtn.onclick = () => { playBtn.onclick = () => {
player.playTracks(tracks, 0); this.player.setQueue(tracks, 0);
this.player.playTrackFromQueue();
}; };
document.title = `${displayTitle} - Monochrome`; document.title = `${displayTitle} - Monochrome`;

View file

@ -1,5 +1,6 @@
:root { :root {
color-scheme: light dark;
--spacing-xs: 0.4rem; --spacing-xs: 0.4rem;
--spacing-sm: 0.5rem; --spacing-sm: 0.5rem;
--spacing-md: 1rem; --spacing-md: 1rem;
@ -16,6 +17,7 @@
} }
:root[data-theme="monochrome"] { :root[data-theme="monochrome"] {
color-scheme: dark;
--background: #000; --background: #000;
--foreground: #fafafa; --foreground: #fafafa;
--card: #111; --card: #111;
@ -37,6 +39,7 @@
} }
:root[data-theme="dark"] { :root[data-theme="dark"] {
color-scheme: dark;
--background: #0a0a0a; --background: #0a0a0a;
--foreground: #ededed; --foreground: #ededed;
--card: #1a1a1a; --card: #1a1a1a;
@ -57,6 +60,7 @@
} }
:root[data-theme="ocean"] { :root[data-theme="ocean"] {
color-scheme: dark;
--background: #0c1821; --background: #0c1821;
--foreground: #e0f4ff; --foreground: #e0f4ff;
--card: #1b2838; --card: #1b2838;
@ -77,6 +81,7 @@
} }
:root[data-theme="purple"] { :root[data-theme="purple"] {
color-scheme: dark;
--background: #0f0514; --background: #0f0514;
--foreground: #f3e8ff; --foreground: #f3e8ff;
--card: #1e0a2e; --card: #1e0a2e;
@ -97,6 +102,7 @@
} }
:root[data-theme="forest"] { :root[data-theme="forest"] {
color-scheme: dark;
--background: #0a1409; --background: #0a1409;
--foreground: #e8f5e9; --foreground: #e8f5e9;
--card: #1a2e1a; --card: #1a2e1a;
@ -117,6 +123,7 @@
} }
:root[data-theme="mocha"] { :root[data-theme="mocha"] {
color-scheme: dark;
--background: #1e1e2e; --background: #1e1e2e;
--foreground: #cdd6f4; --foreground: #cdd6f4;
--card: #313244; --card: #313244;
@ -137,6 +144,7 @@
} }
:root[data-theme="machiatto"] { :root[data-theme="machiatto"] {
color-scheme: dark;
--background: #24273a; --background: #24273a;
--foreground: #cad3f5; --foreground: #cad3f5;
--card: #363a4f; --card: #363a4f;
@ -157,6 +165,7 @@
} }
:root[data-theme="frappe"] { :root[data-theme="frappe"] {
color-scheme: dark;
--background: #303446; --background: #303446;
--foreground:#c6d0f5; --foreground:#c6d0f5;
--card: #414559; --card: #414559;
@ -177,6 +186,7 @@
} }
:root[data-theme="latte"] { :root[data-theme="latte"] {
color-scheme: light;
--background: #eff1f5; --background: #eff1f5;
--foreground: #4c4f69; --foreground: #4c4f69;
--card: #ccd0da; --card: #ccd0da;
@ -197,6 +207,7 @@
} }
:root[data-theme="light"] { :root[data-theme="light"] {
color-scheme: light;
--background: #ffffff; --background: #ffffff;
--foreground: #000000; --foreground: #000000;
--card: #f4f4f5; --card: #f4f4f5;
@ -2893,7 +2904,7 @@ input:checked + .slider::before {
max-width: 100vw; max-width: 100vw;
background: var(--card); background: var(--card);
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
z-index: 2000; z-index: 2050;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transform: translateX(100%); transform: translateX(100%);
@ -3266,54 +3277,6 @@ img:not([src]), img[src=''] {
transition: flex 0.3s ease; transition: flex 0.3s ease;
} }
.fullscreen-lyrics-container {
position: absolute;
right: 0;
top: 0;
width: 0;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(20px);
border-left: 1px solid rgba(255, 255, 255, 0.1);
overflow-y: auto;
overflow-x: hidden;
transition: width 0.3s ease;
padding: 0;
}
.fullscreen-lyrics-container.active {
width: 400px;
padding: 2rem;
}
.fullscreen-lyrics-container > * {
width: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 2rem;
box-sizing: border-box;
}
.fullscreen-lyrics-container::-webkit-scrollbar {
width: 8px;
}
.fullscreen-lyrics-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.fullscreen-lyrics-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.fullscreen-lyrics-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.fullscreen-lyrics-toggle { .fullscreen-lyrics-toggle {
position: absolute; position: absolute;
top: 1rem; top: 1rem;
@ -3345,22 +3308,6 @@ img:not([src]), img[src=''] {
flex-direction: column; flex-direction: column;
} }
.fullscreen-lyrics-container {
top: auto;
bottom: 0;
left: 0;
width: 100%;
height: 0;
border-left: none;
border-top: 1px solid rgba(255, 255, 255, 0.1);
transition: height 0.3s ease;
}
.fullscreen-lyrics-container.active {
width: 50vh;
width: 100%;
}
.fullscreen-lyrics-toggle { .fullscreen-lyrics-toggle {
right: 3.5rem; right: 3.5rem;
} }