From 0d1118011f7ff2bd52c46ae65eef0d9c37a61c40 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Tue, 20 Oct 2026 07:27:42 +0000 Subject: [PATCH] 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. --- .../com/subtracks/ui/components/ArtistArt.kt | 230 ++++++++++++++++++ .../com/subtracks/ui/home/HomeListScreen.kt | 15 +- .../subtracks/ui/home/HomeListViewModel.kt | 3 + .../com/subtracks/ui/home/HomeScreen.kt | 21 +- .../com/subtracks/ui/home/HomeViewModel.kt | 3 + .../ui/library/ArtistDetailScreen.kt | 199 ++------------- .../subtracks/ui/library/ArtistsContent.kt | 10 +- .../com/subtracks/ui/library/LibraryScreen.kt | 3 + .../subtracks/ui/library/LibraryViewModel.kt | 3 + .../com/subtracks/ui/search/SearchScreen.kt | 6 +- .../subtracks/ui/search/SearchViewModel.kt | 3 + .../com/subtracks/ui/AppScreenshotTest.kt | 7 + 12 files changed, 309 insertions(+), 194 deletions(-) create mode 100644 app/src/main/kotlin/com/subtracks/ui/components/ArtistArt.kt diff --git a/app/src/main/kotlin/com/subtracks/ui/components/ArtistArt.kt b/app/src/main/kotlin/com/subtracks/ui/components/ArtistArt.kt new file mode 100644 index 00000000..fea0e6ba --- /dev/null +++ b/app/src/main/kotlin/com/subtracks/ui/components/ArtistArt.kt @@ -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 { + 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, + modifier: Modifier = Modifier, +) { + val covers by produceState(initialValue = emptyList(), 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, + 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) + } +} diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt index 78353645..91dff9b0 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt @@ -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 = emptyList(), decades: List = emptyList(), coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null }, + artistCovers: suspend (String) -> List = { 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, coverArt: (String?, Boolean) -> CoverArtRef?, + artistCovers: suspend (String) -> List, 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 = { 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)) diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt index 8f4541a9..6511e5cc 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt @@ -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 = loadArtistArtCovers(libraryRepository, sourceRepository, artistId) + fun play(song: Song) { val genre = request.genre when { diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt index 3ac2f22c..3182c4b8 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt @@ -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 = { 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 = { 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, coverArt: (String?, Boolean) -> CoverArtRef?, + artistCovers: suspend (String) -> List, 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, coverArt: (String?, Boolean) -> CoverArtRef?, + artistCovers: suspend (String) -> List, 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, 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)) diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt index 4b43f886..d2aae31b 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt @@ -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 = loadArtistArtCovers(libraryRepository, sourceRepository, artistId) + fun playStarred(song: Song) { viewModelScope.playStarredList(libraryRepository, playbackController, song) } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt index 97d7e869..e0549155 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt @@ -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, - 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, diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt index c1f221c2..d4ca1c27 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt @@ -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 = emptyMap(), scrollToTopKey: Any? = null, + artistCovers: suspend (String) -> List = { 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), ) }, diff --git a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt index 1429ad13..f6fc0bb4 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt @@ -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 = { emptyList() }, onOpenSearch: () -> Unit = {}, offline: Boolean = false, onExitOffline: () -> Unit = {}, @@ -539,6 +541,7 @@ fun LibraryScreen( onSync = onSync, downloadStatuses = artistDownloads, scrollToTopKey = scrollToTopKey, + artistCovers = artistCovers, ) } diff --git a/app/src/main/kotlin/com/subtracks/ui/library/LibraryViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/library/LibraryViewModel.kt index 56cd504f..701e3541 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/LibraryViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/LibraryViewModel.kt @@ -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 = loadArtistArtCovers(libraryRepository, sourceRepository, artistId) + fun sync() { if (syncManager.status.value !is SyncStatus.Running) syncManager.requestSync() } diff --git a/app/src/main/kotlin/com/subtracks/ui/search/SearchScreen.kt b/app/src/main/kotlin/com/subtracks/ui/search/SearchScreen.kt index 75f4cef1..f9178683 100644 --- a/app/src/main/kotlin/com/subtracks/ui/search/SearchScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/search/SearchScreen.kt @@ -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 = ALL_FILTERS, onToggleFilter: (SearchFilter) -> Unit = {}, coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null }, + artistCovers: suspend (String) -> List = { 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, header: String, coverArt: (String?, Boolean) -> CoverArtRef?, + artistCovers: suspend (String) -> List, 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))) }, diff --git a/app/src/main/kotlin/com/subtracks/ui/search/SearchViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/search/SearchViewModel.kt index ddb91a70..b1a06843 100644 --- a/app/src/main/kotlin/com/subtracks/ui/search/SearchViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/search/SearchViewModel.kt @@ -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 = loadArtistArtCovers(libraryRepository, sourceRepository, artistId) + fun play(song: Song) { viewModelScope.playSongInContext(libraryRepository, playbackController, song) } diff --git a/app/src/test/kotlin/com/subtracks/ui/AppScreenshotTest.kt b/app/src/test/kotlin/com/subtracks/ui/AppScreenshotTest.kt index a3329b42..6a5b8612 100644 --- a/app/src/test/kotlin/com/subtracks/ui/AppScreenshotTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/AppScreenshotTest.kt @@ -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 = {},