fix: multi-artist click in NowPlaying — individual composables per artist

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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-15 00:44:59 +02:00
1 parent 55eef2f831
commit 8c8f03503b
1 file changed
+40 -34
@@ -33,14 +33,11 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import coil.request.ImageRequest import coil.request.ImageRequest
import androidx.compose.ui.text.AnnotatedString import androidx.compose.foundation.horizontalScroll
import androidx.compose.ui.text.SpanStyle import androidx.compose.foundation.rememberScrollState
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow 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.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -214,45 +211,54 @@ fun NowPlayingScreen(
) )
Spacer(Modifier.height(3.dp)) Spacer(Modifier.height(3.dp))
if (song != null && !song.artist.isNullOrBlank()) { if (song != null && !song.artist.isNullOrBlank()) {
// Build list: prefer song.artists (OpenSubsonic, has real IDs for all), // Prefer OpenSubsonic artists array (has IDs per contributor).
// fall back to single entry from artist/artistId // Fall back to splitting artist string by ';' (same as web app).
val artistList = if (song.artists.size > 1) song.artists val raw: List<com.novasonic.app.api.model.SongArtist> =
else listOf(com.novasonic.app.api.model.SongArtist( if (song.artists.size > 1) song.artists
id = song.artistId ?: "", name = song.artist!!)) 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) { // De-duplicate: if multiple entries share the same ID (Navidrome
val annotated = buildAnnotatedString { // sometimes copies the primary artistId to all entries), only the
// first occurrence keeps it — others become non-navigable.
val seenIds = mutableSetOf<String>()
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 -> artistList.forEachIndexed { idx, a ->
if (idx > 0) { if (idx > 0) {
withStyle(SpanStyle(color = Color.White.copy(0.4f), Text(
fontFamily = Outfit, fontSize = 15.sp)) { append(" · ") } " · ", fontFamily = Outfit,
fontSize = 14.sp, color = Color.White.copy(0.35f),
)
} }
pushStringAnnotation("ARTIST_ID", a.id) val navigable = onArtistClick != null && a.id.isNotBlank()
withStyle(SpanStyle(color = NsAccent, Text(
text = a.name,
fontFamily = Outfit, fontFamily = Outfit,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
fontSize = 15.sp)) { append(a.name) } fontSize = 14.sp,
pop() maxLines = 1,
}
}
ClickableText(
text = annotated,
maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
onClick = { offset -> color = if (navigable) NsAccent else NsAccent.copy(alpha = 0.55f),
annotated.getStringAnnotations("ARTIST_ID", offset, offset) modifier = if (navigable)
.firstOrNull()?.let { onArtistClick(it.item) } Modifier.clickable { onArtistClick!!(a.id) }
}, else Modifier,
)
} else {
Text(
song.artist!!,
fontFamily = Outfit, fontWeight = FontWeight.SemiBold,
fontSize = 15.sp, color = NsAccent,
maxLines = 2, overflow = TextOverflow.Ellipsis,
) )
} }
} }
}
if (!song?.album.isNullOrBlank()) { if (!song?.album.isNullOrBlank()) {
Text( Text(
song!!.album!!, song!!.album!!,