fix(android): scrobble respects 1/3 & 2/3 again; no settings flicker on open

Scrobble timing: at onMediaItemTransition ctrl.duration is still TIME_UNSET (→0),
so getScrobbleDelayMs's third/two_thirds modes hit the 15s floor and scrobbled way
too early (both Navidrome + our play-log). Use the song's KNOWN duration
(song.duration), falling back to the player only if absent. Web was unaffected
(it already used the song's duration).

Settings flicker: the ViewModel started with default state (theme = Dark Ocean)
and rendered a frame before DataStore emitted the real values, so the theme picker
visibly jumped. Gate the form on a 'loaded' flag set after the first emit; show a
spinner until then.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-12 00:24:55 +02:00
1 parent 0c7132a7f2
commit 711a084686
3 files changed
+16 -1

No files matched your search

@@ -315,7 +315,11 @@ class PlayerController(private val context: Context, private val repo: MusicRepo
val mediaId = ctrl.currentMediaItem?.mediaId ?: return
if (mediaId.startsWith("abs:")) return
val song = queue.getOrNull(ctrl.currentMediaItemIndex) ?: return
val dur = ctrl.duration.coerceAtLeast(0)
// Use the song's KNOWN duration — at a media transition ctrl.duration is still
// TIME_UNSET (→0), which collapsed the 1/3 / 2/3 modes to the 15s floor and scrobbled
// far too early. Fall back to the player only if the song has no duration.
val songDurMs = (song.duration?.toLong() ?: 0L) * 1000L
val dur = if (songDurMs > 0) songDurMs else ctrl.duration.coerceAtLeast(0)
scheduleScrobble(song, dur)
scope.launch { runCatching { repo.saveLastSong(song) } }
prefetchLyrics(song)
@@ -67,6 +67,15 @@ fun SettingsScreen(onOpenStats: () -> Unit = {}) {
)
}
// Wait for the persisted settings before drawing the form — otherwise the default state
// (e.g. theme = Dark Ocean) flashes for a frame and then jumps to the real values.
if (!state.loaded) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = NsAccent)
}
return
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
@@ -35,6 +35,7 @@ data class SettingsUiState(
val absUrl: String = "",
val absApiKey: String = "",
val absChapterTrackMode: Boolean = false,
val loaded: Boolean = false,
val isSaving: Boolean = false,
val savedOk: Boolean = false,
val error: String? = null,
@@ -66,6 +67,7 @@ class SettingsViewModel(app: Application) : AndroidViewModel(app) {
absUrl = s.absUrl,
absApiKey = s.absApiKey,
absChapterTrackMode = s.absChapterTrackMode,
loaded = true,
)
}
}