fix: cover art caching, skip desync, notification tap opens now playing
- api.js: cache coverUrl() per id+size so WebView HTTP cache can work;
previously each call generated a new random salt/token → unique URL →
no cache hits → images re-fetched on every render
- player.js: replace _nsBridge('next')/'prev' with explicit seekToIndex
in nextSong/prevSong so native always jumps to the exact same index JS
computed; 'next'/'prev' relied on native's internal pointer being in sync
which caused 'UI shows new song but audio plays old one' when they drifted
- MusicService: set setSessionActivity PendingIntent on MediaSession so
tapping the playback notification opens the app
- MainActivity: handle open_now_playing extra in onCreate (cold start)
and onNewIntent (app already running) to show the Now Playing screen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
66099c12f0
commit
ba382aa6d9
4 files changed
+58
-5
No files matched your search
@@ -29,6 +29,7 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
|
|||||||
private lateinit var webBridge: WebBridge
|
private lateinit var webBridge: WebBridge
|
||||||
private var controllerFuture: ListenableFuture<MediaController>? = null
|
private var controllerFuture: ListenableFuture<MediaController>? = null
|
||||||
private var mediaController: MediaController? = null
|
private var mediaController: MediaController? = null
|
||||||
|
private var pendingOpenNowPlaying = false
|
||||||
|
|
||||||
// ─── Back-press handling ──────────────────────────────────────────────────
|
// ─── Back-press handling ──────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -73,6 +74,19 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
|
|||||||
|
|
||||||
val url = BuildConfig.NOVASONIC_URL.ifEmpty { "https://demo.navidrome.org" }
|
val url = BuildConfig.NOVASONIC_URL.ifEmpty { "https://demo.navidrome.org" }
|
||||||
webView.loadUrl(url)
|
webView.loadUrl(url)
|
||||||
|
|
||||||
|
// Notification tap while app was not running — defer until page is loaded.
|
||||||
|
if (intent?.getBooleanExtra("open_now_playing", false) == true) {
|
||||||
|
pendingOpenNowPlaying = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
// Notification tap while app is already running — WebView is ready immediately.
|
||||||
|
if (intent.getBooleanExtra("open_now_playing", false)) {
|
||||||
|
webView.post { webView.evaluateJavascript("if(window.openNowPlaying)window.openNowPlaying()", null) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() { super.onResume(); webView.onResume() }
|
override fun onResume() { super.onResume(); webView.onResume() }
|
||||||
@@ -128,6 +142,10 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
|
|||||||
updateStateSnapshot()
|
updateStateSnapshot()
|
||||||
callJs("onReady")
|
callJs("onReady")
|
||||||
}
|
}
|
||||||
|
if (pendingOpenNowPlaying) {
|
||||||
|
pendingOpenNowPlaying = false
|
||||||
|
webView.post { webView.evaluateJavascript("if(window.openNowPlaying)window.openNowPlaying()", null) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
webView.webChromeClient = WebChromeClient()
|
webView.webChromeClient = WebChromeClient()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.novasonic.app
|
package com.novasonic.app
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -91,8 +92,19 @@ class MusicService : MediaSessionService() {
|
|||||||
.setWakeMode(C.WAKE_MODE_NETWORK) // holds WakeLock + WifiLock through Doze
|
.setWakeMode(C.WAKE_MODE_NETWORK) // holds WakeLock + WifiLock through Doze
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
// Tapping the notification opens the app directly to the Now Playing screen.
|
||||||
|
val notifTapIntent = Intent(this, MainActivity::class.java).apply {
|
||||||
|
putExtra("open_now_playing", true)
|
||||||
|
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
}
|
||||||
|
val notifTapPending = PendingIntent.getActivity(
|
||||||
|
this, 0, notifTapIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
mediaSession = MediaSession.Builder(this, player)
|
mediaSession = MediaSession.Builder(this, player)
|
||||||
.setBitmapLoader(bitmapLoader) // use cached loader for notification artwork
|
.setBitmapLoader(bitmapLoader) // use cached loader for notification artwork
|
||||||
|
.setSessionActivity(notifTapPending)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
player.addListener(PlayerListener())
|
player.addListener(PlayerListener())
|
||||||
|
|||||||
+11
-1
@@ -16,6 +16,11 @@ export async function api(method, extra='') {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
export function streamUrl(id) { return apiUrl('stream', `&id=${id}&format=raw`); }
|
export function streamUrl(id) { return apiUrl('stream', `&id=${id}&format=raw`); }
|
||||||
|
|
||||||
|
// Stable per-session cache so the same id+size always produces the same URL.
|
||||||
|
// Consistent URLs let WebView's HTTP cache work — without this every render
|
||||||
|
// generates a new token (random salt) and the image is re-fetched every time.
|
||||||
|
const _coverUrlCache = new Map();
|
||||||
export async function getLyricsBySongId(songId) {
|
export async function getLyricsBySongId(songId) {
|
||||||
try {
|
try {
|
||||||
const r = await api('getLyricsBySongId', `&id=${songId}`);
|
const r = await api('getLyricsBySongId', `&id=${songId}`);
|
||||||
@@ -23,7 +28,12 @@ export async function getLyricsBySongId(songId) {
|
|||||||
} catch { return []; }
|
} catch { return []; }
|
||||||
}
|
}
|
||||||
export function downloadUrl(id) { return apiUrl('download', `&id=${id}`); }
|
export function downloadUrl(id) { return apiUrl('download', `&id=${id}`); }
|
||||||
export function coverUrl(id, size=300) { return id ? apiUrl('getCoverArt', `&id=${id}&size=${size}`) : null; }
|
export function coverUrl(id, size=300) {
|
||||||
|
if (!id) return null;
|
||||||
|
const key = `${id}:${size}`;
|
||||||
|
if (!_coverUrlCache.has(key)) _coverUrlCache.set(key, apiUrl('getCoverArt', `&id=${id}&size=${size}`));
|
||||||
|
return _coverUrlCache.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
// ─── NAVIDROME REST AUTH ───
|
// ─── NAVIDROME REST AUTH ───
|
||||||
export async function ndAuth() {
|
export async function ndAuth() {
|
||||||
|
|||||||
+17
-4
@@ -496,13 +496,20 @@ export function prevSong() {
|
|||||||
_clearPreload();
|
_clearPreload();
|
||||||
if (HAS_BRIDGE) {
|
if (HAS_BRIDGE) {
|
||||||
_setSkipping();
|
_setSkipping();
|
||||||
if (_lastPositionMs > 3000) { _resetProgressUI(); _nsBridge('prev'); return; }
|
if (_lastPositionMs > 3000) {
|
||||||
|
// Restart current song explicitly — avoids relying on ExoPlayer's implicit prev-vs-restart logic.
|
||||||
|
_resetProgressUI();
|
||||||
|
_nsBridge('seekTo', 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (S.qIdx > 0) {
|
if (S.qIdx > 0) {
|
||||||
S.qIdx--;
|
S.qIdx--;
|
||||||
const s = S.queue[S.qIdx];
|
const s = S.queue[S.qIdx];
|
||||||
if (s) { _resetProgressUI(); updatePlayerUI(s, true); onNpSongChange(s.id); renderQueue(); }
|
if (s) { _resetProgressUI(); updatePlayerUI(s, true); onNpSongChange(s.id); renderQueue(); }
|
||||||
|
// Explicit index so native doesn't have to agree on what "previous" means.
|
||||||
|
_nsBridge('seekToIndex', S.qIdx, 0);
|
||||||
|
_nsBridge('play');
|
||||||
}
|
}
|
||||||
_nsBridge('prev');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(S.audio.currentTime > 3) { _resetProgressUI(); S.audio.currentTime = 0; return; }
|
if(S.audio.currentTime > 3) { _resetProgressUI(); S.audio.currentTime = 0; return; }
|
||||||
@@ -518,8 +525,13 @@ export function nextSong() {
|
|||||||
S.qIdx = ni;
|
S.qIdx = ni;
|
||||||
const s = S.queue[S.qIdx];
|
const s = S.queue[S.qIdx];
|
||||||
if (s) { _resetProgressUI(); updatePlayerUI(s, true); onNpSongChange(s.id); renderQueue(); }
|
if (s) { _resetProgressUI(); updatePlayerUI(s, true); onNpSongChange(s.id); renderQueue(); }
|
||||||
}
|
// Explicit index prevents desync when JS and native queue pointers diverge.
|
||||||
|
_nsBridge('seekToIndex', ni, 0);
|
||||||
|
_nsBridge('play');
|
||||||
|
} else {
|
||||||
|
// Shuffle mode: native picks the shuffled-next item; onTrackChanged will confirm the index.
|
||||||
_nsBridge('next');
|
_nsBridge('next');
|
||||||
|
}
|
||||||
if (S.radioMode && S.queue.length - S.qIdx < 5) fillRadioQueue();
|
if (S.radioMode && S.queue.length - S.qIdx < 5) fillRadioQueue();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -677,7 +689,8 @@ function _initBarScrub(bar, fill, onMove) {
|
|||||||
_touchStartX = e.touches[0].clientX;
|
_touchStartX = e.touches[0].clientX;
|
||||||
_touchMoved = false;
|
_touchMoved = false;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}, {passive:true});
|
e.preventDefault(); // block synthetic mousedown/click that would trigger scrub on plain tap
|
||||||
|
});
|
||||||
bar.addEventListener('touchmove', e => {
|
bar.addEventListener('touchmove', e => {
|
||||||
const dx = Math.abs(e.touches[0].clientX - _touchStartX);
|
const dx = Math.abs(e.touches[0].clientX - _touchStartX);
|
||||||
if (!_touchMoved && dx >= 8) { _touchMoved = true; startScrub(getPos(e.touches[0].clientX)); }
|
if (!_touchMoved && dx >= 8) { _touchMoved = true; startScrub(getPos(e.touches[0].clientX)); }
|
||||||
|
|||||||
Reference in new issue
Block a user