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.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.shape.RoundedCornerShape 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.Button
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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 @Composable
fun Modifier.statusBarScrim(): Modifier { fun Modifier.statusBarScrim(): Modifier {
val top = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } 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.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan 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.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.MoreHoriz 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton 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.Color
import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.subtracks.data.model.Album import com.subtracks.data.model.Album
@@ -140,6 +148,8 @@ fun ArtistDetailRoute(
onAlbumClick = onAlbumClick, onAlbumClick = onAlbumClick,
onAlbumLongClick = { contextMenuHost?.show(it, actions) }, onAlbumLongClick = { contextMenuHost?.show(it, actions) },
onMore = { artist?.let { contextMenuHost?.show(MenuTarget.Artist(it, artThumbnail ?: art), 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, onAlbumClick: (Album) -> Unit,
onAlbumLongClick: (MenuTarget) -> Unit = {}, onAlbumLongClick: (MenuTarget) -> Unit = {},
onMore: () -> Unit = {}, onMore: () -> Unit = {},
starred: Boolean = false,
onToggleStar: (() -> Unit)? = null,
onShuffle: () -> Unit = {},
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val listState = rememberLazyGridState() val listState = rememberLazyGridState()
@@ -230,12 +243,12 @@ fun ArtistDetailScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }) {
Box( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.onSizeChanged { imageHeightPx = it.height.toFloat() } .onSizeChanged { imageHeightPx = it.height.toFloat() },
.padding(bottom = 12.dp),
) { ) {
Box(Modifier.fillMaxWidth()) {
CoverArt( CoverArt(
ref = art, ref = art,
name = artist?.name.orEmpty(), name = artist?.name.orEmpty(),
@@ -258,13 +271,56 @@ fun ArtistDetailScreen(
).testTag(ARTIST_NAME_SCRIM_TAG), ).testTag(ARTIST_NAME_SCRIM_TAG),
) )
} }
Box(Modifier.align(Alignment.BottomStart).fillMaxWidth()) {
Text( Text(
text = artist?.name.orEmpty(), text = artist?.name.orEmpty(),
style = imageNameStyle, style = imageNameStyle,
color = Color.White, color = Color.White,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.align(Alignment.BottomStart).padding(TITLE_INSET), 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 biography coming soon.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
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") 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), colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
modifier = modifier =
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 package com.subtracks.ui.library
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -15,7 +13,6 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color 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.LoadingState
import com.subtracks.ui.components.MenuTarget import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.components.ResetScrollOnChange import com.subtracks.ui.components.ResetScrollOnChange
import com.subtracks.ui.components.StarredBadge
import com.subtracks.ui.components.rememberViewportFill import com.subtracks.ui.components.rememberViewportFill
import com.subtracks.ui.theme.PrefetchArtworkSeeds import com.subtracks.ui.theme.PrefetchArtworkSeeds
@@ -79,18 +75,11 @@ fun ArtistsContent(
PrefetchArtworkSeeds(coverArt(artist.coverArt, true)) PrefetchArtworkSeeds(coverArt(artist.coverArt, true))
ListItem( ListItem(
headlineContent = { headlineContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text( Text(
text = artist.name, text = artist.name,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f, fill = false),
) )
StarredBadge(artist.starred != null)
}
}, },
supportingContent = { supportingContent = {
Text( Text(