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>
|
</div>
|
||||||
<div class="volume-controls">
|
<div class="volume-controls">
|
||||||
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Library" style="display: none;">
|
<div class="player-actions-row">
|
||||||
</button>
|
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Library" style="display: none;">
|
||||||
<button id="download-current-btn" title="Download current track" class="desktop-only">
|
</button>
|
||||||
<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">
|
<button id="toggle-lyrics-btn" title="Lyrics">
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
<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>
|
||||||
<polyline points="7 10 12 15 17 10"></polyline>
|
</button>
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
<button id="download-current-btn" title="Download current track" class="desktop-only">
|
||||||
</svg>
|
<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">
|
||||||
</button>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||||
<button id="cast-btn" title="Cast" class="desktop-only">
|
<polyline points="7 10 12 15 17 10"></polyline>
|
||||||
<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">
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||||
<path d="M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path>
|
</svg>
|
||||||
<path d="M2 12a9 9 0 0 1 9 9"></path>
|
</button>
|
||||||
<path d="M2 17a5 5 0 0 1 5 5"></path>
|
<button id="cast-btn" title="Cast" class="desktop-only">
|
||||||
<path d="M2 22h.01"></path>
|
<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">
|
||||||
</svg>
|
<path d="M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"></path>
|
||||||
</button>
|
<path d="M2 12a9 9 0 0 1 9 9"></path>
|
||||||
<button id="queue-btn" title="Queue">
|
<path d="M2 17a5 5 0 0 1 5 5"></path>
|
||||||
<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>
|
<path d="M2 22h.01"></path>
|
||||||
</button>
|
</svg>
|
||||||
<button id="volume-btn" title="Mute" class="desktop-only"></button>
|
</button>
|
||||||
<div id="volume-bar" class="volume-bar desktop-only">
|
<button id="queue-btn" title="Queue">
|
||||||
<div id="volume-fill" class="volume-fill"></div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
27
js/app.js
27
js/app.js
|
|
@ -228,7 +228,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
lyricsPanel.classList.toggle('hidden');
|
lyricsPanel.classList.toggle('hidden');
|
||||||
|
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel);
|
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager);
|
||||||
} else {
|
} else {
|
||||||
clearLyricsPanelSync(audioPlayer, lyricsPanel);
|
clearLyricsPanelSync(audioPlayer, lyricsPanel);
|
||||||
}
|
}
|
||||||
|
|
@ -257,6 +257,23 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
ui.closeFullscreenCover();
|
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) => {
|
document.getElementById('close-lyrics-btn')?.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
lyricsPanel.classList.add('hidden');
|
lyricsPanel.classList.add('hidden');
|
||||||
|
|
@ -308,12 +325,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
// Update lyrics panel if it's open
|
// Update lyrics panel if it's open
|
||||||
if (!lyricsPanel.classList.contains('hidden')) {
|
if (!lyricsPanel.classList.contains('hidden')) {
|
||||||
const mode = nowPlayingSettings.getMode();
|
clearLyricsPanelSync(audioPlayer, lyricsPanel);
|
||||||
if (mode === 'lyrics') {
|
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager);
|
||||||
clearLyricsPanelSync(audioPlayer, lyricsPanel);
|
|
||||||
await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Fullscreen/Enlarged Cover if it's open
|
// 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;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showSyncedLyricsPanel(track, audioPlayer, panel) {
|
export async function showSyncedLyricsPanel(track, audioPlayer, panel, lyricsManager = null) {
|
||||||
const content = panel.querySelector('.lyrics-content');
|
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>';
|
content.innerHTML = '<div class="lyrics-loading">Loading lyrics...</div>';
|
||||||
|
|
||||||
const lyricsManager = new LyricsManager();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await lyricsManager.ensureComponentLoaded();
|
await manager.ensureComponentLoaded();
|
||||||
|
|
||||||
const title = track.title;
|
const title = track.title;
|
||||||
const artist = getTrackArtists(track);
|
const artist = getTrackArtists(track);
|
||||||
|
|
@ -203,6 +213,7 @@ export async function showSyncedLyricsPanel(track, audioPlayer, panel) {
|
||||||
if (durationMs) amLyrics.setAttribute('song-duration', durationMs);
|
if (durationMs) amLyrics.setAttribute('song-duration', durationMs);
|
||||||
amLyrics.setAttribute('query', `${title} ${artist}`.trim());
|
amLyrics.setAttribute('query', `${title} ${artist}`.trim());
|
||||||
if (isrc) amLyrics.setAttribute('isrc', isrc);
|
if (isrc) amLyrics.setAttribute('isrc', isrc);
|
||||||
|
|
||||||
amLyrics.setAttribute('highlight-color', '#93c5fd');
|
amLyrics.setAttribute('highlight-color', '#93c5fd');
|
||||||
amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)');
|
amLyrics.setAttribute('hover-background-color', 'rgba(59, 130, 246, 0.14)');
|
||||||
amLyrics.setAttribute('autoscroll', '');
|
amLyrics.setAttribute('autoscroll', '');
|
||||||
|
|
@ -211,57 +222,9 @@ export async function showSyncedLyricsPanel(track, audioPlayer, panel) {
|
||||||
amLyrics.style.width = '100%';
|
amLyrics.style.width = '100%';
|
||||||
|
|
||||||
content.appendChild(amLyrics);
|
content.appendChild(amLyrics);
|
||||||
lyricsManager.amLyricsElement = amLyrics;
|
manager.amLyricsElement = amLyrics;
|
||||||
|
|
||||||
let baseTimeMs = 0;
|
setupLyricsSync(track, audioPlayer, panel, manager, amLyrics);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load lyrics:', 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) {
|
export function clearLyricsPanelSync(audioPlayer, panel) {
|
||||||
if (panel.lyricsUpdateHandler) {
|
if (panel.lyricsUpdateHandler) {
|
||||||
audioPlayer.removeEventListener('timeupdate', 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);
|
color: var(--muted-foreground);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition);
|
transition: all var(--transition);
|
||||||
padding: 0.5rem;
|
padding: 0.25rem;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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/lastfm.js',
|
||||||
'/js/lyrics.js',
|
'/js/lyrics.js',
|
||||||
'/js/downloads.js',
|
'/js/downloads.js',
|
||||||
|
'/js/db.js',
|
||||||
|
'/js/metadata.js',
|
||||||
'/manifest.json',
|
'/manifest.json',
|
||||||
'/assets/logo.svg',
|
'/assets/logo.svg',
|
||||||
'/assets/appicon.png'
|
'/assets/appicon.png',
|
||||||
|
'/assets/96.png'
|
||||||
];
|
];
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue