diff --git a/index.html b/index.html
index 8eeb9bf..b80583f 100644
--- a/index.html
+++ b/index.html
@@ -663,6 +663,13 @@
Theme
+
+
+
Dynamische Hintergrundfarbe
+
Jetzt-Läuft-Hintergrund nach Cover-Hauptfarbe
+
+
+
diff --git a/src/main.js b/src/main.js
index 58515d4..57da774 100644
--- a/src/main.js
+++ b/src/main.js
@@ -9,7 +9,7 @@ import {
initProgressScrub, updateProgress, updatePreviewProgress, stopPreview,
toggleRadioMode, starCurrent, downloadCurrent, renderQueue, playPreview,
toggleStar, dlSongLocal, _applyRadioBtn, _applyShuffleRepeat, _restoreLastPlayed, setPlayIcon,
- npToggleStar, updateMediaSession,
+ npToggleStar, updateMediaSession, updateNpColorMode,
} from './player.js';
import {
navigate, _navGoBack, _navLoad, onHeaderSearch, closeHeaderSearch,
@@ -125,6 +125,7 @@ window.addEventListener('load', () => {
S.cfg = JSON.parse(saved);
if(!S.cfg.backendUrl && window.NS_BACKEND_URL?.trim()) S.cfg.backendUrl = window.NS_BACKEND_URL.trim().replace(/\/$/,'');
startApp();
+ updateNpColorMode();
}
if(window.NS_SERVER_URL?.trim()) { const el=document.getElementById('srv-url'); if(el&&!el.value) el.value=window.NS_SERVER_URL.trim(); }
diff --git a/src/player.js b/src/player.js
index 7b501eb..7e20340 100644
--- a/src/player.js
+++ b/src/player.js
@@ -184,20 +184,20 @@ window.NSPlayer = {
try {
const p = JSON.parse(payloadJson);
if (typeof p.index !== 'number') return;
- // Reconcile with the live native state so stale out-of-order callbacks
- // (e.g. from rapid skips) don't overwrite the already-correct index.
+ // p.index comes directly from ExoPlayer (always accurate).
+ // Only advance to a higher live index to handle rapid forward skips where
+ // the callback can arrive before we processed the previous one.
+ // Never let a stale MediaController snapshot pull us BACK to an old index.
let idx = p.index;
- let liveId = '';
try {
const live = JSON.parse(window.NSBridge.getState() || '{}');
- if (typeof live.currentIndex === 'number') idx = live.currentIndex;
- liveId = live.currentId || '';
+ if (typeof live.currentIndex === 'number' && live.currentIndex > idx) idx = live.currentIndex;
} catch {}
if (idx < 0 || idx >= S.queue.length) return;
- // Reject stale callbacks from the old queue: if the song ID at this index
- // doesn't match what native is actually playing, the queues are mismatched.
- // Allow through if reason='queue' (we triggered a setQueue) or if no IDs to compare.
- if (liveId && S.queue[idx]?.id && String(liveId) !== String(S.queue[idx].id) && p.reason !== 'queue') return;
+ // Verify using the trackId from the callback payload (ExoPlayer-direct, not the
+ // potentially-stale snapshot) so auto-advance is never rejected by a lagged snapshot.
+ const trackId = p.trackId || '';
+ if (trackId && S.queue[idx]?.id && String(trackId) !== String(S.queue[idx].id) && p.reason !== 'queue') return;
const indexChanged = (idx !== S.qIdx);
S.qIdx = idx;
const s = S.queue[S.qIdx];
@@ -484,6 +484,11 @@ export function updatePlayerUI(s, _skipMediaSession = false) {
if (_npCv && _npCv !== _npBgLastUrl) { const _pre = new Image(); _pre.src = _npCv; }
}
+// Apply/remove dynamic-color mode based on current setting.
+export function updateNpColorMode() {
+ document.getElementById('np-overlay')?.classList.toggle('dynamic-color', !!S.cfg?.coverColorBg);
+}
+
let _npBgCurr = 'a';
let _npBgLastUrl = '';
function _crossfadeNpBg(cv) {
diff --git a/src/ui.js b/src/ui.js
index e430ad0..265df4d 100644
--- a/src/ui.js
+++ b/src/ui.js
@@ -3,7 +3,7 @@ import { esc, formatDur, toast } from './utils.js';
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache } from './api.js';
import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
// Circular imports — safe (only used inside function bodies)
-import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon, nativeInsertAt, refreshDisplayMode } from './player.js';
+import { playQueue, dlSongLocal, toggleStar, togglePlay, renderQueue, syncNpStar, updateStarBtn, npToggleStar, playCurrent, _syncNpDirect, setPlayIcon, nativeInsertAt, refreshDisplayMode, updateNpColorMode } from './player.js';
import { navigate, loadPlaylists } from './views.js';
// ─── SIDEBAR ───
@@ -49,6 +49,7 @@ export function openSettings() {
document.getElementById('s-backend').value = S.cfg.backendUrl || '';
document.getElementById('s-show-playcount').classList.toggle('on', !!S.cfg.showPlayCount);
document.getElementById('s-show-romaji').classList.toggle('on', !!S.cfg.showRomaji);
+ document.getElementById('s-cover-color-bg').classList.toggle('on', !!S.cfg.coverColorBg);
_renderThemePicker();
document.getElementById('settings-modal').classList.add('open');
checkBackend();
@@ -83,9 +84,11 @@ export function saveSettings() {
if(credChanged) invalidateCoverCache();
S.cfg.lastfmKey = document.getElementById('s-lastfm').value.trim();
S.cfg.backendUrl = document.getElementById('s-backend').value.trim().replace(/\/$/,'');
- S.cfg.showPlayCount = document.getElementById('s-show-playcount').classList.contains('on');
- const romajiChanged = S.cfg.showRomaji !== document.getElementById('s-show-romaji').classList.contains('on');
- S.cfg.showRomaji = document.getElementById('s-show-romaji').classList.contains('on');
+ S.cfg.showPlayCount = document.getElementById('s-show-playcount').classList.contains('on');
+ const romajiChanged = S.cfg.showRomaji !== document.getElementById('s-show-romaji').classList.contains('on');
+ S.cfg.showRomaji = document.getElementById('s-show-romaji').classList.contains('on');
+ S.cfg.coverColorBg = document.getElementById('s-cover-color-bg').classList.contains('on');
+ updateNpColorMode();
localStorage.setItem('ns_cfg', JSON.stringify(S.cfg));
closeSettings(); toast('✅ Gespeichert');
if (romajiChanged) {
diff --git a/styles.css b/styles.css
index f83fa62..f807cc1 100644
--- a/styles.css
+++ b/styles.css
@@ -563,7 +563,7 @@ button:focus{outline:none}
.toast-container{bottom:calc(var(--player-h) + 80px + 16px)}
.jobs-panel{bottom:calc(var(--player-h) + 80px + 16px)}
.bnav-item{padding:6px 2px;box-sizing:border-box}
- #settings-modal{align-items:flex-start;padding-top:max(env(safe-area-inset-top),16px);padding-bottom:calc(var(--player-h) + 80px + env(safe-area-inset-bottom) + 8px)}
+ #settings-modal{align-items:flex-start;padding-top:max(env(safe-area-inset-top),16px);padding-bottom:calc(80px + env(safe-area-inset-bottom))}
#settings-modal .modal{max-height:100%!important;overflow:hidden}
}
@@ -590,6 +590,10 @@ button:focus{outline:none}
will-change:opacity;
z-index:0;
}
+/* Dynamic cover color mode: more vivid/saturated background */
+.np-overlay.dynamic-color .np-bg{
+ filter:blur(60px) brightness(.5) saturate(3);
+}
.np-inner{
position:relative;z-index:1;