ui: use the album grid for artist icons too

Load up to four thumbnail covers per visible artist row on demand and render the same 1-4 layout inside the circular artist icon. Lists stay on the expensive per-row query for now.
This commit is contained in:
austinried
2026-10-20 07:27:42 +00:00
parent 6e60ff6183
commit 0d1118011f
12 changed files with 309 additions and 194 deletions
@@ -0,0 +1,230 @@
package com.subtracks.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import com.subtracks.data.model.Artist
import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.repo.LibraryRepository
import com.subtracks.data.repo.SourceRepository
import kotlinx.coroutines.flow.first
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
internal suspend fun loadArtistArtCovers(
library: LibraryRepository,
source: SourceRepository,
artistId: String,
): List<CoverArtRef> {
val sourceId = library.activeSourceId.first() ?: return emptyList()
val own = library.artistOwnCoverArt(sourceId, artistId)
val ids =
if (own != null) {
listOf(own)
} else {
library.artistFallbackCoverArts(sourceId, artistId)
}
return ids.take(4).mapNotNull { source.coverArt(it, thumbnail = true) }
}
@Composable
fun ArtistIcon(
artist: Artist,
fallback: CoverArtRef?,
loadCovers: suspend (String) -> List<CoverArtRef>,
modifier: Modifier = Modifier,
) {
val covers by produceState(initialValue = emptyList<CoverArtRef>(), artist.id) {
value = loadCovers(artist.id)
}
if (covers.size >= 2) {
ArtistArtGrid(covers = covers, name = artist.name, modifier = modifier)
} else {
CoverArt(
ref = covers.firstOrNull() ?: fallback,
name = artist.name,
showPlaceholder = covers.isEmpty() && fallback == null,
modifier = modifier,
)
}
}
@Composable
fun ArtistArtGrid(
covers: List<CoverArtRef>,
name: String,
modifier: Modifier = Modifier,
singleThumbnail: CoverArtRef? = null,
) {
when {
covers.size >= 4 -> {
Column(modifier) {
Row(Modifier.weight(1f).fillMaxWidth()) {
covers.take(2).forEach { ref ->
CoverArt(ref = ref, name = name, modifier = Modifier.weight(1f).fillMaxHeight())
}
}
Row(Modifier.weight(1f).fillMaxWidth()) {
covers.drop(2).take(2).forEach { ref ->
CoverArt(ref = ref, name = name, modifier = Modifier.weight(1f).fillMaxHeight())
}
}
}
}
covers.size == 3 -> {
Box(modifier) {
covers.forEachIndexed { index, ref ->
CoverArt(
ref = ref,
name = name,
modifier = Modifier.matchParentSize().clip(YRegionShape(index)),
)
}
}
}
covers.size == 2 -> {
Row(modifier) {
covers.forEach { ref ->
CoverArt(ref = ref, name = name, modifier = Modifier.weight(1f).fillMaxHeight())
}
}
}
covers.size == 1 -> {
CoverArt(
ref = covers.first(),
name = name,
thumbnailRef = singleThumbnail,
showPlaceholder = false,
modifier = modifier,
)
}
}
}
private class YRegionShape(
private val region: Int,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
): Outline {
val center = Offset(size.width / 2f, size.height / 2f)
val left = rayPoint(size, 210.0)
val right = rayPoint(size, 330.0)
val bottom = Offset(size.width / 2f, size.height)
val path = Path()
when (region) {
0 -> {
path.moveTo(center.x, center.y)
path.lineTo(left.x, left.y)
addClockwise(path, size, left, right)
}
1 -> {
path.moveTo(center.x, center.y)
path.lineTo(bottom.x, bottom.y)
addClockwise(path, size, bottom, left)
}
else -> {
path.moveTo(center.x, center.y)
path.lineTo(right.x, right.y)
addClockwise(path, size, right, bottom)
}
}
path.lineTo(center.x, center.y)
path.close()
return Outline.Generic(path)
}
}
private fun rayPoint(
size: Size,
angleDegrees: Double,
): Offset {
val cx = size.width / 2.0
val cy = size.height / 2.0
val radians = angleDegrees * PI / 180.0
val dx = cos(radians)
val dy = sin(radians)
val tx =
when {
dx > 0 -> (size.width - cx) / dx
dx < 0 -> -cx / dx
else -> Double.POSITIVE_INFINITY
}
val ty =
when {
dy > 0 -> (size.height - cy) / dy
dy < 0 -> -cy / dy
else -> Double.POSITIVE_INFINITY
}
val t = min(tx, ty)
return Offset((cx + dx * t).toFloat(), (cy + dy * t).toFloat())
}
private fun addClockwise(
path: Path,
size: Size,
from: Offset,
to: Offset,
) {
val w = size.width
val h = size.height
val corners =
listOf(
Offset(0f, 0f),
Offset(w, 0f),
Offset(w, h),
Offset(0f, h),
)
val fromParam = perimeterParam(from, size)
val toParam = perimeterParam(to, size)
if (fromParam <= toParam) {
corners
.filter { perimeterParam(it, size) > fromParam && perimeterParam(it, size) < toParam }
.forEach { path.lineTo(it.x, it.y) }
} else {
corners
.filter { perimeterParam(it, size) > fromParam }
.forEach { path.lineTo(it.x, it.y) }
corners
.filter { perimeterParam(it, size) < toParam }
.forEach { path.lineTo(it.x, it.y) }
}
path.lineTo(to.x, to.y)
}
private fun perimeterParam(
point: Offset,
size: Size,
): Float {
val w = size.width
val h = size.height
return when {
point.y <= 0f -> point.x
point.x >= w -> w + point.y
point.y >= h -> w + h + (w - point.x)
else -> 2f * w + h + (h - point.y)
}
}
@@ -57,6 +57,7 @@ import com.subtracks.data.model.Playlist
import com.subtracks.data.model.Song
import com.subtracks.data.source.StarType
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.ArtistIcon
import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.ItemActions
@@ -108,6 +109,7 @@ fun HomeListRoute(
genres = genres,
decades = decades,
coverArt = viewModel::coverArt,
artistCovers = viewModel::artistArtCovers,
playingSongId = playback.item?.id,
onBack = onBack,
onAlbumClick = onAlbumClick,
@@ -133,6 +135,7 @@ fun HomeListScreen(
genres: List<String> = emptyList(),
decades: List<Long> = emptyList(),
coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null },
artistCovers: suspend (String) -> List<CoverArtRef> = { emptyList() },
playingSongId: String? = null,
onBack: () -> Unit = {},
onAlbumClick: (Album) -> Unit = {},
@@ -192,6 +195,7 @@ fun HomeListScreen(
ArtistsList(
artists = artists,
coverArt = coverArt,
artistCovers = artistCovers,
info = { artistListInfo(request, it, now, resources) },
onArtistClick = onArtistClick,
onLongClick = onLongClick,
@@ -246,6 +250,7 @@ fun HomeListScreen(
ArtistsList(
artists = artists,
coverArt = coverArt,
artistCovers = artistCovers,
info = { artistListInfo(request, it, now, resources) },
onArtistClick = onArtistClick,
onLongClick = onLongClick,
@@ -444,6 +449,7 @@ internal fun AlbumListRow(
private fun ArtistsList(
artists: LazyPagingItems<Artist>,
coverArt: (String?, Boolean) -> CoverArtRef?,
artistCovers: suspend (String) -> List<CoverArtRef>,
info: (Artist) -> String?,
onArtistClick: (Artist) -> Unit,
onLongClick: (MenuTarget) -> Unit,
@@ -463,6 +469,7 @@ private fun ArtistsList(
ArtistListRow(
artist = artist,
coverArt = coverArt,
artistCovers = artistCovers,
info = info(artist),
onClick = { onArtistClick(artist) },
onLongClick = { onLongClick(MenuTarget.Artist(artist, coverArt(artist.coverArt, true))) },
@@ -479,6 +486,7 @@ internal fun ArtistListRow(
info: String?,
onClick: () -> Unit,
onLongClick: () -> Unit,
artistCovers: suspend (String) -> List<CoverArtRef> = { emptyList() },
) {
Row(
modifier =
@@ -488,9 +496,10 @@ internal fun ArtistListRow(
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
CoverArt(
ref = coverArt(artist.coverArt, true),
name = artist.name,
ArtistIcon(
artist = artist,
fallback = coverArt(artist.coverArt, true),
loadCovers = artistCovers,
modifier = Modifier.size(ALBUM_ROW_COVER).clip(CircleShape),
)
Spacer(Modifier.width(12.dp))
@@ -16,6 +16,7 @@ import com.subtracks.data.prefs.PlaylistSort
import com.subtracks.data.repo.LibraryRepository
import com.subtracks.data.repo.SourceRepository
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.loadArtistArtCovers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@@ -156,6 +157,8 @@ class HomeListViewModel(
thumbnail: Boolean,
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
suspend fun artistArtCovers(artistId: String): List<CoverArtRef> = loadArtistArtCovers(libraryRepository, sourceRepository, artistId)
fun play(song: Song) {
val genre = request.genre
when {
@@ -72,6 +72,7 @@ import com.subtracks.data.model.Playlist
import com.subtracks.data.model.Song
import com.subtracks.data.source.StarType
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.ArtistIcon
import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.EmptyState
@@ -137,6 +138,7 @@ fun HomeRoute(
playlists = viewModel.downloadedPlaylists.collectAsLazyPagingItems(),
songs = viewModel.downloadedSongs.collectAsStateWithLifecycle(initialValue = emptyList()).value,
coverArt = viewModel::coverArt,
artistCovers = viewModel::artistArtCovers,
playingSongId = playback.item?.id,
topInset = topInset,
bottomInset = bottomInset,
@@ -152,6 +154,7 @@ fun HomeRoute(
HomeScreen(
feed = feed,
coverArt = viewModel::coverArt,
artistCovers = viewModel::artistArtCovers,
playingSongId = playback.item?.id,
topInset = topInset,
bottomInset = bottomInset,
@@ -173,6 +176,7 @@ fun HomeScreen(
coverArt: (String?, Boolean) -> CoverArtRef?,
playingSongId: String?,
modifier: Modifier = Modifier,
artistCovers: suspend (String) -> List<CoverArtRef> = { emptyList() },
topInset: Dp = 0.dp,
bottomInset: Dp = 0.dp,
onAlbumClick: (Album) -> Unit = {},
@@ -215,6 +219,7 @@ fun HomeScreen(
HomeSection.RecentlyPlayedArtists,
feed.recentlyPlayedArtists,
coverArt,
artistCovers,
onArtistClick,
onLongClick,
onMore,
@@ -249,6 +254,7 @@ fun HomeScreen(
HomeSection.MostPlayedArtists,
feed.mostPlayedArtists,
coverArt,
artistCovers,
onArtistClick,
onLongClick,
onMore,
@@ -296,6 +302,7 @@ fun OfflineHomeScreen(
coverArt: (String?, Boolean) -> CoverArtRef?,
playingSongId: String?,
modifier: Modifier = Modifier,
artistCovers: suspend (String) -> List<CoverArtRef> = { emptyList() },
topInset: Dp = 0.dp,
bottomInset: Dp = 0.dp,
onAlbumClick: (Album) -> Unit = {},
@@ -312,7 +319,7 @@ fun OfflineHomeScreen(
) {
homeTitle()
downloadedAlbumRow(albums, coverArt, onAlbumClick, onLongClick) { onMore(OfflineListKind.Albums) }
downloadedArtistRow(artists, coverArt, onArtistClick, onLongClick) { onMore(OfflineListKind.Artists) }
downloadedArtistRow(artists, coverArt, artistCovers, onArtistClick, onLongClick) { onMore(OfflineListKind.Artists) }
downloadedPlaylistRow(playlists, coverArt, onPlaylistClick, onLongClick) { onMore(OfflineListKind.Playlists) }
songList(
key = "downloaded-songs",
@@ -365,6 +372,7 @@ private fun LazyListScope.downloadedAlbumRow(
private fun LazyListScope.downloadedArtistRow(
artists: LazyPagingItems<Artist>,
coverArt: (String?, Boolean) -> CoverArtRef?,
artistCovers: suspend (String) -> List<CoverArtRef>,
onArtistClick: (Artist) -> Unit,
onLongClick: (MenuTarget) -> Unit,
onMore: () -> Unit,
@@ -386,6 +394,7 @@ private fun LazyListScope.downloadedArtistRow(
ArtistTile(
artist = artist,
ref = ref,
artistCovers = artistCovers,
onClick = { onArtistClick(artist) },
onLongClick = { onLongClick(MenuTarget.Artist(artist, ref)) },
)
@@ -607,6 +616,7 @@ private fun LazyListScope.artistRow(
section: HomeSection,
artists: List<Artist>,
coverArt: (String?, Boolean) -> CoverArtRef?,
artistCovers: suspend (String) -> List<CoverArtRef>,
onArtistClick: (Artist) -> Unit,
onLongClick: (MenuTarget) -> Unit,
onMore: (HomeSection) -> Unit,
@@ -629,6 +639,7 @@ private fun LazyListScope.artistRow(
ArtistTile(
artist = artist,
ref = ref,
artistCovers = artistCovers,
onClick = { onArtistClick(artist) },
onLongClick = { onLongClick(MenuTarget.Artist(artist, ref)) },
)
@@ -780,6 +791,7 @@ private fun AlbumTile(
private fun ArtistTile(
artist: Artist,
ref: CoverArtRef?,
artistCovers: suspend (String) -> List<CoverArtRef>,
onClick: () -> Unit,
onLongClick: () -> Unit,
) {
@@ -787,9 +799,10 @@ private fun ArtistTile(
modifier = Modifier.width(ARTIST_TILE_SIZE).combinedClickable(onClick = onClick, onLongClick = onLongClick),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CoverArt(
ref = ref,
name = artist.name,
ArtistIcon(
artist = artist,
fallback = ref,
loadCovers = artistCovers,
modifier = Modifier.size(ARTIST_TILE_SIZE).clip(CircleShape),
)
Spacer(Modifier.height(6.dp))
@@ -17,6 +17,7 @@ import com.subtracks.data.repo.rediscoverCutoff
import com.subtracks.data.sync.SyncManager
import com.subtracks.data.sync.SyncStatus
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.loadArtistArtCovers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -127,6 +128,8 @@ class HomeViewModel(
thumbnail: Boolean,
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
suspend fun artistArtCovers(artistId: String): List<CoverArtRef> = loadArtistArtCovers(libraryRepository, sourceRepository, artistId)
fun playStarred(song: Song) {
viewModelScope.playStarredList(libraryRepository, playbackController, song)
}
@@ -102,6 +102,7 @@ import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.ListDownloadStatus
import com.subtracks.data.source.StarType
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.ArtistArtGrid
import com.subtracks.ui.components.CloseWhenDownloadsGone
import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.CoverArt
@@ -402,13 +403,22 @@ fun ArtistDetailScreen(
.onSizeChanged { imageHeightPx = it.height.toFloat() },
) {
Box(Modifier.fillMaxWidth()) {
ArtistHeroArt(
covers = artCovers,
fallback = art,
fallbackThumbnail = artThumbnail,
name = displayName,
modifier = Modifier.fillMaxWidth().height(ART_HEIGHT),
)
if (artCovers.isNotEmpty()) {
ArtistArtGrid(
covers = artCovers,
name = displayName,
singleThumbnail = artThumbnail,
modifier = Modifier.fillMaxWidth().height(ART_HEIGHT),
)
} else {
CoverArt(
ref = art,
name = displayName,
thumbnailRef = artThumbnail,
showPlaceholder = art == null,
modifier = Modifier.fillMaxWidth().height(ART_HEIGHT),
)
}
if (nameScrimAlpha > 0f) {
Box(
modifier =
@@ -833,181 +843,6 @@ internal fun splitBioCollapsed(
return BioLines(lines, more = true)
}
@Composable
private fun ArtistHeroArt(
covers: List<CoverArtRef>,
fallback: CoverArtRef?,
fallbackThumbnail: CoverArtRef?,
name: String,
modifier: Modifier = Modifier,
) {
when {
covers.size >= 4 -> {
Column(modifier) {
Row(Modifier.weight(1f).fillMaxWidth()) {
covers.take(2).forEach { ref ->
CoverArt(ref = ref, name = name, modifier = Modifier.weight(1f).fillMaxHeight())
}
}
Row(Modifier.weight(1f).fillMaxWidth()) {
covers.drop(2).take(2).forEach { ref ->
CoverArt(ref = ref, name = name, modifier = Modifier.weight(1f).fillMaxHeight())
}
}
}
}
covers.size == 3 -> {
Box(modifier) {
covers.forEachIndexed { index, ref ->
CoverArt(
ref = ref,
name = name,
modifier = Modifier.matchParentSize().clip(YRegionShape(index)),
)
}
}
}
covers.size == 2 -> {
Row(modifier) {
covers.forEach { ref ->
CoverArt(ref = ref, name = name, modifier = Modifier.weight(1f).fillMaxHeight())
}
}
}
covers.size == 1 -> {
CoverArt(
ref = covers.first(),
name = name,
thumbnailRef = fallbackThumbnail,
showPlaceholder = false,
modifier = modifier,
)
}
else -> {
CoverArt(
ref = fallback,
name = name,
thumbnailRef = fallbackThumbnail,
showPlaceholder = fallback == null,
modifier = modifier,
)
}
}
}
private class YRegionShape(
private val region: Int,
) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density,
): Outline {
val center = Offset(size.width / 2f, size.height / 2f)
val left = rayPoint(size, 210.0)
val right = rayPoint(size, 330.0)
val bottom = Offset(size.width / 2f, size.height)
val path = Path()
when (region) {
0 -> {
path.moveTo(center.x, center.y)
path.lineTo(left.x, left.y)
addClockwise(path, size, left, right)
}
1 -> {
path.moveTo(center.x, center.y)
path.lineTo(bottom.x, bottom.y)
addClockwise(path, size, bottom, left)
}
else -> {
path.moveTo(center.x, center.y)
path.lineTo(right.x, right.y)
addClockwise(path, size, right, bottom)
}
}
path.lineTo(center.x, center.y)
path.close()
return Outline.Generic(path)
}
}
private fun rayPoint(
size: Size,
angleDegrees: Double,
): Offset {
val cx = size.width / 2.0
val cy = size.height / 2.0
val radians = angleDegrees * PI / 180.0
val dx = cos(radians)
val dy = sin(radians)
val tx =
when {
dx > 0 -> (size.width - cx) / dx
dx < 0 -> -cx / dx
else -> Double.POSITIVE_INFINITY
}
val ty =
when {
dy > 0 -> (size.height - cy) / dy
dy < 0 -> -cy / dy
else -> Double.POSITIVE_INFINITY
}
val t = min(tx, ty)
return Offset((cx + dx * t).toFloat(), (cy + dy * t).toFloat())
}
private fun addClockwise(
path: Path,
size: Size,
from: Offset,
to: Offset,
) {
val w = size.width
val h = size.height
val corners =
listOf(
Offset(0f, 0f),
Offset(w, 0f),
Offset(w, h),
Offset(0f, h),
)
val fromParam = perimeterParam(from, size)
val toParam = perimeterParam(to, size)
if (fromParam <= toParam) {
corners
.filter { perimeterParam(it, size) > fromParam && perimeterParam(it, size) < toParam }
.forEach { path.lineTo(it.x, it.y) }
} else {
corners
.filter { perimeterParam(it, size) > fromParam }
.forEach { path.lineTo(it.x, it.y) }
corners
.filter { perimeterParam(it, size) < toParam }
.forEach { path.lineTo(it.x, it.y) }
}
path.lineTo(to.x, to.y)
}
private fun perimeterParam(
point: Offset,
size: Size,
): Float {
val w = size.width
val h = size.height
return when {
point.y <= 0f -> point.x
point.x >= w -> w + point.y
point.y >= h -> w + h + (w - point.x)
else -> 2f * w + h + (h - point.y)
}
}
@Composable
private fun ArtistSectionHeader(
text: String,
@@ -29,7 +29,7 @@ import com.subtracks.R
import com.subtracks.data.model.Artist
import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.ListDownloadStatus
import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.ArtistIcon
import com.subtracks.ui.components.EmptyState
import com.subtracks.ui.components.FilteredEmptyState
import com.subtracks.ui.components.ListDownloadIndicator
@@ -55,6 +55,7 @@ fun ArtistsContent(
onSync: () -> Unit = {},
downloadStatuses: Map<String, ListDownloadStatus> = emptyMap(),
scrollToTopKey: Any? = null,
artistCovers: suspend (String) -> List<CoverArtRef> = { emptyList() },
) {
when {
items.itemCount == 0 && items.loadState.refresh is LoadState.Loading -> {
@@ -104,9 +105,10 @@ fun ArtistsContent(
)
},
leadingContent = {
CoverArt(
ref = coverArt(artist.coverArt, true),
name = artist.name,
ArtistIcon(
artist = artist,
fallback = coverArt(artist.coverArt, true),
loadCovers = artistCovers,
modifier = Modifier.size(48.dp).clip(CircleShape),
)
},
@@ -308,6 +308,7 @@ fun LibraryRoute(
artists = viewModel.artists,
playlists = viewModel.playlists,
coverArt = viewModel::coverArt,
artistCovers = viewModel::artistArtCovers,
onAlbumClick = onAlbumClick,
onArtistClick = onArtistClick,
onPlaylistClick = onPlaylistClick,
@@ -396,6 +397,7 @@ fun LibraryScreen(
onSync: () -> Unit,
onOpenSettings: () -> Unit,
modifier: Modifier = Modifier,
artistCovers: suspend (String) -> List<CoverArtRef> = { emptyList() },
onOpenSearch: () -> Unit = {},
offline: Boolean = false,
onExitOffline: () -> Unit = {},
@@ -539,6 +541,7 @@ fun LibraryScreen(
onSync = onSync,
downloadStatuses = artistDownloads,
scrollToTopKey = scrollToTopKey,
artistCovers = artistCovers,
)
}
@@ -23,6 +23,7 @@ import com.subtracks.data.repo.SourceRepository
import com.subtracks.data.sync.SyncManager
import com.subtracks.data.sync.SyncStatus
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.loadArtistArtCovers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -211,6 +212,8 @@ class LibraryViewModel(
thumbnail: Boolean,
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
suspend fun artistArtCovers(artistId: String): List<CoverArtRef> = loadArtistArtCovers(libraryRepository, sourceRepository, artistId)
fun sync() {
if (syncManager.status.value !is SyncStatus.Running) syncManager.requestSync()
}
@@ -106,6 +106,7 @@ fun SearchRoute(
if (next != 0) filterMask = next
},
coverArt = viewModel::coverArt,
artistCovers = viewModel::artistArtCovers,
playingSongId = playback.item?.id,
onBack = onBack,
onAlbumClick = {
@@ -146,6 +147,7 @@ fun SearchScreen(
filters: Set<SearchFilter> = ALL_FILTERS,
onToggleFilter: (SearchFilter) -> Unit = {},
coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null },
artistCovers: suspend (String) -> List<CoverArtRef> = { emptyList() },
playingSongId: String? = null,
onBack: () -> Unit = {},
onAlbumClick: (Album) -> Unit = {},
@@ -215,7 +217,7 @@ fun SearchScreen(
contentPadding = PaddingValues(bottom = 24.dp),
) {
if (showPlaylists) playlistResults(results.playlists, playlistsLabel, coverArt, onPlaylistClick, onLongClick)
if (showArtists) artistResults(results.artists, artistsLabel, coverArt, onArtistClick, onLongClick)
if (showArtists) artistResults(results.artists, artistsLabel, coverArt, artistCovers, onArtistClick, onLongClick)
if (showAlbums) albumResults(results.albums, albumsLabel, coverArt, onAlbumClick, onLongClick)
if (showSongs) songResults(results.songs, songsLabel, coverArt, playingSongId, onSongClick, onLongClick)
}
@@ -309,6 +311,7 @@ private fun LazyListScope.artistResults(
artists: List<Artist>,
header: String,
coverArt: (String?, Boolean) -> CoverArtRef?,
artistCovers: suspend (String) -> List<CoverArtRef>,
onClick: (Artist) -> Unit,
onLongClick: (MenuTarget) -> Unit,
) {
@@ -318,6 +321,7 @@ private fun LazyListScope.artistResults(
ArtistListRow(
artist = artist,
coverArt = coverArt,
artistCovers = artistCovers,
info = null,
onClick = { onClick(artist) },
onLongClick = { onLongClick(MenuTarget.Artist(artist, coverArt(artist.coverArt, true))) },
@@ -12,6 +12,7 @@ import com.subtracks.data.model.Song
import com.subtracks.data.repo.LibraryRepository
import com.subtracks.data.repo.SourceRepository
import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.loadArtistArtCovers
import com.subtracks.ui.home.playSongInContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -74,6 +75,8 @@ class SearchViewModel(
thumbnail: Boolean,
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
suspend fun artistArtCovers(artistId: String): List<CoverArtRef> = loadArtistArtCovers(libraryRepository, sourceRepository, artistId)
fun play(song: Song) {
viewModelScope.playSongInContext(libraryRepository, playbackController, song)
}
@@ -613,6 +613,13 @@ class AppScreenshotTest {
artists = remember { flowOf(PagingData.from(Fixtures.artists)) },
playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) },
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
artistCovers = { id ->
Fixtures.albums
.filter { it.artistId == id }
.take(4)
.mapNotNull { it.coverArt }
.map { CoverArtRef(it, "test:$it") }
},
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},