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:
1 parent
0e651e8be3
commit
f15c005a3c
3 files changed
+50
-11
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 ->
|
||||
{
|
||||
onArtistClick = { artistId ->
|
||||
showNowPlaying = false
|
||||
navController.navigate("artist/$artistId")
|
||||
}
|
||||
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,17 +213,46 @@ fun NowPlayingScreen(
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(Modifier.height(3.dp))
|
||||
if (!song?.artist.isNullOrBlank()) {
|
||||
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!!,
|
||||
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,
|
||||
maxLines = 2, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!song?.album.isNullOrBlank()) {
|
||||
Text(
|
||||
song!!.album!!,
|
||||
|
||||
Reference in new issue
Block a user