UI: reorganize bottom controls and improve lyrics panel behavior
This commit is contained in:
parent
84adbaa811
commit
91da9b887d
5 changed files with 178 additions and 86 deletions
53
index.html
53
index.html
|
|
@ -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>
|
||||
|
|
|
|||
27
js/app.js
27
js/app.js
|
|
@ -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
|
||||
|
|
|
|||
147
js/lyrics.js
147
js/lyrics.js
|
|
@ -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);
|
||||
|
|
|
|||
32
styles.css
32
styles.css
|
|
@ -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
5
sw.js
|
|
@ -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 => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue