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 ─────────────────────────────────────────────────────────────
|
// ── Domain models ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SongArtist(
|
||||||
|
val id: String = "",
|
||||||
|
val name: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Song(
|
data class Song(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -48,6 +54,7 @@ data class Song(
|
|||||||
val albumId: String? = null,
|
val albumId: String? = null,
|
||||||
val artist: String? = null,
|
val artist: String? = null,
|
||||||
val artistId: String? = null,
|
val artistId: String? = null,
|
||||||
|
val artists: List<SongArtist> = emptyList(),
|
||||||
val track: Int? = null,
|
val track: Int? = null,
|
||||||
val year: Int? = null,
|
val year: Int? = null,
|
||||||
val genre: String? = null,
|
val genre: String? = null,
|
||||||
|
|||||||
@@ -324,11 +324,9 @@ fun AppNavigation() {
|
|||||||
onShowOptions = {
|
onShowOptions = {
|
||||||
playerState.currentSong?.let { s -> contextMenuSong = s }
|
playerState.currentSong?.let { s -> contextMenuSong = s }
|
||||||
},
|
},
|
||||||
onArtistClick = playerState.currentSong?.artistId?.let { artistId ->
|
onArtistClick = { artistId ->
|
||||||
{
|
|
||||||
showNowPlaying = false
|
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.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.ui.text.SpanStyle
|
||||||
|
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
|
||||||
@@ -59,7 +64,7 @@ fun NowPlayingScreen(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onStarToggle: (() -> Unit)? = null,
|
onStarToggle: (() -> Unit)? = null,
|
||||||
onShowOptions: (() -> Unit)? = null,
|
onShowOptions: (() -> Unit)? = null,
|
||||||
onArtistClick: (() -> Unit)? = null,
|
onArtistClick: ((artistId: String) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val song = state.currentSong
|
val song = state.currentSong
|
||||||
var tab by remember { mutableStateOf(NpTab.PLAYER) }
|
var tab by remember { mutableStateOf(NpTab.PLAYER) }
|
||||||
@@ -208,17 +213,46 @@ fun NowPlayingScreen(
|
|||||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(3.dp))
|
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(
|
Text(
|
||||||
song!!.artist!!,
|
song.artist!!,
|
||||||
fontFamily = Outfit, fontWeight = FontWeight.SemiBold,
|
fontFamily = Outfit, fontWeight = FontWeight.SemiBold,
|
||||||
fontSize = 15.sp, color = NsAccent,
|
fontSize = 15.sp, color = NsAccent,
|
||||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
maxLines = 2, overflow = TextOverflow.Ellipsis,
|
||||||
modifier = if (onArtistClick != null)
|
|
||||||
Modifier.clickable(onClick = onArtistClick)
|
|
||||||
else Modifier,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!song?.album.isNullOrBlank()) {
|
if (!song?.album.isNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
song!!.album!!,
|
song!!.album!!,
|
||||||
|
|||||||
Reference in new issue
Block a user