diff --git a/js/app.js b/js/app.js
index 62a90dd..bfd5076 100644
--- a/js/app.js
+++ b/js/app.js
@@ -3,7 +3,7 @@ import { apiSettings, themeManager, nowPlayingSettings } from './storage.js';
import { UIRenderer } from './ui.js';
import { Player } from './player.js';
import { LastFMScrobbler } from './lastfm.js';
-import { LyricsManager, createLyricsPanel, showKaraokeView } from './lyrics.js';
+import { LyricsManager, createLyricsPanel, showKaraokeView, showSyncedLyricsPanel, clearLyricsPanelSync } from './lyrics.js';
import { createRouter, updateTabTitle } from './router.js';
import { initializeSettings } from './settings.js';
import { initializePlayerEvents, initializeTrackInteractions } from './events.js';
@@ -75,7 +75,7 @@ function initializeCasting(audioPlayer, castBtn) {
}
}
-function initializeKeyboardShortcuts(player, audioPlayer) {
+function initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel) {
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea')) return;
@@ -128,9 +128,9 @@ function initializeKeyboardShortcuts(player, audioPlayer) {
case 'escape':
document.getElementById('search-input')?.blur();
document.getElementById('queue-modal-overlay').style.display = 'none';
- const lyricsPanel = document.getElementById('lyrics-panel');
if (lyricsPanel) {
lyricsPanel.classList.add('hidden');
+ clearLyricsPanelSync(audioPlayer, lyricsPanel);
}
const karaokeView = document.getElementById('karaoke-view');
if (karaokeView) {
@@ -205,7 +205,7 @@ document.addEventListener('DOMContentLoaded', async () => {
initializePlayerEvents(player, audioPlayer, scrobbler);
initializeTrackInteractions(player, api, document.querySelector('.main-content'), document.getElementById('context-menu'));
initializeUIInteractions(player, api);
- initializeKeyboardShortcuts(player, audioPlayer);
+ initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel);
initializeMediaSessionHandlers(player);
const castBtn = document.getElementById('cast-btn');
@@ -221,6 +221,8 @@ document.addEventListener('DOMContentLoaded', async () => {
if (mode === 'karaoke') {
lyricsPanel.classList.add('hidden');
+ clearLyricsPanelSync(audioPlayer, lyricsPanel);
+
const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id);
if (lyricsData) {
showKaraokeView(player.currentTrack, lyricsData, audioPlayer);
@@ -239,18 +241,13 @@ document.addEventListener('DOMContentLoaded', async () => {
if (lyricsData) {
lyricsManager.currentLyrics = lyricsData;
-
- if (lyricsData.lyrics) {
- const lines = lyricsData.lyrics.split('\n');
- content.innerHTML = lines.map(line =>
- `
${line || ' '}
`
- ).join('');
- } else {
- content.innerHTML = 'No lyrics available
';
- }
+ showSyncedLyricsPanel(lyricsData, audioPlayer, lyricsPanel);
} else {
content.innerHTML = 'Failed to load lyrics
';
}
+ } else {
+ // Clear sync when hiding
+ clearLyricsPanelSync(audioPlayer, lyricsPanel);
}
}
});
@@ -258,6 +255,7 @@ document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('close-lyrics-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
lyricsPanel.classList.add('hidden');
+ clearLyricsPanelSync(audioPlayer, lyricsPanel);
});
document.getElementById('download-lrc-btn')?.addEventListener('click', (e) => {
@@ -271,6 +269,36 @@ document.addEventListener('DOMContentLoaded', async () => {
downloadCurrentTrack(player.currentTrack, player.quality, api, lyricsManager);
});
+ // Auto-update lyrics when track changes
+ let previousTrackId = null;
+ audioPlayer.addEventListener('play', async () => {
+ if (!player.currentTrack) return;
+
+ const currentTrackId = player.currentTrack.id;
+ if (currentTrackId === previousTrackId) return;
+ previousTrackId = currentTrackId;
+
+ // Update lyrics panel if it's open
+ if (!lyricsPanel.classList.contains('hidden')) {
+ const mode = nowPlayingSettings.getMode();
+ if (mode === 'lyrics') {
+ const content = lyricsPanel.querySelector('.lyrics-content');
+ content.innerHTML = 'Loading lyrics...
';
+
+ const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id);
+
+ if (lyricsData) {
+ lyricsManager.currentLyrics = lyricsData;
+ // Clear old sync before showing new
+ clearLyricsPanelSync(audioPlayer, lyricsPanel);
+ showSyncedLyricsPanel(lyricsData, audioPlayer, lyricsPanel);
+ } else {
+ content.innerHTML = 'No lyrics available for this track
';
+ }
+ }
+ }
+ });
+
document.addEventListener('click', async (e) => {
if (e.target.closest('#play-album-btn')) {
const btn = e.target.closest('#play-album-btn');
diff --git a/js/lyrics.js b/js/lyrics.js
index c319c68..31d0308 100644
--- a/js/lyrics.js
+++ b/js/lyrics.js
@@ -125,6 +125,74 @@ export function createLyricsPanel() {
return panel;
}
+export function showSyncedLyricsPanel(lyricsData, audioPlayer, panel) {
+ const content = panel.querySelector('.lyrics-content');
+
+ const syncedLyrics = lyricsData.subtitles
+ ? parseSyncedLyricsSimple(lyricsData.subtitles)
+ : null;
+
+ if (syncedLyrics && syncedLyrics.length > 0) {
+ // Render synced lyrics
+ content.innerHTML = '';
+ syncedLyrics.forEach((line, index) => {
+ const lineEl = document.createElement('p');
+ lineEl.className = 'lyrics-line synced-line';
+ lineEl.textContent = line.text || '♪';
+ lineEl.dataset.index = index;
+ lineEl.dataset.time = line.time;
+ content.appendChild(lineEl);
+ });
+
+ let currentLineIndex = -1;
+
+ const updateLyrics = () => {
+ const currentTime = audioPlayer.currentTime;
+ const newIndex = getCurrentLineIndex(syncedLyrics, currentTime);
+
+ if (newIndex !== currentLineIndex) {
+ currentLineIndex = newIndex;
+
+ content.querySelectorAll('.synced-line').forEach((line, index) => {
+ line.classList.remove('active', 'upcoming', 'past');
+
+ if (index === currentLineIndex) {
+ line.classList.add('active');
+ // Smooth scroll to active line
+ line.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ } else if (index === currentLineIndex + 1) {
+ line.classList.add('upcoming');
+ } else if (index < currentLineIndex) {
+ line.classList.add('past');
+ }
+ });
+ }
+ };
+
+ // Store the update function so we can remove it later
+ panel.lyricsUpdateHandler = updateLyrics;
+ audioPlayer.addEventListener('timeupdate', updateLyrics);
+
+ // Initial update
+ updateLyrics();
+ } else if (lyricsData.lyrics) {
+ // Fallback to static lyrics
+ const lines = lyricsData.lyrics.split('\n');
+ content.innerHTML = lines.map(line =>
+ `${line || ' '}
`
+ ).join('');
+ } else {
+ content.innerHTML = 'No lyrics available
';
+ }
+}
+
+export function clearLyricsPanelSync(audioPlayer, panel) {
+ if (panel.lyricsUpdateHandler) {
+ audioPlayer.removeEventListener('timeupdate', panel.lyricsUpdateHandler);
+ panel.lyricsUpdateHandler = null;
+ }
+}
+
export function showKaraokeView(track, lyricsData, audioPlayer) {
const view = document.createElement('div');
view.id = 'karaoke-view';
@@ -162,25 +230,44 @@ export function showKaraokeView(track, lyricsData, audioPlayer) {
lyricsContainer.appendChild(lineEl);
});
- let updateInterval = setInterval(() => {
+ let currentLineIndex = -1;
+
+ const updateLyrics = () => {
const currentTime = audioPlayer.currentTime;
- const currentIndex = getCurrentLineIndex(syncedLyrics, currentTime);
+ const newIndex = getCurrentLineIndex(syncedLyrics, currentTime);
- document.querySelectorAll('.karaoke-line').forEach((line, index) => {
- line.classList.toggle('active', index === currentIndex);
- line.classList.toggle('past', index < currentIndex);
- });
-
- if (currentIndex >= 0) {
- const activeLine = lyricsContainer.children[currentIndex];
- if (activeLine) {
- activeLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ if (newIndex !== currentLineIndex) {
+ currentLineIndex = newIndex;
+
+ document.querySelectorAll('.karaoke-line').forEach((line, index) => {
+ line.classList.remove('active', 'upcoming', 'past');
+
+ if (index === currentLineIndex) {
+ line.classList.add('active');
+ } else if (index === currentLineIndex + 1) {
+ line.classList.add('upcoming');
+ } else if (index < currentLineIndex) {
+ line.classList.add('past');
+ }
+ });
+
+ if (currentLineIndex >= 0) {
+ const activeLine = lyricsContainer.children[currentLineIndex];
+ if (activeLine) {
+ activeLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ }
}
}
- }, 100);
+ };
+
+ // Use timeupdate event for better sync
+ audioPlayer.addEventListener('timeupdate', updateLyrics);
+
+ // Initial update
+ updateLyrics();
view.querySelector('#close-karaoke-btn').addEventListener('click', () => {
- clearInterval(updateInterval);
+ audioPlayer.removeEventListener('timeupdate', updateLyrics);
view.remove();
});
diff --git a/styles.css b/styles.css
index e3aacec..df9f4a9 100644
--- a/styles.css
+++ b/styles.css
@@ -2377,12 +2377,48 @@ input:checked + .slider::before {
flex: 1;
overflow-y: auto;
padding: 1rem;
+ scroll-behavior: smooth;
}
.lyrics-line {
margin: 0.75rem 0;
line-height: 1.6;
color: var(--foreground);
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* Synced lyrics styling with Apple Music animations */
+.synced-line {
+ padding: 0.5rem 0;
+ font-size: 1.125rem;
+ line-height: 1.8;
+ color: var(--muted-foreground);
+ opacity: 0.5;
+ transform: scale(0.95);
+ filter: blur(0.5px);
+ transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.synced-line.active {
+ color: var(--highlight);
+ font-weight: 600;
+ font-size: 1.25rem;
+ opacity: 1;
+ transform: scale(1);
+ filter: blur(0);
+ text-shadow: 0 0 20px rgba(var(--highlight-rgb), 0.3);
+}
+
+.synced-line.upcoming {
+ opacity: 0.7;
+ transform: scale(0.98);
+ filter: blur(0.3px);
+}
+
+.synced-line.past {
+ opacity: 0.3;
+ transform: scale(0.93);
+ filter: blur(1px);
}
.lyrics-loading,
@@ -2433,28 +2469,44 @@ input:checked + .slider::before {
flex-direction: column;
align-items: center;
gap: 1rem;
+ scroll-behavior: smooth;
}
.karaoke-line {
font-size: 1.5rem;
line-height: 1.8;
color: var(--muted-foreground);
- transition: all 0.3s ease;
+ transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
text-align: center;
max-width: 800px;
- opacity: 0.4;
+ opacity: 0.35;
+ transform: scale(0.92);
+ filter: blur(1px);
+ padding: 0.5rem 1rem;
}
.karaoke-line.active {
color: var(--highlight);
- font-size: 2rem;
- font-weight: 600;
+ font-size: 2.25rem;
+ font-weight: 700;
opacity: 1;
- transform: scale(1.1);
+ transform: scale(1.05);
+ filter: blur(0);
+ text-shadow: 0 0 30px rgba(var(--highlight-rgb), 0.4),
+ 0 0 60px rgba(var(--highlight-rgb), 0.2);
+ letter-spacing: 0.02em;
+}
+
+.karaoke-line.upcoming {
+ opacity: 0.55;
+ transform: scale(0.96);
+ filter: blur(0.5px);
}
.karaoke-line.past {
- opacity: 0.6;
+ opacity: 0.25;
+ transform: scale(0.88);
+ filter: blur(1.5px);
}
/* Mobile adjustments */
@@ -2463,6 +2515,14 @@ input:checked + .slider::before {
width: 100vw;
}
+ .synced-line {
+ font-size: 1rem;
+ }
+
+ .synced-line.active {
+ font-size: 1.125rem;
+ }
+
.karaoke-title {
font-size: 1.5rem;
}
@@ -2476,7 +2536,7 @@ input:checked + .slider::before {
}
.karaoke-line.active {
- font-size: 1.5rem;
+ font-size: 1.75rem;
}
}