mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
show disc headings in multi-disc album track lists
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
package com.subtracks.ui.library
|
package com.subtracks.ui.library
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
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.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
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
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
@@ -64,6 +67,7 @@ fun AlbumDetailScreen(
|
|||||||
playingSongId: String? = null,
|
playingSongId: String? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val multiDisc = songs.map { it.disc ?: 1L }.distinct().size > 1
|
||||||
HeroDetailScaffold(
|
HeroDetailScaffold(
|
||||||
artwork = artwork,
|
artwork = artwork,
|
||||||
title = album?.name.orEmpty(),
|
title = album?.name.orEmpty(),
|
||||||
@@ -88,14 +92,32 @@ fun AlbumDetailScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
content = { rowModifier ->
|
content = { rowModifier ->
|
||||||
itemsIndexed(songs, key = { _, song -> song.id }) { index, song ->
|
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(
|
SongRow(
|
||||||
song = song,
|
song = song,
|
||||||
isPlaying = song.id == playingSongId,
|
isPlaying = song.id == playingSongId,
|
||||||
modifier = rowModifier.clickable { onSongClick(index) },
|
modifier = rowModifier.clickable { onSongClick(index) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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<ComponentActivity>()
|
||||||
|
|
||||||
|
@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<Song>) {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
|||||||
import androidx.compose.ui.test.onAllNodesWithTag
|
import androidx.compose.ui.test.onAllNodesWithTag
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onRoot
|
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.PagingData
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
@@ -155,6 +157,36 @@ class AppScreenshotTest {
|
|||||||
composeRule.onRoot().captureRoboImage("src/test/screenshots/album_detail_cool.png")
|
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
|
@Test
|
||||||
fun artistDetail() {
|
fun artistDetail() {
|
||||||
composeRule.setContent {
|
composeRule.setContent {
|
||||||
@@ -489,7 +521,7 @@ private object Fixtures {
|
|||||||
recentRank = null,
|
recentRank = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun song(
|
fun song(
|
||||||
id: String,
|
id: String,
|
||||||
albumId: String,
|
albumId: String,
|
||||||
artistId: String,
|
artistId: String,
|
||||||
@@ -498,6 +530,7 @@ private object Fixtures {
|
|||||||
artist: String,
|
artist: String,
|
||||||
track: Long,
|
track: Long,
|
||||||
duration: Long,
|
duration: Long,
|
||||||
|
disc: Long? = 1,
|
||||||
) = Song(
|
) = Song(
|
||||||
sourceId = 1,
|
sourceId = 1,
|
||||||
id = id,
|
id = id,
|
||||||
@@ -508,7 +541,7 @@ private object Fixtures {
|
|||||||
artist = artist,
|
artist = artist,
|
||||||
duration = duration,
|
duration = duration,
|
||||||
track = track,
|
track = track,
|
||||||
disc = 1,
|
disc = disc,
|
||||||
starred = null,
|
starred = null,
|
||||||
genre = null,
|
genre = null,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user