fix: login flash, album name casing, lrclib duration mismatch

- Android: show nothing while DataStore loads credentials (null initial
  value) to prevent login screen flash on startup when already logged in
- Web: dispAlbumName now only uses sortName as romaji for Japanese titles;
  English albums with lowercase ALBUMSORT tags now display correct casing
- lrclib: add no-duration fallback exact match to find songs where local
  file duration differs from lrclib entry (e.g. Sanctuary Opening 5:58 vs 4:24)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-15 22:50:11 +02:00
1 parent 0272b9e99f
commit f7012dab49
5 files changed
+34 -8

No files matched your search

@@ -68,7 +68,7 @@ object LrcLibClient {
val cacheKey = "$artist|$title"
if (cache.containsKey(cacheKey)) return cache[cacheKey]
// 1. lrclib exact match — single request, only trust HTTP 200
// 1a. lrclib exact match with duration
val exact = runCatching {
val resp = client.get("https://lrclib.net/api/get") {
parameter("artist_name", artist)
@@ -78,9 +78,19 @@ object LrcLibClient {
}
if (resp.status == HttpStatusCode.OK) resp.body<LrcLibResponse>() else null
}.getOrNull()
if (exact.hasContent()) return exact.also { cache[cacheKey] = it }
// 1b. lrclib exact match without duration — handles version mismatches (e.g. 5:58 file vs 4:24 in lrclib)
val exactNoDur = if (durationSecs != null && !album.isNullOrBlank()) runCatching {
val resp = client.get("https://lrclib.net/api/get") {
parameter("artist_name", artist)
parameter("track_name", title)
parameter("album_name", album)
}
if (resp.status == HttpStatusCode.OK) resp.body<LrcLibResponse>() else null
}.getOrNull() else null
if (exactNoDur.hasContent()) return exactNoDur.also { cache[cacheKey] = it }
// 2+3. lrclib search — artist+title and title-only in parallel (2 requests, not 3)
val lrclibResult = coroutineScope {
val artistDef = async {
@@ -35,12 +35,15 @@ private fun NovaSonicRoot() {
val context = LocalContext.current
val prefs = remember { PrefsStore(context) }
val creds by prefs.credentials.collectAsStateWithLifecycle(
initialValue = PrefsStore.Credentials("", "", ""),
initialValue = null,
)
var forceLogin by remember { mutableStateOf(false) }
if (!creds.isValid || forceLogin) {
// null = still loading from DataStore; don't show anything yet to avoid login screen flash
if (creds == null) return
if (!creds!!.isValid || forceLogin) {
LoginScreen(onLoggedIn = { forceLogin = false })
} else {
AppNavigation()
@@ -118,7 +118,7 @@ fun NowPlayingScreen(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
.padding(horizontal = 20.dp)
.padding(bottom = 16.dp),
.padding(bottom = 28.dp),
) {
NpCircleBtn(onClick = onDismiss) {
Icon(Icons.Default.KeyboardArrowDown, "Schließen",
@@ -386,7 +386,7 @@ fun NowPlayingScreen(
// Center of active line sits at 30% from top (clearly upper area)
val targetY = (lyricsPaddingPx + activeIdx * lineHeightPx + lineHeightPx / 2f
- lyricsViewH * 0.30f).coerceAtLeast(0f)
lyricsScrollState.animateScrollTo(targetY.toInt(), tween<Int>(280, easing = FastOutSlowInEasing))
lyricsScrollState.animateScrollTo(targetY.toInt(), tween<Float>(280, easing = FastOutSlowInEasing))
}
when {