rework the artist detail header

- drop the passive star badge from the artists list
- add an album count, star, more and placeholder bio under the artist image
- move the more action down from the top bar into that row
- put a shuffle button beside the artist name, which can now wrap to two lines
- remove the now-unused StarredBadge component
This commit is contained in:
austinried
2026-10-04 00:30:02 +00:00
parent 9cb93d37e1
commit 581c5e20a3
3 changed files with 111 additions and 68 deletions
@@ -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() }
@@ -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(),
)
}
}
@@ -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(