feat(web): scroll-row arrows + stable desktop media tile

Home rows get hover left/right arrows that only show for the scrollable
direction (enhanceScrollRow + initScrollRows, called from startApp);
visibility recomputed on scroll/resize/content-load.

Fix the OS media tile flicker when skipping on desktop PWA: mutate the
existing MediaMetadata in place instead of assigning a new one each track
(the replacement made Windows tear down and rebuild the tile). Action
handlers are now set once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-06-26 14:43:43 +02:00
1 parent 7498e45b16
commit d65fbcd11e
5 files changed
+98 -9

No files matched your search

+2
View File
@@ -3,6 +3,7 @@ import { toast } from './utils.js';
import { api, fetchRomajiIndex, loadRomajiOverrides } from './api.js';
import { injectPWA } from './pwa.js';
import { loadSavedTheme } from './themes.js';
import { initScrollRows } from './ui/components.js';
import {
playQueue, playCurrent, togglePlay, prevSong, nextSong, onEnded,
toggleShuffle, toggleRepeat, toggleQueue, setVolume, seekTo,
@@ -221,6 +222,7 @@ window.addEventListener('load', () => {
injectPWA();
initProgressScrub();
initKeyboardShortcuts();
initScrollRows();
// Click player-art to open Now Playing
document.getElementById('player-art')?.addEventListener('click', openNowPlaying);
+24 -9
View File
@@ -1019,23 +1019,38 @@ export function setVolume(v) {
// ─── MEDIA SESSION ────────────────────────────────────────────────────────
let _msHandlersSet = false;
export function updateMediaSession(s) {
// When native is active, Media3 owns the system notification — don't touch it.
if (HAS_BRIDGE) return;
if(!navigator.mediaSession) return;
const ms = navigator.mediaSession;
if(!ms) return;
const artwork = s.coverArt ? [
{ src: _cachedCoverUrl(s.coverArt, 256), sizes:'256x256', type:'image/jpeg' },
{ src: _cachedCoverUrl(s.coverArt, 512), sizes:'512x512', type:'image/jpeg' },
{ src: _cachedCoverUrl(s.coverArt, 1024), sizes:'1024x1024', type:'image/jpeg' },
] : [];
navigator.mediaSession.metadata = new MediaMetadata({ title:s.title, artist:s.artist||'', album:s.album||'', artwork });
navigator.mediaSession.setActionHandler('play', togglePlay);
navigator.mediaSession.setActionHandler('pause', togglePlay);
navigator.mediaSession.setActionHandler('previoustrack', prevSong);
navigator.mediaSession.setActionHandler('nexttrack', nextSong);
navigator.mediaSession.setActionHandler('seekbackward', d => { S.audio.currentTime = Math.max(0, S.audio.currentTime-(d.seekOffset||10)); });
navigator.mediaSession.setActionHandler('seekforward', d => { S.audio.currentTime = Math.min(S.audio.duration||0, S.audio.currentTime+(d.seekOffset||10)); });
navigator.mediaSession.setActionHandler('seekto', d => { if(d.seekTime!=null) S.audio.currentTime = d.seekTime; });
// Mutate the existing metadata in place rather than assigning a new MediaMetadata —
// replacing the object makes Windows tear down & rebuild the media tile (visible
// flicker when skipping). Spotify-style: keep one object, just update its fields.
if (ms.metadata) {
ms.metadata.title = s.title;
ms.metadata.artist = s.artist || '';
ms.metadata.album = s.album || '';
ms.metadata.artwork = artwork;
} else {
ms.metadata = new MediaMetadata({ title:s.title, artist:s.artist||'', album:s.album||'', artwork });
}
// Action handlers are stable — set them once, not on every track change.
if (_msHandlersSet) return;
_msHandlersSet = true;
ms.setActionHandler('play', togglePlay);
ms.setActionHandler('pause', togglePlay);
ms.setActionHandler('previoustrack', prevSong);
ms.setActionHandler('nexttrack', nextSong);
ms.setActionHandler('seekbackward', d => { S.audio.currentTime = Math.max(0, S.audio.currentTime-(d.seekOffset||10)); });
ms.setActionHandler('seekforward', d => { S.audio.currentTime = Math.min(S.audio.duration||0, S.audio.currentTime+(d.seekOffset||10)); });
ms.setActionHandler('seekto', d => { if(d.seekTime!=null) S.audio.currentTime = d.seekTime; });
}
export function syncMediaSessionPosition() {
+52
View File
@@ -103,3 +103,55 @@ export function mkSection(title) {
d.innerHTML = `<div class="search-section-title">${title}</div>`;
return d;
}
// ─── HORIZONTAL SCROLL-ROW ARROWS (desktop) ───
const _CHEV_L = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>';
const _CHEV_R = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>';
/** Wrap a `.scroll-row` with left/right arrows that appear only for the scrollable direction. */
export function enhanceScrollRow(row) {
if (!row || row.dataset.saEnhanced) return;
row.dataset.saEnhanced = '1';
const wrap = document.createElement('div');
wrap.className = 'scroll-row-wrap';
row.parentNode.insertBefore(wrap, row);
wrap.appendChild(row);
const mkBtn = (cls, html, label) => {
const b = document.createElement('button');
b.type = 'button';
b.className = 'scroll-arrow ' + cls;
b.setAttribute('aria-label', label);
b.innerHTML = html;
wrap.appendChild(b);
return b;
};
const left = mkBtn('sa-left', _CHEV_L, 'Zurück');
const right = mkBtn('sa-right', _CHEV_R, 'Weiter');
const EPS = 4;
const update = () => {
left.classList.toggle('can-scroll', row.scrollLeft > EPS);
right.classList.toggle('can-scroll', row.scrollLeft < row.scrollWidth - row.clientWidth - EPS);
// Vertically center the arrows on the cover art (fallback: CSS default 72px)
const art = row.querySelector('.card-art') || row.querySelector('.card');
if (art) {
const top = art.getBoundingClientRect().top - wrap.getBoundingClientRect().top + art.offsetHeight / 2;
if (top > 0) { left.style.top = right.style.top = top + 'px'; }
}
};
const step = () => Math.max(160, row.clientWidth * 0.8);
left.addEventListener('click', () => row.scrollBy({ left: -step(), behavior: 'smooth' }));
right.addEventListener('click', () => row.scrollBy({ left: step(), behavior: 'smooth' }));
row.addEventListener('scroll', update, { passive: true });
new ResizeObserver(update).observe(row);
new MutationObserver(update).observe(row, { childList: true });
update();
}
/** Enhance every `.scroll-row` currently in the DOM (the static Home rows). */
export function initScrollRows() {
document.querySelectorAll('.scroll-row').forEach(enhanceScrollRow);
}