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) + } +}