feat: wire NSBridge fully + HD artwork + disable watchdog in wrapper mode

player.js
- Replace `_native()` function with `const HAS_BRIDGE` (evaluated once at
  module load so bridge detection is rock-solid)
- Add `buildTrackList()` — maps S.queue to NSBridge Track array with 1024px
  artworkUrl so lock-screen cover is crisp on high-DPR displays
- Add `_pushQueueIfBridge()` — called by `fillRadioQueue()` so radio top-ups
  are pushed to ExoPlayer in real time (fixes autoplay gap after radio refill)
- `playQueue`: route through `NSBridge.setQueue` when bridge present; old
  `S.audio` path retained for browser/PWA fallback
- `playCurrent`: `NSBridge.seekToIndex + play` in bridge mode
- `nextSong / prevSong / togglePlay / onEnded`: delegate to bridge, skip local
  S.qIdx manipulation (native sends onTrackChanged callback instead)
- `seekTo / _initBarScrub`: use `_lastDurationMs` for ms-accurate seeks
- `onTrackChanged`: guard with `p.index !== S.qIdx` to avoid redundant redraws
- `updateMediaSession`: artwork sizes 96/300/512 → 256/512/1024
- Player-bar cover: 100 → 256px; NP cover/bg: 400 → 1024px;
  queue items: 60 → 128px; preload hint sizes: 96/300/512 → 256/512/1024

main.js
- Wrap watchdog setInterval in `typeof window.NSBridge === 'undefined'` guard;
  the foreground service keeps ExoPlayer alive — JS watchdog not needed there

views.js
- Album detail / playlist detail covers: 300 → 512px
- Playlist grid cards: 200 → 512px
- Search result song cards: 200 → 256px
- Artist top-songs / all-songs thumbnails: 60 → 128px
- Partial album art in _renderArtistAlbums: 300 → 512px

components.js
- makeAlbumCard: 200 → 512px
- makeSongItem: 60 → 128px

nowplaying.js
- NP queue items: 60 → 128px
- NP action sheet thumb: 80 → 256px

sw.js
- Bump CACHE to ns-v41 so stale small-cover entries are evicted on next load

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-09 10:33:43 +02:00
1 parent 0ee2a62adb
commit 76ecfe2268
6 files changed
+144 -113

No files matched your search

+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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) => {
+19 -16
View File
@@ -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();
+2 -2
View File
@@ -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 = `<div class="nqi-num">${i+1}</div><div class="nqi-art">${cv ? `<img src="${cv}">` : '♪'}</div><div class="nqi-info"><div class="nqi-title">${esc(s.title)}</div><div class="nqi-artist">${esc(s.artist||'')}</div></div>`;
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 = `
<div class="thumb">${cv ? `<img src="${cv}">` : '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:22px">🎵</div>'}</div>
<div><div class="title">${esc(s.title)}</div><div class="sub">${esc(s.artist||'')}${s.album?' · '+esc(s.album):''}</div></div>`;
+114 -86
View File
@@ -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 ? `<img src="${cv}" style="width:100%;height:100%;object-fit:cover">` : '<div class="no-art">♪</div>';
@@ -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 = `<div class="qi-idx">${i+1}</div><div class="qi-art">${cv ? `<img src="${cv}">` : '♪'}</div><div class="qi-info"><div class="qi-title">${esc(s.title)}</div><div class="qi-artist">${esc(s.artist||'')}</div></div>`;
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 ? `<img src="${cv}" style="width:100%;height:100%;object-fit:cover">` : '<div class="no-art">♪</div>';
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();
+6 -6
View File
@@ -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 = `<div class="topsong-num">${i+1}</div>
<div class="topsong-thumb">${cv ? `<img src="${cv}" loading="lazy">` : '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:18px">🎵</div>'}</div>
@@ -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 `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(name)}','${esc(aid)}')">${esc(name)}</span>`;
}).join('<span style="color:var(--text3)"> • </span>') || '';
const cw = document.getElementById('album-cover-wrap');
const cv = coverUrl(album.coverArt, 300);
const cv = coverUrl(album.coverArt, 512);
cw.innerHTML = cv ? `<img src="${cv}" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>💿</div>'">` : '<div class="no-art">💿</div>';
const sl = document.getElementById('album-songs');
sl.innerHTML = '<div class="spinner"></div>';
@@ -500,7 +500,7 @@ export async function loadPlaylists() {
g.innerHTML = '';
if(!lists.length) { g.innerHTML = '<div class="empty-state"><div class="icon">🎶</div><p>Keine Playlists vorhanden</p></div>'; 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 ? `<img src="${cv}" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>🎶</div>'">` : '<div class="no-art">🎶</div>';
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);