feat: integrate lightbox for media images and improve media session handling

- Add lightbox support to song/playlist cover images by adding lb-trigger class and data-lightbox attributes
- Prevent default click behavior in lightbox.js to avoid navigation when opening image modal
- Update player UI to conditionally enable/disable lightbox on album art based on cover presence
- Set navigator.mediaSession.playbackState proactively during audio reassignment to prevent OS throttling
- Wrap media session seek handlers in try-catch blocks for compatibility with mobile streaming sources
This commit is contained in:
Brian Fertig 2026-04-15 21:24:54 -06:00
parent 5eadf57068
commit df14aa8975
5 changed files with 22 additions and 7 deletions

View File

@ -46,6 +46,8 @@
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const trigger = e.target.closest('[data-lightbox]'); const trigger = e.target.closest('[data-lightbox]');
if (trigger) { if (trigger) {
e.preventDefault();
e.stopPropagation();
open(trigger.src || trigger.dataset.src, trigger.alt); open(trigger.src || trigger.dataset.src, trigger.alt);
return; return;
} }

View File

@ -36,6 +36,8 @@
titleEl.textContent = 'Nothing playing'; titleEl.textContent = 'Nothing playing';
artistEl.textContent = ''; artistEl.textContent = '';
coverEl.src = '/static/vendor/cover-placeholder.svg'; coverEl.src = '/static/vendor/cover-placeholder.svg';
coverEl.removeAttribute('data-lightbox');
coverEl.classList.remove('lb-trigger');
queueInfoEl.textContent = ''; queueInfoEl.textContent = '';
updatePlayerSocial(null); updatePlayerSocial(null);
return; return;
@ -43,6 +45,13 @@
titleEl.textContent = cur.title; titleEl.textContent = cur.title;
artistEl.textContent = cur.artist || ''; artistEl.textContent = cur.artist || '';
coverEl.src = cur.cover || '/static/vendor/cover-placeholder.svg'; coverEl.src = cur.cover || '/static/vendor/cover-placeholder.svg';
if (cur.cover) {
coverEl.setAttribute('data-lightbox', '');
coverEl.classList.add('lb-trigger');
} else {
coverEl.removeAttribute('data-lightbox');
coverEl.classList.remove('lb-trigger');
}
queueInfoEl.textContent = state.queue.length > 1 queueInfoEl.textContent = state.queue.length > 1
? `${state.index + 1} / ${state.queue.length}` ? `${state.index + 1} / ${state.queue.length}`
: ''; : '';
@ -73,6 +82,9 @@
? [{ src: new URL(cur.cover, window.location.origin).href, sizes: '512x512', type: 'image/jpeg' }] ? [{ src: new URL(cur.cover, window.location.origin).href, sizes: '512x512', type: 'image/jpeg' }]
: [], : [],
}); });
// Proactively mark as playing so the OS doesn't throttle during the
// brief gap when audio.src is reassigned (which fires a pause event).
navigator.mediaSession.playbackState = 'playing';
} }
function playCurrent() { function playCurrent() {
@ -126,9 +138,10 @@
navigator.mediaSession.setActionHandler('pause', () => audio.pause()); navigator.mediaSession.setActionHandler('pause', () => audio.pause());
navigator.mediaSession.setActionHandler('nexttrack', () => next()); navigator.mediaSession.setActionHandler('nexttrack', () => next());
navigator.mediaSession.setActionHandler('previoustrack', () => prev()); navigator.mediaSession.setActionHandler('previoustrack', () => prev());
navigator.mediaSession.setActionHandler('seekto', (d) => { audio.currentTime = d.seekTime; }); // seek handlers may throw on some mobile browsers for streaming sources
navigator.mediaSession.setActionHandler('seekbackward', (d) => { audio.currentTime = Math.max(0, audio.currentTime - (d.seekOffset || 10)); }); try { navigator.mediaSession.setActionHandler('seekto', (d) => { audio.currentTime = d.seekTime; }); } catch (_) {}
navigator.mediaSession.setActionHandler('seekforward', (d) => { audio.currentTime = Math.min(audio.duration || Infinity, audio.currentTime + (d.seekOffset || 10)); }); try { navigator.mediaSession.setActionHandler('seekbackward', (d) => { audio.currentTime = Math.max(0, audio.currentTime - (d.seekOffset || 10)); }); } catch (_) {}
try { navigator.mediaSession.setActionHandler('seekforward', (d) => { audio.currentTime = Math.min(audio.duration || Infinity, audio.currentTime + (d.seekOffset || 10)); }); } catch (_) {}
} }
repeatBtn.addEventListener('click', () => { repeatBtn.addEventListener('click', () => {

View File

@ -11,7 +11,7 @@
<input type="checkbox" class="song-select" value="<%= s.id %>" aria-label="Select <%= s.title %>"> <input type="checkbox" class="song-select" value="<%= s.id %>" aria-label="Select <%= s.title %>">
<% } %> <% } %>
<% if (s.coverPath) { %> <% if (s.coverPath) { %>
<img src="/media/<%= s.coverPath %>" alt="" class="song-cover" /> <img src="/media/<%= s.coverPath %>" alt="" class="song-cover lb-trigger" data-lightbox />
<% } else { %> <% } else { %>
<div class="song-cover cover-fallback">♪</div> <div class="song-cover cover-fallback">♪</div>
<% } %> <% } %>

View File

@ -12,7 +12,7 @@
<% featured.forEach((p) => { %> <% featured.forEach((p) => { %>
<a class="card" href="/playlists/<%= p.slug %>" data-playlist-id="<%= p.id %>"> <a class="card" href="/playlists/<%= p.slug %>" data-playlist-id="<%= p.id %>">
<% if (p.coverPath) { %> <% if (p.coverPath) { %>
<img src="/media/<%= p.coverPath %>" alt="" /> <img src="/media/<%= p.coverPath %>" alt="" class="lb-trigger" data-lightbox />
<% } else { %> <% } else { %>
<div class="cover-fallback">🎧</div> <div class="cover-fallback">🎧</div>
<% } %> <% } %>

View File

@ -7,7 +7,7 @@
<% recentlyLiked.forEach((s) => { %> <% recentlyLiked.forEach((s) => { %>
<a href="/songs/<%= s.slug %>" class="liked-card"> <a href="/songs/<%= s.slug %>" class="liked-card">
<% if (s.coverPath) { %> <% if (s.coverPath) { %>
<img src="/media/<%= s.coverPath %>" alt="" class="liked-card-cover"> <img src="/media/<%= s.coverPath %>" alt="" class="liked-card-cover lb-trigger" data-lightbox>
<% } else { %> <% } else { %>
<div class="liked-card-cover liked-card-cover--fallback">♪</div> <div class="liked-card-cover liked-card-cover--fallback">♪</div>
<% } %> <% } %>
@ -53,7 +53,7 @@
<% mostPopular.forEach((s) => { %> <% mostPopular.forEach((s) => { %>
<a href="/songs/<%= s.slug %>" class="liked-card"> <a href="/songs/<%= s.slug %>" class="liked-card">
<% if (s.coverPath) { %> <% if (s.coverPath) { %>
<img src="/media/<%= s.coverPath %>" alt="" class="liked-card-cover"> <img src="/media/<%= s.coverPath %>" alt="" class="liked-card-cover lb-trigger" data-lightbox>
<% } else { %> <% } else { %>
<div class="liked-card-cover liked-card-cover--fallback">♪</div> <div class="liked-card-cover liked-card-cover--fallback">♪</div>
<% } %> <% } %>