diff --git a/app/src/main/kotlin/com/subtracks/ui/components/Components.kt b/app/src/main/kotlin/com/subtracks/ui/components/Components.kt index 95d1f9c4..7147d781 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/Components.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/Components.kt @@ -17,11 +17,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -304,22 +301,6 @@ fun FilteredEmptyState( } } -@Composable -fun StarredBadge( - starred: Boolean, - modifier: Modifier = Modifier, - size: Dp = 14.dp, -) { - if (starred) { - Icon( - imageVector = Icons.Rounded.Star, - contentDescription = "Starred", - tint = MaterialTheme.colorScheme.primary, - modifier = modifier.size(size), - ) - } -} - @Composable fun Modifier.statusBarScrim(): Modifier { val top = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } 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 8524a09c..e13a4f34 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.aspectRatio @@ -18,7 +19,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan @@ -29,6 +32,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.MoreHoriz +import androidx.compose.material.icons.rounded.Shuffle +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.StarBorder import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -53,10 +59,12 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.subtracks.data.model.Album @@ -140,6 +148,8 @@ fun ArtistDetailRoute( onAlbumClick = onAlbumClick, onAlbumLongClick = { contextMenuHost?.show(it, actions) }, onMore = { artist?.let { contextMenuHost?.show(MenuTarget.Artist(it, artThumbnail ?: art), actions) } }, + starred = artist?.starred != null, + onToggleStar = artist?.let { a -> { setStar(StarType.Artist, a.id, a.starred == null) } }, ) } @@ -156,6 +166,9 @@ fun ArtistDetailScreen( onAlbumClick: (Album) -> Unit, onAlbumLongClick: (MenuTarget) -> Unit = {}, onMore: () -> Unit = {}, + starred: Boolean = false, + onToggleStar: (() -> Unit)? = null, + onShuffle: () -> Unit = {}, modifier: Modifier = Modifier, ) { val listState = rememberLazyGridState() @@ -230,41 +243,84 @@ fun ArtistDetailScreen( modifier = Modifier.fillMaxSize(), ) { item(span = { GridItemSpan(maxLineSpan) }) { - Box( + Column( Modifier .fillMaxWidth() - .onSizeChanged { imageHeightPx = it.height.toFloat() } - .padding(bottom = 12.dp), + .onSizeChanged { imageHeightPx = it.height.toFloat() }, ) { - CoverArt( - ref = art, - name = artist?.name.orEmpty(), - thumbnailRef = artThumbnail, - showPlaceholder = art == null, - modifier = Modifier.fillMaxWidth().height(ART_HEIGHT), - ) - if (nameScrimAlpha > 0f) { - Box( - modifier = - Modifier - .align(Alignment.BottomStart) - .fillMaxWidth() - .height(ART_HEIGHT * 0.4f) - .graphicsLayer { alpha = nameScrimAlpha } - .background( - Brush.verticalGradient( - listOf(Color.Transparent, Color.Black.copy(alpha = 0.6f)), - ), - ).testTag(ARTIST_NAME_SCRIM_TAG), + Box(Modifier.fillMaxWidth()) { + CoverArt( + ref = art, + name = artist?.name.orEmpty(), + thumbnailRef = artThumbnail, + showPlaceholder = art == null, + modifier = Modifier.fillMaxWidth().height(ART_HEIGHT), ) + if (nameScrimAlpha > 0f) { + Box( + modifier = + Modifier + .align(Alignment.BottomStart) + .fillMaxWidth() + .height(ART_HEIGHT * 0.4f) + .graphicsLayer { alpha = nameScrimAlpha } + .background( + Brush.verticalGradient( + listOf(Color.Transparent, Color.Black.copy(alpha = 0.6f)), + ), + ).testTag(ARTIST_NAME_SCRIM_TAG), + ) + } + Box(Modifier.align(Alignment.BottomStart).fillMaxWidth()) { + Text( + text = artist?.name.orEmpty(), + style = imageNameStyle, + color = Color.White, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth().padding(TITLE_INSET), + ) + IconButton( + onClick = onShuffle, + modifier = Modifier.align(Alignment.BottomEnd).padding(end = 8.dp, bottom = 8.dp), + ) { + ShadowedIcon(Icons.Rounded.Shuffle, "Shuffle artist", size = 28.dp) + } + } + } + Row( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "${artist?.albumCount ?: 0} ${if (artist?.albumCount == 1L) "album" else "albums"}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f), + ) + IconButton(onClick = { onToggleStar?.invoke() }, enabled = onToggleStar != null) { + Icon( + imageVector = if (starred) Icons.Rounded.Star else Icons.Rounded.StarBorder, + contentDescription = if (starred) "Unstar" else "Star", + tint = + if (starred) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + IconButton(onClick = onMore) { + Icon(Icons.Rounded.MoreHoriz, contentDescription = "More options") + } } Text( - text = artist?.name.orEmpty(), - style = imageNameStyle, - color = Color.White, - maxLines = 1, + text = "Artist biography coming soon.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, overflow = TextOverflow.Ellipsis, - modifier = Modifier.align(Alignment.BottomStart).padding(TITLE_INSET), + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, bottom = 16.dp), ) } } @@ -336,11 +392,6 @@ fun ArtistDetailScreen( Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back") } }, - actions = { - IconButton(onClick = onMore, modifier = Modifier.graphicsLayer { alpha = barFraction }) { - Icon(Icons.Rounded.MoreHoriz, contentDescription = "More options") - } - }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), modifier = Modifier @@ -355,3 +406,25 @@ fun ArtistDetailScreen( } } } + +@Composable +private fun ShadowedIcon( + imageVector: ImageVector, + contentDescription: String, + size: Dp = 24.dp, +) { + Box(Modifier.size(size)) { + Icon( + imageVector = imageVector, + contentDescription = null, + tint = Color.Black.copy(alpha = 0.5f), + modifier = Modifier.matchParentSize().offset(x = 0.5.dp, y = 1.5.dp), + ) + Icon( + imageVector = imageVector, + contentDescription = contentDescription, + tint = Color.White, + modifier = Modifier.matchParentSize(), + ) + } +} 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 87022872..48c37126 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt @@ -1,9 +1,7 @@ package com.subtracks.ui.library import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height @@ -15,7 +13,6 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -33,7 +30,6 @@ import com.subtracks.ui.components.FilteredEmptyState import com.subtracks.ui.components.LoadingState import com.subtracks.ui.components.MenuTarget import com.subtracks.ui.components.ResetScrollOnChange -import com.subtracks.ui.components.StarredBadge import com.subtracks.ui.components.rememberViewportFill import com.subtracks.ui.theme.PrefetchArtworkSeeds @@ -79,18 +75,11 @@ fun ArtistsContent( PrefetchArtworkSeeds(coverArt(artist.coverArt, true)) ListItem( headlineContent = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = artist.name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), - ) - StarredBadge(artist.starred != null) - } + Text( + text = artist.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) }, supportingContent = { Text(