feat(web+android): Folgen-Herz ins Künstler-Banner, Favoriten zeigen Künstler und Alben
Das Herz sitzt jetzt im Banner neben dem Pin-Zahnrad statt in der Aktionsleiste: die Leiste ist "was ich jetzt tue" (abspielen, mischen, Konzerte), Folgen ist ein Zustand und gehört an den Künstler. Auf beiden Plattformen gleich. Beim Album bleibt das Herz in der Aktionsleiste, dort gibt es kein Banner. Dazu die Übersicht, wem man folgt: die Favoritenseite bekommt Abschnitte Künstler / Alben / Songs. Ohne zusätzlichen Aufruf — getStarred2 liefert Künstler und Alben im selben Paket wie die Songs, wir haben sie bisher verworfen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f043beb5d
commit
19f241e493
8 files changed
+190
-54
No files matched your search
@@ -415,6 +415,8 @@ fun AppNavigation() {
|
||||
onPlay = { songs, idx -> player.playSongs(songs, idx) },
|
||||
onStarToggle = { song -> starToggle(song.id, song.starred != null) },
|
||||
onShowContextMenu = ::showContextMenu,
|
||||
onArtistClick = { id -> navController.navigate("artist/$id") },
|
||||
onAlbumClick = { id -> navController.navigate("album/$id") },
|
||||
)
|
||||
}
|
||||
composable(TopLevel.ABS.route) {
|
||||
|
||||
@@ -263,6 +263,24 @@ fun ArtistDetailScreen(
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
// Folgen gehoert an den Kuenstler, nicht in die Aktionsleiste darunter: die Leiste
|
||||
// ist "was ich jetzt tue", Folgen ist ein Zustand. Deshalb als Herz ins Banner,
|
||||
// direkt neben das Zahnrad — genau wie im Web.
|
||||
val overrides by StarState.overrides.collectAsStateWithLifecycle()
|
||||
val folgt = overrides[artist.id] ?: (artist.starred != null)
|
||||
Surface(
|
||||
onClick = { vm.folgenUmschalten(artist.id, artist.starred != null) },
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = Color(0x59000000),
|
||||
modifier = Modifier.align(Alignment.TopEnd).padding(top = 10.dp, end = 52.dp),
|
||||
) {
|
||||
Icon(
|
||||
if (folgt) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||
if (folgt) "Du folgst ${artist.name}" else "${artist.name} folgen",
|
||||
tint = if (folgt) NsAccent else Color.White.copy(0.72f),
|
||||
modifier = Modifier.padding(7.dp).size(17.dp),
|
||||
)
|
||||
}
|
||||
// "Wrong artist?" pin — minimal icon-only chip, top-right over the hero.
|
||||
Surface(
|
||||
onClick = { vm.openPinPicker() },
|
||||
@@ -323,18 +341,6 @@ fun ArtistDetailScreen(
|
||||
onClick = { vm.shuffleAll() },
|
||||
)
|
||||
}
|
||||
item {
|
||||
// Folgen = Navidromes Stern auf dem Kuenstler. StarState kennt keine Songs,
|
||||
// sondern IDs — und Navidrome behandelt Song-, Album- und Kuenstler-IDs beim
|
||||
// Markieren gleich, der vorhandene Weg traegt also ohne Aenderung.
|
||||
val overrides by StarState.overrides.collectAsStateWithLifecycle()
|
||||
val folgt = overrides[artist.id] ?: (artist.starred != null)
|
||||
ArtistActionPill(
|
||||
icon = if (folgt) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||
label = if (folgt) "Folge ich" else "Folgen",
|
||||
hervorgehoben = folgt,
|
||||
) { vm.folgenUmschalten(artist.id, artist.starred != null) }
|
||||
}
|
||||
item {
|
||||
val eventCount = state.events.size
|
||||
val konzertLabel = if (eventCount > 0) "Konzerte ($eventCount)" else "Konzerte"
|
||||
@@ -600,26 +606,20 @@ fun ArtistDetailScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtistActionPill(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
hervorgehoben: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val farbe = if (hervorgehoben) NsAccent else NsOnSurface
|
||||
private fun ArtistActionPill(icon: ImageVector, label: String, onClick: () -> Unit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(NsSurface)
|
||||
.border(1.dp, if (hervorgehoben) NsAccent else NsBorder, RoundedCornerShape(20.dp))
|
||||
.border(1.dp, NsBorder, RoundedCornerShape(20.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 18.dp, vertical = 9.dp),
|
||||
) {
|
||||
Icon(icon, null, tint = farbe, modifier = Modifier.size(16.dp))
|
||||
Icon(icon, null, tint = NsOnSurface, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(label, fontFamily = Outfit, fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp, color = farbe)
|
||||
fontSize = 13.sp, color = NsOnSurface)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
package com.novasonic.app.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import coil.compose.AsyncImage
|
||||
import com.novasonic.app.ui.component.AlbumCard
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -28,18 +38,27 @@ fun StarredScreen(
|
||||
onPlay: (List<Song>, Int) -> Unit,
|
||||
onStarToggle: (Song) -> Unit,
|
||||
onShowContextMenu: ((Song) -> Unit)? = null,
|
||||
onArtistClick: (String) -> Unit = {},
|
||||
onAlbumClick: (String) -> Unit = {},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val repo = context.app.repository
|
||||
|
||||
var songs by remember { mutableStateOf<List<Song>>(emptyList()) }
|
||||
// getStarred2 liefert Kuenstler und Alben im SELBEN Paket wie die Songs — die Uebersicht
|
||||
// "wem folge ich" kostet also keinen zusaetzlichen Aufruf.
|
||||
var gefolgteKuenstler by remember { mutableStateOf<List<com.novasonic.app.api.model.Artist>>(emptyList()) }
|
||||
var gemerkteAlben by remember { mutableStateOf<List<com.novasonic.app.api.model.Album>>(emptyList()) }
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
var error by remember { mutableStateOf<String?>(null) }
|
||||
var sort by remember { mutableStateOf(StarredSort.NEWEST) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
songs = repo.getStarred().song
|
||||
val markiert = repo.getStarred()
|
||||
songs = markiert.song
|
||||
gefolgteKuenstler = markiert.artist.sortedBy { it.name.lowercase() }
|
||||
gemerkteAlben = markiert.album.sortedByDescending { it.starred ?: "" }
|
||||
loading = false
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
@@ -100,12 +119,43 @@ fun StarredScreen(
|
||||
Box(Modifier.fillMaxSize().padding(32.dp), contentAlignment = Alignment.Center) {
|
||||
Text(error!!, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
} else if (sorted.isEmpty()) {
|
||||
} else if (sorted.isEmpty() && gefolgteKuenstler.isEmpty() && gemerkteAlben.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize().padding(32.dp), contentAlignment = Alignment.Center) {
|
||||
Text("Noch keine Favoriten", color = NsOnSurface2)
|
||||
}
|
||||
} else {
|
||||
LazyColumn {
|
||||
if (gefolgteKuenstler.isNotEmpty()) {
|
||||
item { FavoritenLabel("Künstler, denen du folgst (${gefolgteKuenstler.size})") }
|
||||
item {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(gefolgteKuenstler, key = { it.id }) { a ->
|
||||
GefolgterKuenstler(a.name, coverUrl(a.coverArt), a.albumCount) { onArtistClick(a.id) }
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
if (gemerkteAlben.isNotEmpty()) {
|
||||
item { FavoritenLabel("Gemerkte Alben (${gemerkteAlben.size})") }
|
||||
item {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
items(gemerkteAlben, key = { it.id }) { al ->
|
||||
AlbumCard(album = al, coverUrl = coverUrl(al.coverArt)) { onAlbumClick(al.id) }
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
if (sorted.isNotEmpty() && (gefolgteKuenstler.isNotEmpty() || gemerkteAlben.isNotEmpty())) {
|
||||
item { FavoritenLabel("Songs (${sorted.size})") }
|
||||
}
|
||||
itemsIndexed(sorted, key = { _, s -> s.id }) { idx, song ->
|
||||
SongRow(
|
||||
song = song,
|
||||
@@ -122,3 +172,30 @@ fun StarredScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FavoritenLabel(titel: String) {
|
||||
Text(
|
||||
titel,
|
||||
fontFamily = Outfit, fontWeight = FontWeight.SemiBold, fontSize = 13.sp,
|
||||
color = NsOnSurface2,
|
||||
modifier = Modifier.padding(start = 20.dp, top = 14.dp, bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GefolgterKuenstler(name: String, bild: String?, albumCount: Int, onClick: () -> Unit) {
|
||||
Column(Modifier.width(110.dp).clickable(onClick = onClick)) {
|
||||
AsyncImage(
|
||||
model = bild, contentDescription = name,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(110.dp).clip(CircleShape).background(NsSurface2),
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(name, fontFamily = Outfit, fontWeight = FontWeight.SemiBold, fontSize = 13.sp,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.onSurface)
|
||||
Text("$albumCount Alben", fontFamily = Outfit, fontSize = 11.sp,
|
||||
color = NsOnSurface2, maxLines = 1)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user