make the now playing art and artist names tap targets

Tapping the cover art on now playing opens its album and tapping the artist name opens
the artist, matching the context menu actions; the album detail subtitle does the same
for its artist. Both are inert when the queue item has no album/artist id.

Artist detail's album cells also stop drawing the letter placeholder underneath: with a
thumbnail underlay and the full image on top, the letter only showed because the default
`showPlaceholder = true` was left on, so a cold cell flashed a letter before the art.
They now match the hero's `showPlaceholder = art == null`.
This commit is contained in:
austinried
2026-10-04 00:30:00 +00:00
parent edd6d850dd
commit b55e5cc5e8
5 changed files with 138 additions and 7 deletions
@@ -201,6 +201,7 @@ fun HeroHeader(
controlsModifier: Modifier = Modifier,
thumbnailRef: CoverArtRef? = null,
comment: String? = null,
onSubtitleClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
Column(
@@ -235,6 +236,7 @@ fun HeroHeader(
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = if (onSubtitleClick != null) Modifier.clickable { onSubtitleClick() } else Modifier,
)
}
if (!comment.isNullOrBlank()) {
@@ -80,6 +80,11 @@ fun AlbumDetailRoute(
onShuffle = viewModel::shuffle,
onPlay = viewModel::playAll,
onMore = { album?.let { contextMenuHost?.show(MenuTarget.Album(it, viewModel.coverArt(it.coverArt, true)), actions) } },
onArtistClick =
album
?.takeIf { !it.albumArtist.isNullOrBlank() }
?.let { it.artistId }
?.let { id -> { onViewArtist(id) } },
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId },
)
}
@@ -98,6 +103,7 @@ fun AlbumDetailScreen(
onPlay: () -> Unit = { onSongClick(0) },
onDownload: () -> Unit = {},
onMore: () -> Unit = {},
onArtistClick: (() -> Unit)? = null,
playingSongId: String? = null,
modifier: Modifier = Modifier,
) {
@@ -124,15 +130,17 @@ fun AlbumDetailScreen(
title = album?.name.orEmpty(),
onBack = onBack,
header = { controlsModifier, topInset ->
val artistName = album?.albumArtist?.takeIf { it.isNotBlank() }
HeroHeader(
art = coverArt(album?.coverArt, false),
thumbnailRef = coverArt(album?.coverArt, true),
name = album?.name.orEmpty(),
subtitle =
listOfNotNull(
album?.albumArtist?.takeIf { it.isNotBlank() },
artistName,
album?.year?.takeIf { it > 0 }?.toString(),
).joinToString(" $DOT "),
onSubtitleClick = onArtistClick,
hasSongs = songs.isNotEmpty(),
onPlay = onPlay,
onShuffle = onShuffle,
@@ -270,7 +270,9 @@ fun ArtistDetailScreen(
}
}
itemsIndexed(albums, key = { _, album -> album.id }) { index, album ->
PrefetchArtworkSeeds(coverArt(album.coverArt, true))
val art = coverArt(album.coverArt, false)
val thumbnail = coverArt(album.coverArt, true)
PrefetchArtworkSeeds(thumbnail)
Column(
modifier =
Modifier
@@ -279,14 +281,15 @@ fun ArtistDetailScreen(
end = if (index % 2 == 1) 16.dp else 0.dp,
).combinedClickable(
onClick = { onAlbumClick(album) },
onLongClick = { onAlbumLongClick(MenuTarget.Album(album, coverArt(album.coverArt, true))) },
onLongClick = { onAlbumLongClick(MenuTarget.Album(album, thumbnail)) },
).onSizeChanged { albumHeightPx = it.height.toFloat() },
) {
Box {
CoverArt(
ref = coverArt(album.coverArt, false),
ref = art,
name = album.name,
thumbnailRef = coverArt(album.coverArt, true),
thumbnailRef = thumbnail,
showPlaceholder = art == null,
modifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(2.dp)),
)
StarredBadge(
@@ -3,6 +3,7 @@ package com.subtracks.ui.playback
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -50,6 +51,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
@@ -80,6 +82,8 @@ import com.subtracks.ui.theme.HeroGradient
import com.subtracks.ui.theme.rememberArtworkColors
import org.koin.compose.koinInject
const val NOW_PLAYING_COVER_TAG = "now-playing-cover"
@Composable
fun NowPlayingRoute(
onBack: () -> Unit,
@@ -139,6 +143,8 @@ fun NowPlayingRoute(
onShuffle = controller::toggleShuffle,
onRepeat = controller::cycleRepeat,
onMore = { song?.let { contextMenuHost?.show(MenuTarget.Song(it, thumbnail), actions) } },
onAlbumClick = song?.albumId?.let { id -> { onViewAlbum(id) } },
onArtistClick = song?.artistId?.let { id -> { onViewArtist(id) } },
onSeek = controller::seekTo,
modifier = modifier,
)
@@ -182,6 +188,8 @@ fun NowPlayingScreen(
onShuffle: () -> Unit = {},
onRepeat: () -> Unit = {},
onMore: () -> Unit = {},
onAlbumClick: (() -> Unit)? = null,
onArtistClick: (() -> Unit)? = null,
onSeek: (Long) -> Unit,
thumbnailRef: CoverArtRef? = null,
modifier: Modifier = Modifier,
@@ -247,7 +255,13 @@ fun NowPlayingScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier.weight(1f).fillMaxWidth().padding(top = 2.dp),
modifier =
Modifier
.weight(1f)
.fillMaxWidth()
.padding(top = 2.dp)
.clickable(enabled = onAlbumClick != null) { onAlbumClick?.invoke() }
.testTag(NOW_PLAYING_COVER_TAG),
contentAlignment = Alignment.Center,
) {
CoverArt(
@@ -286,7 +300,13 @@ fun NowPlayingScreen(
modifier = Modifier.basicMarquee(),
)
}
Box(Modifier.height(subtitleHeight), contentAlignment = Alignment.Center) {
Box(
modifier =
Modifier
.height(subtitleHeight)
.clickable(enabled = onArtistClick != null) { onArtistClick?.invoke() },
contentAlignment = Alignment.Center,
) {
Text(
text = state.item?.artist.orEmpty(),
style = subtitleStyle,
@@ -0,0 +1,98 @@
package com.subtracks.ui.playback
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.model.CoverArtRef
import com.subtracks.playback.PlaybackState
import com.subtracks.playback.QueueItem
import com.subtracks.ui.theme.SubtracksTheme
import com.subtracks.ui.theme.artworkColorsFromSeed
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.GraphicsMode
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class NowPlayingTapsTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()
private val state =
PlaybackState(
item =
QueueItem(
id = "s-eiirp",
title = "Everything In Its Right Place",
artist = "Radiohead",
album = "Kid A",
coverArtId = "art-al-kid-a",
),
isPlaying = true,
durationMs = 251_000,
hasNext = true,
hasPrevious = false,
)
private fun render(
onAlbumClick: () -> Unit = {},
onArtistClick: () -> Unit = {},
) {
composeRule.setContent {
SubtracksTheme {
NowPlayingScreen(
state = state,
positionMs = 62_000,
title = "Kid A",
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
artwork = artworkColorsFromSeed(android.graphics.Color.rgb(120, 80, 200)),
onBack = {},
onQueue = {},
onPlayPause = {},
onNext = {},
onPrevious = {},
onAlbumClick = onAlbumClick,
onArtistClick = onArtistClick,
onSeek = {},
)
}
}
}
@Test
fun tappingTheCoverArtOpensTheAlbum() {
var opened = false
render(onAlbumClick = { opened = true })
composeRule.onNodeWithTag(NOW_PLAYING_COVER_TAG).performClick()
assertTrue("the cover art should open the album", opened)
}
@Test
fun tappingTheArtistNameOpensTheArtist() {
var opened = false
render(onArtistClick = { opened = true })
composeRule.onNodeWithText("Radiohead").performClick()
assertTrue("the artist name should open the artist", opened)
}
@Test
fun theTapTargetsStayInertWithoutADestination() {
var opened = false
render()
composeRule.onNodeWithTag(NOW_PLAYING_COVER_TAG).performClick()
composeRule.onNodeWithText("Radiohead").performClick()
assertFalse(opened)
}
}