feat: dynamic NP cover color, taller settings modal, fix Android song sync
- Now Playing: add "Dynamische Hintergrundfarbe" toggle in Theme settings; when on, np-bg uses higher saturation/brightness so the cover color really shows through the blurred background - Settings modal: extend to bottom nav on mobile (was leaving ~74px dead space for the player bar which the overlay already covers at z-index 200) - Android: fix onTrackChanged always staying on the first song — the snapshot from MediaController can lag behind ExoPlayer on auto-advance, causing the stale currentIndex to overwrite the correct p.index; now only advance to a higher live index (for rapid-skip catch-up) and use p.trackId directly instead of the potentially-stale snapshot id for the queue-mismatch guard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
c405465f5c
commit
9d81cdde31
5 files changed
+32
-12
No files matched your search
@@ -663,6 +663,13 @@
|
|||||||
Theme
|
Theme
|
||||||
</h3>
|
</h3>
|
||||||
<div id="theme-picker" class="theme-picker"></div>
|
<div id="theme-picker" class="theme-picker"></div>
|
||||||
|
<div class="edit-pl-toggle-row" style="margin-top:14px">
|
||||||
|
<div>
|
||||||
|
<span>Dynamische Hintergrundfarbe</span>
|
||||||
|
<div class="hint" style="margin-top:2px">Jetzt-Läuft-Hintergrund nach Cover-Hauptfarbe</div>
|
||||||
|
</div>
|
||||||
|
<div id="s-cover-color-bg" class="toggle" onclick="this.classList.toggle('on')"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Sticky footer -->
|
<!-- Sticky footer -->
|
||||||
|
|||||||
+2
-1
@@ -9,7 +9,7 @@ import {
|
|||||||
initProgressScrub, updateProgress, updatePreviewProgress, stopPreview,
|
initProgressScrub, updateProgress, updatePreviewProgress, stopPreview,
|
||||||
toggleRadioMode, starCurrent, downloadCurrent, renderQueue, playPreview,
|
toggleRadioMode, starCurrent, downloadCurrent, renderQueue, playPreview,
|
||||||
toggleStar, dlSongLocal, _applyRadioBtn, _applyShuffleRepeat, _restoreLastPlayed, setPlayIcon,
|
toggleStar, dlSongLocal, _applyRadioBtn, _applyShuffleRepeat, _restoreLastPlayed, setPlayIcon,
|
||||||
npToggleStar, updateMediaSession,
|
npToggleStar, updateMediaSession, updateNpColorMode,
|
||||||
} from './player.js';
|
} from './player.js';
|
||||||
import {
|
import {
|
||||||
navigate, _navGoBack, _navLoad, onHeaderSearch, closeHeaderSearch,
|
navigate, _navGoBack, _navLoad, onHeaderSearch, closeHeaderSearch,
|
||||||
@@ -125,6 +125,7 @@ window.addEventListener('load', () => {
|
|||||||
S.cfg = JSON.parse(saved);
|
S.cfg = JSON.parse(saved);
|
||||||
if(!S.cfg.backendUrl && window.NS_BACKEND_URL?.trim()) S.cfg.backendUrl = window.NS_BACKEND_URL.trim().replace(/\/$/,'');
|
if(!S.cfg.backendUrl && window.NS_BACKEND_URL?.trim()) S.cfg.backendUrl = window.NS_BACKEND_URL.trim().replace(/\/$/,'');
|
||||||
startApp();
|
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(); }
|
if(window.NS_SERVER_URL?.trim()) { const el=document.getElementById('srv-url'); if(el&&!el.value) el.value=window.NS_SERVER_URL.trim(); }
|
||||||
|
|
||||||
|
|||||||
+14
-9
@@ -184,20 +184,20 @@ window.NSPlayer = {
|
|||||||
try {
|
try {
|
||||||
const p = JSON.parse(payloadJson);
|
const p = JSON.parse(payloadJson);
|
||||||
if (typeof p.index !== 'number') return;
|
if (typeof p.index !== 'number') return;
|
||||||
// Reconcile with the live native state so stale out-of-order callbacks
|
// p.index comes directly from ExoPlayer (always accurate).
|
||||||
// (e.g. from rapid skips) don't overwrite the already-correct index.
|
// 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 idx = p.index;
|
||||||
let liveId = '';
|
|
||||||
try {
|
try {
|
||||||
const live = JSON.parse(window.NSBridge.getState() || '{}');
|
const live = JSON.parse(window.NSBridge.getState() || '{}');
|
||||||
if (typeof live.currentIndex === 'number') idx = live.currentIndex;
|
if (typeof live.currentIndex === 'number' && live.currentIndex > idx) idx = live.currentIndex;
|
||||||
liveId = live.currentId || '';
|
|
||||||
} catch {}
|
} catch {}
|
||||||
if (idx < 0 || idx >= S.queue.length) return;
|
if (idx < 0 || idx >= S.queue.length) return;
|
||||||
// Reject stale callbacks from the old queue: if the song ID at this index
|
// Verify using the trackId from the callback payload (ExoPlayer-direct, not the
|
||||||
// doesn't match what native is actually playing, the queues are mismatched.
|
// potentially-stale snapshot) so auto-advance is never rejected by a lagged snapshot.
|
||||||
// Allow through if reason='queue' (we triggered a setQueue) or if no IDs to compare.
|
const trackId = p.trackId || '';
|
||||||
if (liveId && S.queue[idx]?.id && String(liveId) !== String(S.queue[idx].id) && p.reason !== 'queue') return;
|
if (trackId && S.queue[idx]?.id && String(trackId) !== String(S.queue[idx].id) && p.reason !== 'queue') return;
|
||||||
const indexChanged = (idx !== S.qIdx);
|
const indexChanged = (idx !== S.qIdx);
|
||||||
S.qIdx = idx;
|
S.qIdx = idx;
|
||||||
const s = S.queue[S.qIdx];
|
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; }
|
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 _npBgCurr = 'a';
|
||||||
let _npBgLastUrl = '';
|
let _npBgLastUrl = '';
|
||||||
function _crossfadeNpBg(cv) {
|
function _crossfadeNpBg(cv) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { esc, formatDur, toast } from './utils.js';
|
|||||||
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache } from './api.js';
|
import { api, coverUrl, ndAuth, fetchRomajiIndex, romajiConvert, romajiLookup, invalidateRomajiIndex, invalidateCoverCache } from './api.js';
|
||||||
import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
|
import { THEMES, applyTheme, getCurrentThemeId } from './themes.js';
|
||||||
// Circular imports — safe (only used inside function bodies)
|
// 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';
|
import { navigate, loadPlaylists } from './views.js';
|
||||||
|
|
||||||
// ─── SIDEBAR ───
|
// ─── SIDEBAR ───
|
||||||
@@ -49,6 +49,7 @@ export function openSettings() {
|
|||||||
document.getElementById('s-backend').value = S.cfg.backendUrl || '';
|
document.getElementById('s-backend').value = S.cfg.backendUrl || '';
|
||||||
document.getElementById('s-show-playcount').classList.toggle('on', !!S.cfg.showPlayCount);
|
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-show-romaji').classList.toggle('on', !!S.cfg.showRomaji);
|
||||||
|
document.getElementById('s-cover-color-bg').classList.toggle('on', !!S.cfg.coverColorBg);
|
||||||
_renderThemePicker();
|
_renderThemePicker();
|
||||||
document.getElementById('settings-modal').classList.add('open');
|
document.getElementById('settings-modal').classList.add('open');
|
||||||
checkBackend();
|
checkBackend();
|
||||||
@@ -86,6 +87,8 @@ export function saveSettings() {
|
|||||||
S.cfg.showPlayCount = document.getElementById('s-show-playcount').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');
|
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.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));
|
localStorage.setItem('ns_cfg', JSON.stringify(S.cfg));
|
||||||
closeSettings(); toast('✅ Gespeichert');
|
closeSettings(); toast('✅ Gespeichert');
|
||||||
if (romajiChanged) {
|
if (romajiChanged) {
|
||||||
|
|||||||
+5
-1
@@ -563,7 +563,7 @@ button:focus{outline:none}
|
|||||||
.toast-container{bottom:calc(var(--player-h) + 80px + 16px)}
|
.toast-container{bottom:calc(var(--player-h) + 80px + 16px)}
|
||||||
.jobs-panel{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}
|
.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}
|
#settings-modal .modal{max-height:100%!important;overflow:hidden}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +590,10 @@ button:focus{outline:none}
|
|||||||
will-change:opacity;
|
will-change:opacity;
|
||||||
z-index:0;
|
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{
|
.np-inner{
|
||||||
position:relative;z-index:1;
|
position:relative;z-index:1;
|
||||||
|
|||||||
Reference in new issue
Block a user