From b55e5cc5e89a203e801b2978b341842c0f39f46e Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:28:30 +0000 Subject: [PATCH] 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`. --- .../ui/components/HeroDetailScaffold.kt | 2 + .../subtracks/ui/library/AlbumDetailScreen.kt | 10 +- .../ui/library/ArtistDetailScreen.kt | 11 ++- .../subtracks/ui/playback/NowPlayingScreen.kt | 24 ++++- .../ui/playback/NowPlayingTapsTest.kt | 98 +++++++++++++++++++ 5 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingTapsTest.kt diff --git a/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt b/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt index 01483db9..e32a33fc 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt @@ -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()) { diff --git a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt index cfcbc5be..25a9c204 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt @@ -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, 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 d0e9a4bc..fe97e088 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt @@ -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( diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt index ff37c271..1c5029d2 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt @@ -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, diff --git a/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingTapsTest.kt b/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingTapsTest.kt new file mode 100644 index 00000000..f55f3f39 --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/ui/playback/NowPlayingTapsTest.kt @@ -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() + + 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) + } +}