diff --git a/public/sw.js b/public/sw.js index 42c2ce3..feca39b 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v40'; +const CACHE = 'ns-v41'; const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js']; // Auth token params stripped from cache keys so artwork survives across sessions/token refreshes. diff --git a/src/components.js b/src/components.js index 5327080..482a14d 100644 --- a/src/components.js +++ b/src/components.js @@ -19,7 +19,7 @@ export function makeCard(title, sub, imgUrl, fallback='πŸ’Ώ') { } export function makeAlbumCard(a) { - const cv = coverUrl(a.coverArt, 200); + const cv = coverUrl(a.coverArt, 512); const c = makeCard(a.name, a.artist||'', cv, 'πŸ’Ώ'); c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(a); }; c.querySelector('.card-info').onclick = () => navigate('album-detail', a); @@ -31,7 +31,7 @@ export function makeSongItem(s, idx, list, { noArtistLink = false, noAlbumLink = const el = document.createElement('div'); if(s.starred && !s._starred) s._starred = true; el.className = 'song-item' + (S.queue[S.qIdx]?.id === s.id ? ' playing' : ''); - const cv = coverUrl(s.coverArt, 60); + const cv = coverUrl(s.coverArt, 128); const artistParts = (s.artist || '').split(/\s*(?:\/|β€’|,)\s*/).filter(Boolean); const artistHtml = artistParts.length ? artistParts.map((name, i) => { diff --git a/src/main.js b/src/main.js index 8bb63d2..efa2247 100644 --- a/src/main.js +++ b/src/main.js @@ -182,22 +182,25 @@ window.addEventListener('load', () => { // pageshow fires more reliably than visibilitychange on iOS PWA and Samsung window.addEventListener('pageshow', e => { if(e.persisted) _onForeground(); }); - // Watchdog: detect when audio silently stalled in background (ended event not fired, or frozen) - let _wdLast = 0, _wdStalledAt = 0; - setInterval(() => { - if(!S.audio.src || S.audio.paused) { _wdStalledAt = 0; _wdLast = 0; return; } - const dur = S.audio.duration; - const cur = S.audio.currentTime; - // ended but event didn't fire (background throttling) - if(dur > 0 && cur >= dur - 0.5) { onEnded(); _wdStalledAt = 0; _wdLast = 0; return; } - if(cur !== _wdLast) { _wdStalledAt = 0; _wdLast = cur; return; } - if(!_wdStalledAt) { _wdStalledAt = Date.now(); return; } - // frozen for >8s β†’ try to resume - if(Date.now() - _wdStalledAt > 8000) { - _wdStalledAt = 0; - S.audio.play().catch(() => {}); - } - }, 3000); + // Watchdog: detect when audio silently stalled in background (ended event not fired, or frozen). + // Disabled with Bridge β€” the foreground service keeps ExoPlayer alive independently. + if (typeof window.NSBridge === 'undefined') { + let _wdLast = 0, _wdStalledAt = 0; + setInterval(() => { + if(!S.audio.src || S.audio.paused) { _wdStalledAt = 0; _wdLast = 0; return; } + const dur = S.audio.duration; + const cur = S.audio.currentTime; + // ended but event didn't fire (background throttling) + if(dur > 0 && cur >= dur - 0.5) { onEnded(); _wdStalledAt = 0; _wdLast = 0; return; } + if(cur !== _wdLast) { _wdStalledAt = 0; _wdLast = cur; return; } + if(!_wdStalledAt) { _wdStalledAt = Date.now(); return; } + // frozen for >8s β†’ try to resume + if(Date.now() - _wdStalledAt > 8000) { + _wdStalledAt = 0; + S.audio.play().catch(() => {}); + } + }, 3000); + } injectPWA(); initProgressScrub(); diff --git a/src/nowplaying.js b/src/nowplaying.js index 67e116a..831a19c 100644 --- a/src/nowplaying.js +++ b/src/nowplaying.js @@ -230,7 +230,7 @@ export function renderNpQueue() { S.queue.forEach((s, i) => { const item = document.createElement('div'); item.className = 'np-queue-item' + (i===S.qIdx ? ' nq-active' : ''); - const cv = coverUrl(s.coverArt, 60); + const cv = coverUrl(s.coverArt, 128); item.innerHTML = `
${i+1}
${cv ? `` : 'β™ͺ'}
${esc(s.title)}
${esc(s.artist||'')}
`; item.onclick = () => { S.qIdx=i; playCurrent(); switchNpTab('player'); }; el.appendChild(item); @@ -254,7 +254,7 @@ export function npGoAlbum() { // ─── NP ACTION SHEET ─── export function openNpMenu() { const s = S.queue[S.qIdx]; if(!s) return; - const cv = coverUrl(s.coverArt, 80); + const cv = coverUrl(s.coverArt, 256); document.getElementById('np-sheet-song').innerHTML = `
${cv ? `` : '
🎡
'}
${esc(s.title)}
${esc(s.artist||'')}${s.album?' Β· '+esc(s.album):''}
`; diff --git a/src/player.js b/src/player.js index 5680167..090d429 100644 --- a/src/player.js +++ b/src/player.js @@ -6,27 +6,47 @@ import { loadRecommendations } from './views.js'; import { updateLyricsHighlight, onNpSongChange } from './nowplaying.js'; // ─── NATIVE BRIDGE DETECTION ─────────────────────────────────────────────── -// Returns true when running inside the NovaSonic Android wrapper (window.NSBridge present). -// Checked at call-time so it works even if the bridge appears after module load. -const _native = () => !!(window.NSBridge); +// True when running inside the NovaSonic Android wrapper (NSBridge injected before loadUrl). +const HAS_BRIDGE = typeof window.NSBridge !== 'undefined'; // Track native playback state; driven by window.NSPlayer callbacks below. let _nativePos = 0; // ms -let _nativeDur = 0; // ms let _nativePlaying = false; +let _lastDurationMs = 0; // ms β€” set by onPositionUpdate, used for seek calculations // Unified duration / position / seek helpers β€” use native state when available, // fall back to HTMLAudioElement when not. -const _dur = () => _native() ? _nativeDur / 1000 : (S.audio.duration || 0); -const _pos = () => _native() ? _nativePos / 1000 : (S.audio.currentTime || 0); -const _seek = ms => _native() ? _nsBridge('seekTo', ms) : (S.audio.currentTime = ms / 1000); +const _dur = () => HAS_BRIDGE ? _lastDurationMs / 1000 : (S.audio.duration || 0); +const _pos = () => HAS_BRIDGE ? _nativePos / 1000 : (S.audio.currentTime || 0); +const _seek = ms => HAS_BRIDGE ? _nsBridge('seekTo', ms) : (S.audio.currentTime = ms / 1000); // Safe NSBridge caller β€” never throws into JS. function _nsBridge(method, ...args) { try { window.NSBridge[method](...args); } catch(_) {} } -// Convert a Navidrome song object to the NSBridge Track JSON schema. +// Build the NSBridge Track JSON array from S.queue. +function buildTrackList() { + return S.queue.map(s => ({ + id: s.id, + url: streamUrl(s.id), + title: s.title || '', + artist: s.artist || '', + album: s.album || null, + artworkUrl: s.coverArt ? coverUrl(s.coverArt, 1024) : null, + durationMs: Math.floor((s.duration || 0) * 1000), + })); +} + +// Push the current JS queue to native whenever S.queue changes (radio top-ups, etc.). +function _pushQueueIfBridge() { + if (!HAS_BRIDGE) return; + try { + window.NSBridge.setQueue(JSON.stringify(buildTrackList()), S.qIdx, !S.audio.paused); + } catch (e) { console.warn('NSBridge.setQueue failed', e); } +} + +// Convert a single Navidrome song to the NSBridge Track schema (used for addToQueue). function _trackToNative(s) { return { id: s.id, @@ -34,8 +54,8 @@ function _trackToNative(s) { title: s.title || '', artist: s.artist || '', album: s.album || null, - artworkUrl: s.coverArt ? _cachedCoverUrl(s.coverArt, 512) : null, - durationMs: Math.round((s.duration || 0) * 1000), + artworkUrl: s.coverArt ? coverUrl(s.coverArt, 1024) : null, + durationMs: Math.floor((s.duration || 0) * 1000), }; } @@ -46,66 +66,74 @@ window.NSPlayer = { // Called when MediaController connects (first load or WebView reload). onReady() { - const raw = window.NSBridge?.getState(); - const state = raw ? (JSON.parse(raw) || {}) : {}; - const nativeLen = state.queueLength || 0; - - if (nativeLen > 0 && nativeLen === S.queue.length) { - // Native still has the same queue (e.g. after a WebView reload) β€” re-sync UI. - S.qIdx = state.currentIndex ?? S.qIdx; - _nativePlaying = state.isPlaying || false; - _nativePos = state.positionMs || 0; - const s = S.queue[S.qIdx]; - if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); } - setPlayIcon(_nativePlaying); - renderQueue(); - } else if (S.queue.length > 0) { - // Native queue is empty or stale β€” push JS-side state (restored from localStorage). - const tracks = S.queue.map(_trackToNative); - _nsBridge('setQueue', JSON.stringify(tracks), S.qIdx, false); + if (HAS_BRIDGE && S.queue.length > 0) { + try { + const raw = window.NSBridge.getState(); + const state = raw ? (JSON.parse(raw) || {}) : {}; + const nativeLen = state.queueLength || 0; + if (nativeLen > 0 && nativeLen === S.queue.length) { + // Native still has the same queue β€” just re-sync UI state. + S.qIdx = state.currentIndex ?? S.qIdx; + _nativePlaying = state.isPlaying || false; + _nativePos = state.positionMs || 0; + const s = S.queue[S.qIdx]; + if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); } + setPlayIcon(_nativePlaying); + renderQueue(); + } else { + // Native queue is empty or stale β€” push JS state (restored from localStorage). + window.NSBridge.setQueue(JSON.stringify(buildTrackList()), S.qIdx, false); + } + } catch (e) {} } - // Always sync settings. + // Always sync shuffle/repeat settings. _nsBridge('setRepeatMode', S.repeat === false ? 'off' : S.repeat); _nsBridge('setShuffleMode', S.shuffle); }, // Play/pause/buffer state changed. onPlaybackStateChanged(stateJson) { - const state = JSON.parse(stateJson); - const prev = _nativePlaying; - _nativePlaying = state.isPlaying; - if (state.isPlaying !== prev) { - setPlayIcon(state.isPlaying); - document.getElementById('player-art')?.classList.toggle('spinning', state.isPlaying); - if (navigator.mediaSession) - navigator.mediaSession.playbackState = state.isPlaying ? 'playing' : 'paused'; - } + try { + const state = JSON.parse(stateJson); + const prev = _nativePlaying; + _nativePlaying = state.isPlaying; + if (state.isPlaying !== prev) { + setPlayIcon(state.isPlaying); + document.getElementById('player-art')?.classList.toggle('spinning', state.isPlaying); + if (navigator.mediaSession) + navigator.mediaSession.playbackState = state.isPlaying ? 'playing' : 'paused'; + } + } catch (e) {} }, // Native moved to a different track (auto-advance, seek, or queue replace). onTrackChanged(payloadJson) { - const payload = JSON.parse(payloadJson); - S.qIdx = payload.index; - const s = S.queue[S.qIdx]; - if (!s) return; - updatePlayerUI(s, true); - onNpSongChange(s.id); - renderQueue(); - // Radio mode: top up the queue when running low; push new songs to native. - if (S.radioMode && S.queue.length - S.qIdx < 5) { - const prevLen = S.queue.length; - fillRadioQueue().then(() => { - S.queue.slice(prevLen).forEach(t => - _nsBridge('addToQueue', JSON.stringify(_trackToNative(t)), -1) - ); - }); - } + try { + const payload = JSON.parse(payloadJson); + if (typeof payload.index === 'number' && payload.index !== S.qIdx) { + S.qIdx = payload.index; + const s = S.queue[S.qIdx]; + if (!s) return; + updatePlayerUI(s, true); + onNpSongChange(s.id); + renderQueue(); + // Radio mode: top up the queue when running low; push new songs to native. + if (S.radioMode && S.queue.length - S.qIdx < 5) { + const prevLen = S.queue.length; + fillRadioQueue().then(() => { + S.queue.slice(prevLen).forEach(t => + _nsBridge('addToQueue', JSON.stringify(_trackToNative(t)), -1) + ); + }); + } + } + } catch (e) {} }, // ~250 ms position tick while playing. onPositionUpdate(posMs, durMs) { - _nativePos = posMs; - _nativeDur = durMs; + _nativePos = posMs; + _lastDurationMs = durMs; if (S._scrubbing) return; const dur = durMs / 1000; const cur = posMs / 1000; @@ -156,7 +184,7 @@ function _cachedCoverUrl(id, size) { // ─── AUDIO SESSION β€” set once, never again ─────────────────────────────── let _audioSessionSet = false; function _setAudioSession() { - if (_native() || _audioSessionSet) return; + if (HAS_BRIDGE || _audioSessionSet) return; _audioSessionSet = true; try { if(navigator.audioSession) navigator.audioSession.type = 'playback'; } catch {} } @@ -175,7 +203,7 @@ function _nextIdx() { } function _preloadNext() { - if (_native()) return; // ExoPlayer handles buffering natively + if (HAS_BRIDGE) return; // ExoPlayer handles buffering natively const ni = _nextIdx(); if(ni < 0) return; const next = S.queue[ni]; @@ -189,7 +217,7 @@ function _preloadNext() { _preLink.href = _preUrl; document.head.appendChild(_preLink); if(next.coverArt) { - [96, 300, 512].forEach(size => { + [256, 512, 1024].forEach(size => { const img = new Image(); img.src = _cachedCoverUrl(next.coverArt, size); }); @@ -209,9 +237,8 @@ export function playQueue(songs, startIdx) { S.qIdx = startIdx; _clearPreload(); - if (_native()) { - const tracks = S.queue.map(_trackToNative); - _nsBridge('setQueue', JSON.stringify(tracks), startIdx, true); + if (HAS_BRIDGE) { + window.NSBridge.setQueue(JSON.stringify(buildTrackList()), startIdx, true); renderQueue(); if (S.radioMode) fillRadioQueue(); return; @@ -226,7 +253,7 @@ export function playCurrent() { if(S.qIdx < 0 || S.qIdx >= S.queue.length) return; const s = S.queue[S.qIdx]; - if (_native()) { + if (HAS_BRIDGE) { // Update DOM immediately for snappy feedback; onTrackChanged will confirm. S._trackChanging = true; updatePlayerUI(s, true); @@ -306,7 +333,7 @@ export function updatePlayerUI(s, _skipMediaSession = false) { document.getElementById('player-album-sep').style.display = s.album ? '' : 'none'; document.getElementById('player-album-name').textContent = s.album || ''; const art = document.getElementById('player-art'); - const cv = coverUrl(s.coverArt, 100); + const cv = coverUrl(s.coverArt, 256); const existingImg = art.querySelector('img'); if (cv && existingImg) existingImg.src = cv; else art.innerHTML = cv ? `` : '
β™ͺ
'; @@ -353,7 +380,7 @@ function _crossfadeNpBg(cv) { export function _syncNpDirect(s) { if(!S.npOpen || !s) return; - const cv = coverUrl(s.coverArt, 400); + const cv = coverUrl(s.coverArt, 1024); const cover = document.getElementById('np-cover'); if (cover) { const existingImg = cover.querySelector('img'); @@ -380,7 +407,7 @@ export function _syncNpDirect(s) { // ─── TRANSPORT ──────────────────────────────────────────────────────────── export function togglePlay() { - if (_native()) { _nsBridge('togglePlay'); return; } + if (HAS_BRIDGE) { _nsBridge('togglePlay'); return; } if(S.audio.src) S.audio.paused ? S.audio.play() : S.audio.pause(); } @@ -399,14 +426,14 @@ export function setPlayIcon(playing) { export function prevSong() { _clearPreload(); - if (_native()) { _nsBridge('prev'); return; } + if (HAS_BRIDGE) { _nsBridge('prev'); return; } if(S.audio.currentTime > 3) { S.audio.currentTime = 0; return; } if(S.qIdx > 0) { S.qIdx--; playCurrent(); } } export function nextSong() { _clearPreload(); - if (_native()) { _nsBridge('next'); return; } + if (HAS_BRIDGE) { _nsBridge('next'); return; } if(S.shuffle) S.qIdx = Math.floor(Math.random() * S.queue.length); else if(S.qIdx < S.queue.length - 1) S.qIdx++; else if(S.repeat) S.qIdx = 0; @@ -437,14 +464,14 @@ export function toggleShuffle() { S.shuffle = !S.shuffle; localStorage.setItem('ns_shuffle', S.shuffle ? '1' : '0'); _applyShuffleRepeat(); - if (_native()) _nsBridge('setShuffleMode', S.shuffle); + if (HAS_BRIDGE) _nsBridge('setShuffleMode', S.shuffle); } export function toggleRepeat() { S.repeat = S.repeat === false ? 'all' : S.repeat === 'all' ? 'one' : false; localStorage.setItem('ns_repeat', S.repeat === false ? '0' : S.repeat); _applyShuffleRepeat(); - if (_native()) _nsBridge('setRepeatMode', S.repeat === false ? 'off' : S.repeat); + if (HAS_BRIDGE) _nsBridge('setRepeatMode', S.repeat === false ? 'off' : S.repeat); } // ─── RADIO ──────────────────────────────────────────────────────────────── @@ -469,13 +496,14 @@ export async function fillRadioQueue() { const r = await api('getRandomSongs', '&size=20'); S.queue.push(...(r.randomSongs?.song || [])); renderQueue(); + _pushQueueIfBridge(); } catch {} } // ─── PROGRESS & SCRUBBING ───────────────────────────────────────────────── export function updateProgress() { - if(_native()) return; // driven by NSPlayer.onPositionUpdate instead + if(HAS_BRIDGE) return; // driven by NSPlayer.onPositionUpdate instead if(!S.audio.duration || S._scrubbing) return; syncMediaSessionPosition(); const rem = S.audio.duration - S.audio.currentTime; @@ -504,8 +532,8 @@ export function seekTo(e) { const fill = document.getElementById('progress-fill'); if (fill) { fill.style.transition = 'none'; fill.style.width = (pos * 100) + '%'; } S._scrubbing = true; - if (_native()) { - _nsBridge('seekTo', Math.round(pos * _nativeDur)); + if (HAS_BRIDGE) { + _nsBridge('seekTo', Math.round(pos * _lastDurationMs)); const done = () => { S._scrubbing = false; requestAnimationFrame(() => { if (fill) fill.style.transition = ''; }); }; setTimeout(done, 500); } else { @@ -537,8 +565,8 @@ function _initBarScrub(bar, fill, onMove) { function endScrub(pos) { fill.style.width = (pos * 100) + '%'; const dur = _dur(); - if (_native()) { - _nsBridge('seekTo', Math.round(pos * _nativeDur)); + if (HAS_BRIDGE) { + _nsBridge('seekTo', Math.round(pos * _lastDurationMs)); S._scrubbing = false; requestAnimationFrame(() => { fill.style.transition = ''; }); } else if (dur) { @@ -570,19 +598,19 @@ export function setVolume(v) { const absAudio = document.getElementById('abs-audio'); if (absAudio) absAudio.volume = vol; document.querySelectorAll('.volume-slider-sync').forEach(el => { el.value = v; }); - if (_native()) _nsBridge('setVolume', vol); + if (HAS_BRIDGE) _nsBridge('setVolume', vol); } // ─── MEDIA SESSION ──────────────────────────────────────────────────────── export function updateMediaSession(s) { // When native is active, Media3 owns the system notification β€” don't touch it. - if (_native()) return; + if (HAS_BRIDGE) return; if(!navigator.mediaSession) return; const artwork = s.coverArt ? [ - { src: _cachedCoverUrl(s.coverArt, 96), sizes:'96x96', type:'image/jpeg' }, - { src: _cachedCoverUrl(s.coverArt, 300), sizes:'300x300', type:'image/jpeg' }, - { src: _cachedCoverUrl(s.coverArt, 512), sizes:'512x512', type:'image/jpeg' }, + { 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); @@ -595,7 +623,7 @@ export function updateMediaSession(s) { } export function syncMediaSessionPosition() { - if (_native()) return; + if (HAS_BRIDGE) return; if(!navigator.mediaSession?.setPositionState || !S.audio.duration || isNaN(S.audio.duration)) return; const now = Date.now(); if(now - S._msPosLast < 1000) return; @@ -670,7 +698,7 @@ export function renderQueue() { S.queue.forEach((s, i) => { const item = document.createElement('div'); item.className = 'queue-item' + (i === S.qIdx ? ' active' : ''); - const cv = coverUrl(s.coverArt, 60); + const cv = coverUrl(s.coverArt, 128); item.innerHTML = `
${i+1}
${cv ? `` : 'β™ͺ'}
${esc(s.title)}
${esc(s.artist||'')}
`; item.onclick = () => { S.qIdx = i; playCurrent(); }; el.appendChild(item); @@ -702,9 +730,9 @@ export function downloadCurrent() { export function playPreview(track) { if(!track.previewUrl) { toast('Kein Preview verfΓΌgbar'); return; } - const wasPlaying = _native() ? _nativePlaying : !S.audio.paused; + const wasPlaying = HAS_BRIDGE ? _nativePlaying : !S.audio.paused; if(wasPlaying) { - if (_native()) _nsBridge('pause'); + if (HAS_BRIDGE) _nsBridge('pause'); else S.audio.pause(); } S.previewCurrent = track; @@ -727,7 +755,7 @@ export function stopPreview(resume) { document.getElementById('preview-fill').style.width = '0%'; S.previewCurrent = null; if (shouldResume) { - if (_native()) _nsBridge('play'); + if (HAS_BRIDGE) _nsBridge('play'); else if (S.audio.src) S.audio.play(); } } @@ -758,11 +786,11 @@ export function _restoreLastPlayed() { document.getElementById('player-album-sep').style.display = s.album ? '' : 'none'; document.getElementById('player-album-name').textContent = s.album || ''; const art = document.getElementById('player-art'); - const cv = coverUrl(s.coverArt, 100); + const cv = coverUrl(s.coverArt, 256); art.innerHTML = cv ? `` : '
β™ͺ
'; updateStarBtn(); document.title = `${s.title} β€” NovaSonic`; - if (_native()) return; // native will sync state via NSPlayer.onReady + if (HAS_BRIDGE) return; // native will sync state via NSPlayer.onReady // Web path: prime the audio element so play button works immediately. S.audio.src = streamUrl(s.id); S.audio.load(); diff --git a/src/views.js b/src/views.js index 0c26373..77907fa 100644 --- a/src/views.js +++ b/src/views.js @@ -250,7 +250,7 @@ export async function loadArtistDetail(artist) { tsEl.appendChild(h); const list = document.createElement('div'); list.style.marginBottom = '8px'; topSongs.forEach((s, i) => { - const cv = coverUrl(s.coverArt, 60); + const cv = coverUrl(s.coverArt, 128); const row = document.createElement('div'); row.className = 'topsong-item'; row.innerHTML = `
${i+1}
${cv ? `` : '
🎡
'}
@@ -360,7 +360,7 @@ function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) { if(partial.length) { const g = addGrid('UnvollstΓ€ndig', partial.length); partial.forEach(({ nd, itunes, have, total }) => { - const art = itunes?.artworkUrl100?.replace('100x100','300x300') || coverUrl(nd.coverArt, 300); + const art = itunes?.artworkUrl100?.replace('100x100','512x512') || coverUrl(nd.coverArt, 512); const c = makeCard(nd.name, `${have} / ${total} Tracks`, art, 'πŸ’Ώ'); c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(nd); }; c.querySelector('.card-info').onclick = () => itunes ? navigate('itunes-album-detail', itunes) : navigate('album-detail', { ...nd, _fromArtist: artist }); @@ -471,7 +471,7 @@ export async function loadAlbumDetail(album) { return `${esc(name)}`; }).join(' β€’ ') || ''; const cw = document.getElementById('album-cover-wrap'); - const cv = coverUrl(album.coverArt, 300); + const cv = coverUrl(album.coverArt, 512); cw.innerHTML = cv ? `` : '
πŸ’Ώ
'; const sl = document.getElementById('album-songs'); sl.innerHTML = '
'; @@ -500,7 +500,7 @@ export async function loadPlaylists() { g.innerHTML = ''; if(!lists.length) { g.innerHTML = '
🎢

Keine Playlists vorhanden

'; return; } lists.forEach(pl => { - const cv = coverUrl(pl.coverArt, 200); + const cv = coverUrl(pl.coverArt, 512); const c = makeCard(pl.name, pl.songCount+' Titel', cv, '🎢'); c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayPlaylist(pl); }; c.querySelector('.card-info').onclick = () => navigate('playlist-detail', pl); @@ -531,7 +531,7 @@ export async function loadPlaylistDetail(pl) { S._currentPlaylist = {...pl}; document.getElementById('playlist-detail-name').textContent = pl.name; const cw = document.getElementById('playlist-cover'); - const cv = coverUrl(pl.coverArt, 300); + const cv = coverUrl(pl.coverArt, 512); cw.innerHTML = cv ? `` : '
🎢
'; document.getElementById('btn-edit-playlist').onclick = () => openEditPlModal(S._currentPlaylist); const sl = document.getElementById('playlist-songs'); @@ -701,7 +701,7 @@ export async function doLocalSearch(q) { const s = mkSection('Titel'); const g = document.createElement('div'); g.className = 'scroll-row'; songs.forEach((so,i) => { - const cv = coverUrl(so.coverArt, 200); + const cv = coverUrl(so.coverArt, 256); const c = makeCard(so.title, so.artist||'', cv, '🎡'); c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); playQueue(songs, i); }; c.onclick = () => playQueue(songs, i);