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