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
+45 -39
@@ -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<com.novasonic.app.api.model.SongArtist> =
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<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 ->
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()) {