feat(android): Search artists/albums as horizontal carousels

Audit v2 #14. Artists + Albums search sections were rendering as
plain vertical TextRows (name-only for artists, title+artist for
albums). Now match Flutter:

* Artists: horizontal LazyRow of ArtistCard (cover + name)
* Albums: horizontal LazyRow of AlbumCard (cover + title + artist)
* Section headers gain a count suffix ("Artists 12") matching
  Flutter's _SectionHeader pattern; also applied to the Tracks header
  for consistency.

Removed the now-unused TextRow helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 12:47:05 -04:00
parent a42a4df736
commit ec585997ae
@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.CircularProgressIndicator
@@ -50,6 +51,8 @@ import androidx.navigation.NavHostController
import com.composables.icons.lucide.ArrowLeft
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.X
import com.fabledsword.minstrel.library.widgets.AlbumCard
import com.fabledsword.minstrel.library.widgets.ArtistCard
import com.fabledsword.minstrel.models.AlbumRef
import com.fabledsword.minstrel.models.ArtistRef
import com.fabledsword.minstrel.models.SearchResponseRef
@@ -208,14 +211,16 @@ private fun LazyListScope.artistsSection(
onArtistClick: (String) -> Unit,
) {
if (artists.isEmpty()) return
item { SectionHeader("Artists") }
items(items = artists, key = { "art-${it.id}" }) { artist ->
TextRow(
primary = artist.name,
secondary = null,
onClick = { onArtistClick(artist.id) },
)
HorizontalDivider()
item { SectionHeader("Artists", artists.size) }
item {
LazyRow(
contentPadding = PaddingValues(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(items = artists, key = { "art-${it.id}" }) { artist ->
ArtistCard(artist = artist, onClick = { onArtistClick(artist.id) })
}
}
}
}
@@ -224,14 +229,16 @@ private fun LazyListScope.albumsSection(
onAlbumClick: (String) -> Unit,
) {
if (albums.isEmpty()) return
item { SectionHeader("Albums") }
items(items = albums, key = { "alb-${it.id}" }) { album ->
TextRow(
primary = album.title,
secondary = album.artistName.takeIf { it.isNotEmpty() },
onClick = { onAlbumClick(album.id) },
)
HorizontalDivider()
item { SectionHeader("Albums", albums.size) }
item {
LazyRow(
contentPadding = PaddingValues(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(items = albums, key = { "alb-${it.id}" }) { album ->
AlbumCard(album = album, onClick = { onAlbumClick(album.id) })
}
}
}
}
@@ -243,7 +250,7 @@ private fun LazyListScope.tracksSection(
onNavigateToArtist: (String) -> Unit,
) {
if (tracks.isEmpty()) return
item { SectionHeader("Tracks") }
item { SectionHeader("Tracks", tracks.size) }
items(items = tracks, key = { "trk-${it.id}" }) { track ->
TrackResultRow(
track = track,
@@ -307,45 +314,24 @@ private fun TrackResultRow(
}
@Composable
private fun SectionHeader(text: String) {
Text(
text = text,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
@Composable
private fun TextRow(primary: String, secondary: String?, onClick: () -> Unit) {
private fun SectionHeader(label: String, count: Int) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = primary,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (!secondary.isNullOrEmpty()) {
Text(
text = secondary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
Text(
text = label,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground,
)
Text(
text = "$count",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}