From 711a0846862c860320764fffa5397057b592e9f8 Mon Sep 17 00:00:00 2001 From: Honey Date: Wed, 12 Aug 2026 00:24:55 +0200 Subject: [PATCH] fix(android): scrobble respects 1/3 & 2/3 again; no settings flicker on open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../kotlin/com/novasonic/app/player/PlayerController.kt | 6 +++++- .../kotlin/com/novasonic/app/ui/screen/SettingsScreen.kt | 9 +++++++++ .../com/novasonic/app/viewmodel/SettingsViewModel.kt | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt b/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt index a333f39..6f1e5af 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/player/PlayerController.kt @@ -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) diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/SettingsScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/SettingsScreen.kt index ff1babf..ebe444d 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/SettingsScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/SettingsScreen.kt @@ -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 diff --git a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/SettingsViewModel.kt b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/SettingsViewModel.kt index 8d31278..d0df2a8 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/SettingsViewModel.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/SettingsViewModel.kt @@ -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, ) } }