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 e7cfa82a..77a685d1 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt @@ -1,11 +1,14 @@ package com.subtracks.ui.library import androidx.compose.foundation.clickable -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.subtracks.data.model.Album import com.subtracks.data.model.CoverArtRef @@ -64,6 +67,7 @@ fun AlbumDetailScreen( playingSongId: String? = null, modifier: Modifier = Modifier, ) { + val multiDisc = songs.map { it.disc ?: 1L }.distinct().size > 1 HeroDetailScaffold( artwork = artwork, title = album?.name.orEmpty(), @@ -88,14 +92,32 @@ fun AlbumDetailScreen( ) }, content = { rowModifier -> - itemsIndexed(songs, key = { _, song -> song.id }) { index, song -> - SongRow( - song = song, - isPlaying = song.id == playingSongId, - modifier = rowModifier.clickable { onSongClick(index) }, - ) + var lastDisc: Long? = null + songs.forEachIndexed { index, song -> + val disc = song.disc ?: 1L + if (multiDisc && disc != lastDisc) { + item(key = "disc:$disc") { DiscHeader(disc) } + } + lastDisc = disc + item(key = song.id) { + SongRow( + song = song, + isPlaying = song.id == playingSongId, + modifier = rowModifier.clickable { onSongClick(index) }, + ) + } } }, modifier = modifier, ) } + +@Composable +private fun DiscHeader(disc: Long) { + Text( + text = "Disc $disc", + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 20.dp, bottom = 4.dp), + ) +} diff --git a/app/src/test/kotlin/com/subtracks/ui/AlbumDiscsTest.kt b/app/src/test/kotlin/com/subtracks/ui/AlbumDiscsTest.kt new file mode 100644 index 00000000..03725c29 --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/ui/AlbumDiscsTest.kt @@ -0,0 +1,101 @@ +package com.subtracks.ui + +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.subtracks.data.model.Album +import com.subtracks.data.model.Song +import com.subtracks.ui.library.AlbumDetailScreen +import com.subtracks.ui.theme.SubtracksTheme +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@RunWith(AndroidJUnit4::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL) +class AlbumDiscsTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun multiDiscAlbumShowsDiscHeaders() { + setContent(listOf(song("s-a", disc = 1, track = 1), song("s-b", disc = 1, track = 2), song("s-c", disc = 2, track = 1))) + assertEquals(1, nodesWithText("Disc 1")) + assertEquals(1, nodesWithText("Disc 2")) + } + + @Test + fun singleDiscAlbumShowsNoDiscHeaders() { + setContent(listOf(song("s-a", disc = 1, track = 1), song("s-b", disc = 1, track = 2))) + assertEquals(0, nodesWithText("Disc 1")) + } + + @Test + fun discHeadersAreHiddenWhenDiscsAreMissing() { + setContent(listOf(song("s-a", disc = null, track = 1), song("s-b", disc = null, track = 2))) + assertEquals(0, nodesWithText("Disc 1")) + } + + private fun setContent(songs: List) { + composeRule.setContent { + SubtracksTheme { + AlbumDetailScreen( + album = album(), + songs = songs, + coverArt = { _, _ -> null }, + artwork = null, + onBack = {}, + onSongClick = {}, + modifier = Modifier.fillMaxSize(), + ) + } + } + composeRule.waitUntil(timeoutMillis = 5_000) { nodesWithText(songs.first().title) == 1 } + } + + private fun nodesWithText(text: String) = composeRule.onAllNodesWithText(text).fetchSemanticsNodes().size + + private fun song( + id: String, + disc: Long?, + track: Long, + ) = Song( + sourceId = 1, + id = id, + albumId = "al-1", + artistId = "ar-1", + title = "Song $id", + album = "Album", + artist = "Artist", + duration = 200, + track = track, + disc = disc, + starred = null, + genre = null, + ) + + private fun album() = + Album( + sourceId = 1, + id = "al-1", + artistId = "ar-1", + name = "Album", + albumArtist = "Artist", + created = 0, + coverArt = null, + genre = null, + year = null, + starred = null, + songCount = 3, + frequentRank = null, + recentRank = null, + ) +} diff --git a/app/src/test/kotlin/com/subtracks/ui/AppScreenshotTest.kt b/app/src/test/kotlin/com/subtracks/ui/AppScreenshotTest.kt index 2191a061..3c3f9fa6 100644 --- a/app/src/test/kotlin/com/subtracks/ui/AppScreenshotTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/AppScreenshotTest.kt @@ -15,6 +15,8 @@ import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeUp import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems import androidx.test.core.app.ApplicationProvider @@ -155,6 +157,36 @@ class AppScreenshotTest { composeRule.onRoot().captureRoboImage("src/test/screenshots/album_detail_cool.png") } + @Test + fun albumDetailMultiDisc() { + val songs = + listOf( + Fixtures.song("s-eiirp", "al-kid-a", "ar-radiohead", "Everything In Its Right Place", "Kid A", "Radiohead", 1, 251), + Fixtures.song("s-kid-a", "al-kid-a", "ar-radiohead", "Kid A", "Kid A", "Radiohead", 2, 274), + Fixtures.song("s-anthem", "al-kid-a", "ar-radiohead", "The National Anthem", "Kid A", "Radiohead", 1, 351, disc = 2), + Fixtures.song("s-htdc", "al-kid-a", "ar-radiohead", "How to Disappear Completely", "Kid A", "Radiohead", 2, 356, disc = 2), + ) + composeRule.setContent { + SubtracksTheme { + AlbumDetailScreen( + album = Fixtures.albums.first(), + songs = songs, + coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } }, + artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)), + onBack = {}, + onSongClick = {}, + modifier = Modifier.fillMaxSize(), + ) + } + } + awaitText("Everything In Its Right Place") + composeRule.onRoot().performTouchInput { swipeUp(startY = centerY + 600f, endY = centerY - 600f, durationMillis = 400) } + composeRule.waitUntil(timeoutMillis = 5_000) { + composeRule.onAllNodesWithText("Disc 2").fetchSemanticsNodes().isNotEmpty() + } + composeRule.onRoot().captureRoboImage("src/test/screenshots/album_detail_multidisc.png") + } + @Test fun artistDetail() { composeRule.setContent { @@ -489,7 +521,7 @@ private object Fixtures { recentRank = null, ) - private fun song( + fun song( id: String, albumId: String, artistId: String, @@ -498,6 +530,7 @@ private object Fixtures { artist: String, track: Long, duration: Long, + disc: Long? = 1, ) = Song( sourceId = 1, id = id, @@ -508,7 +541,7 @@ private object Fixtures { artist = artist, duration = duration, track = track, - disc = 1, + disc = disc, starred = null, genre = null, )