feat: multi-artist support in NowPlaying (OpenSubsonic artists array)

- Song model: add SongArtist{id,name} + artists: List<SongArtist>
  Navidrome sends this via OpenSubsonic extension — previously ignored
- NowPlaying: if song.artists has >1 entry, render each name as an
  inline AnnotatedString with ClickableText, separated by ' · ';
  every artist navigates to their own page with their real ID
  Falls back to single artist/artistId entry when array is absent
- AppNavigation: onArtistClick now receives artistId: String instead
  of a fixed closure, enabling per-artist navigation from the sheet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-15 00:37:08 +02:00
1 parent 0e651e8be3
commit f15c005a3c
3 files changed
+55 -16

No files matched your search

@@ -40,6 +40,12 @@ data class SubsonicError(val code: Int, val message: String)
// ── Domain models ─────────────────────────────────────────────────────────────
@Serializable
data class SongArtist(
val id: String = "",
val name: String = "",
)
@Serializable
data class Song(
val id: String,
@@ -48,6 +54,7 @@ data class Song(
val albumId: String? = null,
val artist: String? = null,
val artistId: String? = null,
val artists: List<SongArtist> = emptyList(),
val track: Int? = null,
val year: Int? = null,
val genre: String? = null,
@@ -324,11 +324,9 @@ fun AppNavigation() {
onShowOptions = {
playerState.currentSong?.let { s -> contextMenuSong = s }
},
onArtistClick = playerState.currentSong?.artistId?.let { artistId ->
{
showNowPlaying = false
navController.navigate("artist/$artistId")
}
onArtistClick = { artistId ->
showNowPlaying = false
if (artistId.isNotBlank()) navController.navigate("artist/$artistId")
},
)
}
@@ -33,9 +33,14 @@ 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.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
@@ -59,7 +64,7 @@ fun NowPlayingScreen(
onDismiss: () -> Unit,
onStarToggle: (() -> Unit)? = null,
onShowOptions: (() -> Unit)? = null,
onArtistClick: (() -> Unit)? = null,
onArtistClick: ((artistId: String) -> Unit)? = null,
) {
val song = state.currentSong
var tab by remember { mutableStateOf(NpTab.PLAYER) }
@@ -208,16 +213,45 @@ fun NowPlayingScreen(
maxLines = 1, overflow = TextOverflow.Ellipsis,
)
Spacer(Modifier.height(3.dp))
if (!song?.artist.isNullOrBlank()) {
Text(
song!!.artist!!,
fontFamily = Outfit, fontWeight = FontWeight.SemiBold,
fontSize = 15.sp, color = NsAccent,
maxLines = 1, overflow = TextOverflow.Ellipsis,
modifier = if (onArtistClick != null)
Modifier.clickable(onClick = onArtistClick)
else Modifier,
)
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!!))
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()
}
}
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()) {
Text(