From f15c005a3c391c2d383f56cf019af64b1dd36fbd Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 15 Jun 2026 00:37:08 +0200 Subject: [PATCH] feat: multi-artist support in NowPlaying (OpenSubsonic artists array) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Song model: add SongArtist{id,name} + artists: List 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 --- .../com/novasonic/app/api/model/Models.kt | 7 +++ .../com/novasonic/app/ui/AppNavigation.kt | 8 +-- .../app/ui/screen/NowPlayingScreen.kt | 56 +++++++++++++++---- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/api/model/Models.kt b/android/app/src/main/kotlin/com/novasonic/app/api/model/Models.kt index 5e0d628..c193690 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/api/model/Models.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/api/model/Models.kt @@ -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 = emptyList(), val track: Int? = null, val year: Int? = null, val genre: String? = null, diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt index 72f8a2a..cdf66f8 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt @@ -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") }, ) } 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 4fa412b..57079f6 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,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(