From 8c8f03503b0b1907b9e9f633405642e33017c48b Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 15 Jun 2026 00:44:59 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20multi-artist=20click=20in=20NowPlaying?= =?UTF-8?q?=20=E2=80=94=20individual=20composables=20per=20artist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace AnnotatedString/ClickableText (offset-based click was unreliable) with a horizontally-scrollable Row of individual Text composables. Each artist has its own explicit clickable modifier bound directly to its ID. ID de-duplication: if Navidrome copies the primary artistId to all entries in the artists array, only the first keeps the ID; others render as non-navigable text (dimmed NsAccent) instead of silently going to wrong page. Fallback: split song.artist by ';' when artists array is absent/single, matching the web app's parseArtists() logic. Co-Authored-By: Claude Sonnet 4.6 --- .../app/ui/screen/NowPlayingScreen.kt | 84 ++++++++++--------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/NowPlayingScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/NowPlayingScreen.kt index 57079f6..489c24c 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/NowPlayingScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/NowPlayingScreen.kt @@ -33,14 +33,11 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import coil.request.ImageRequest -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.rememberScrollState import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle -import androidx.compose.foundation.text.ClickableText import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -214,43 +211,52 @@ fun NowPlayingScreen( ) Spacer(Modifier.height(3.dp)) if (song != null && !song.artist.isNullOrBlank()) { - // Build list: prefer song.artists (OpenSubsonic, has real IDs for all), - // fall back to single entry from artist/artistId - val artistList = if (song.artists.size > 1) song.artists - else listOf(com.novasonic.app.api.model.SongArtist( - id = song.artistId ?: "", name = song.artist!!)) + // Prefer OpenSubsonic artists array (has IDs per contributor). + // Fall back to splitting artist string by ';' (same as web app). + val raw: List = + if (song.artists.size > 1) song.artists + else song.artist!!.split(";").map { part -> + val n = part.trim() + com.novasonic.app.api.model.SongArtist( + id = if (n.equals(song.artist?.trim(), ignoreCase = true)) song.artistId ?: "" else "", + name = n, + ) + }.filter { it.name.isNotBlank() } - if (onArtistClick != null) { - val annotated = buildAnnotatedString { - artistList.forEachIndexed { idx, a -> - if (idx > 0) { - withStyle(SpanStyle(color = Color.White.copy(0.4f), - fontFamily = Outfit, fontSize = 15.sp)) { append(" · ") } - } - pushStringAnnotation("ARTIST_ID", a.id) - withStyle(SpanStyle(color = NsAccent, - fontFamily = Outfit, - fontWeight = FontWeight.SemiBold, - fontSize = 15.sp)) { append(a.name) } - pop() + // De-duplicate: if multiple entries share the same ID (Navidrome + // sometimes copies the primary artistId to all entries), only the + // first occurrence keeps it — others become non-navigable. + val seenIds = mutableSetOf() + val artistList = raw.map { a -> + if (a.id.isNotBlank() && seenIds.add(a.id)) a + else a.copy(id = "") + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.horizontalScroll(rememberScrollState()), + ) { + artistList.forEachIndexed { idx, a -> + if (idx > 0) { + Text( + " · ", fontFamily = Outfit, + fontSize = 14.sp, color = Color.White.copy(0.35f), + ) } + val navigable = onArtistClick != null && a.id.isNotBlank() + Text( + text = a.name, + fontFamily = Outfit, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (navigable) NsAccent else NsAccent.copy(alpha = 0.55f), + modifier = if (navigable) + Modifier.clickable { onArtistClick!!(a.id) } + else Modifier, + ) } - ClickableText( - text = annotated, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - onClick = { offset -> - annotated.getStringAnnotations("ARTIST_ID", offset, offset) - .firstOrNull()?.let { onArtistClick(it.item) } - }, - ) - } else { - Text( - song.artist!!, - fontFamily = Outfit, fontWeight = FontWeight.SemiBold, - fontSize = 15.sp, color = NsAccent, - maxLines = 2, overflow = TextOverflow.Ellipsis, - ) } } if (!song?.album.isNullOrBlank()) {