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:
1 parent
0ee2a62adb
commit
76ecfe2268
6 files changed
+144
-113
No files matched your search
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user