Merge pull request #19 from nh9961/main
Add synced lyrics support and enhance styling for karaoke mode
This commit is contained in:
commit
34c6c45439
3 changed files with 208 additions and 33 deletions
54
js/app.js
54
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 =>
|
||||
`<p class="lyrics-line">${line || ' '}</p>`
|
||||
).join('');
|
||||
} else {
|
||||
content.innerHTML = '<div class="lyrics-error">No lyrics available</div>';
|
||||
}
|
||||
showSyncedLyricsPanel(lyricsData, audioPlayer, lyricsPanel);
|
||||
} else {
|
||||
content.innerHTML = '<div class="lyrics-error">Failed to load lyrics</div>';
|
||||
}
|
||||
} 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 = '<div class="lyrics-loading">Loading lyrics...</div>';
|
||||
|
||||
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 = '<div class="lyrics-error">No lyrics available for this track</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', async (e) => {
|
||||
if (e.target.closest('#play-album-btn')) {
|
||||
const btn = e.target.closest('#play-album-btn');
|
||||
|
|
|
|||
113
js/lyrics.js
113
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 =>
|
||||
`<p class="lyrics-line">${line || ' '}</p>`
|
||||
).join('');
|
||||
} else {
|
||||
content.innerHTML = '<div class="lyrics-error">No lyrics available</div>';
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
|
|
|||
74
styles.css
74
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue