mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
show album disc titles instead of generic disc numbers
Navidrome and gonic expose OpenSubsonic discTitles on the album response. Parse them into a transient Album.discTitles map and persist each album's titles in a child discs table (sourceId, albumId, disc, title) cascading from albums. Songs keep only their disc number, and the album screen joins by disc to render a title when one exists, falling back to "Disc N". Titles are read opportunistically: a server that doesn't send discTitles just leaves the table empty, so no API version negotiation is needed. Titles apply to single- and multi-disc albums, are pruned when removed, and are stored per album. Schema 10 -> 11.
This commit is contained in:
@@ -8,6 +8,8 @@ import androidx.room3.Upsert
|
||||
import androidx.room3.paging.PagingSourceDaoReturnTypeConverter
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.Disc
|
||||
import com.subtracks.data.model.DiscKey
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.Song
|
||||
@@ -66,6 +68,9 @@ interface LibraryDao {
|
||||
@Query("SELECT id FROM albums WHERE sourceId = :sourceId")
|
||||
suspend fun albumIds(sourceId: Long): List<String>
|
||||
|
||||
@Query("SELECT albumId, disc FROM discs WHERE sourceId = :sourceId")
|
||||
suspend fun discKeys(sourceId: Long): List<DiscKey>
|
||||
|
||||
@Query("SELECT id FROM playlists WHERE sourceId = :sourceId")
|
||||
suspend fun playlistIds(sourceId: Long): List<String>
|
||||
|
||||
@@ -84,6 +89,13 @@ interface LibraryDao {
|
||||
ids: Collection<String>,
|
||||
)
|
||||
|
||||
@Query("DELETE FROM discs WHERE sourceId = :sourceId AND albumId = :albumId AND disc = :disc")
|
||||
suspend fun deleteDisc(
|
||||
sourceId: Long,
|
||||
albumId: String,
|
||||
disc: Long,
|
||||
)
|
||||
|
||||
@Query("DELETE FROM playlists WHERE sourceId = :sourceId AND id IN (:ids)")
|
||||
suspend fun deletePlaylists(
|
||||
sourceId: Long,
|
||||
@@ -541,6 +553,12 @@ interface LibraryDao {
|
||||
artistId: String,
|
||||
): Flow<List<Album>>
|
||||
|
||||
@Query("SELECT * FROM discs WHERE sourceId = :sourceId AND albumId = :albumId ORDER BY disc")
|
||||
fun discs(
|
||||
sourceId: Long,
|
||||
albumId: String,
|
||||
): Flow<List<Disc>>
|
||||
|
||||
@Query("SELECT * FROM playlists WHERE sourceId = :sourceId AND id = :playlistId")
|
||||
fun playlist(
|
||||
sourceId: Long,
|
||||
|
||||
@@ -93,6 +93,18 @@ val MIGRATION_9_10 =
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_10_11 =
|
||||
object : Migration(10, 11) {
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
connection.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS `discs` (`sourceId` INTEGER NOT NULL, `albumId` TEXT NOT NULL, " +
|
||||
"`disc` INTEGER NOT NULL, `title` TEXT NOT NULL, PRIMARY KEY(`sourceId`, `albumId`, `disc`), " +
|
||||
"FOREIGN KEY(`sourceId`, `albumId`) REFERENCES `albums`(`sourceId`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
)
|
||||
connection.execSQL("CREATE INDEX IF NOT EXISTS `index_discs_sourceId_albumId` ON `discs` (`sourceId`, `albumId`)")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATIONS: Array<Migration> =
|
||||
arrayOf(
|
||||
MIGRATION_1_2,
|
||||
@@ -104,4 +116,5 @@ val MIGRATIONS: Array<Migration> =
|
||||
MIGRATION_7_8,
|
||||
MIGRATION_8_9,
|
||||
MIGRATION_9_10,
|
||||
MIGRATION_10_11,
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.room3.RoomDatabase
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.ArtworkSeed
|
||||
import com.subtracks.data.model.Disc
|
||||
import com.subtracks.data.model.PlaybackCursor
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
@@ -22,6 +23,7 @@ import com.subtracks.data.model.SubsonicSource
|
||||
SubsonicSource::class,
|
||||
Artist::class,
|
||||
Album::class,
|
||||
Disc::class,
|
||||
Playlist::class,
|
||||
PlaylistSong::class,
|
||||
Song::class,
|
||||
@@ -30,7 +32,7 @@ import com.subtracks.data.model.SubsonicSource
|
||||
ShuffleOrder::class,
|
||||
ArtworkSeed::class,
|
||||
],
|
||||
version = 10,
|
||||
version = 11,
|
||||
exportSchema = true,
|
||||
)
|
||||
@ColumnTypeConverters(QueueKindConverter::class)
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.room3.ColumnTypeConverter
|
||||
import androidx.room3.Embedded
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.ForeignKey
|
||||
import androidx.room3.Ignore
|
||||
import androidx.room3.Index
|
||||
import androidx.room3.PrimaryKey
|
||||
|
||||
@@ -85,6 +86,27 @@ data class Album(
|
||||
val songCount: Long,
|
||||
val frequentRank: Long?,
|
||||
val recentRank: Long?,
|
||||
@Ignore val discTitles: Map<Long, String> = emptyMap(),
|
||||
)
|
||||
|
||||
@Entity(
|
||||
tableName = "discs",
|
||||
primaryKeys = ["sourceId", "albumId", "disc"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Album::class,
|
||||
parentColumns = ["sourceId", "id"],
|
||||
childColumns = ["sourceId", "albumId"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
indices = [Index("sourceId", "albumId")],
|
||||
)
|
||||
data class Disc(
|
||||
val sourceId: Long,
|
||||
val albumId: String,
|
||||
val disc: Long,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Entity(
|
||||
@@ -166,6 +188,11 @@ data class SongListItem(
|
||||
val coverArt: String?,
|
||||
)
|
||||
|
||||
data class DiscKey(
|
||||
val albumId: String,
|
||||
val disc: Long,
|
||||
)
|
||||
|
||||
enum class QueueKind { Playlist, Album, Song, Songs }
|
||||
|
||||
@Entity(
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.paging.PagingSource
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.Disc
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.SongListItem
|
||||
@@ -213,6 +214,11 @@ class LibraryRepository(
|
||||
artistId: String,
|
||||
): Flow<List<Album>> = db.libraryDao().albumsForArtist(sourceId, artistId)
|
||||
|
||||
fun albumDiscs(
|
||||
sourceId: Long,
|
||||
albumId: String,
|
||||
): Flow<List<Disc>> = db.libraryDao().discs(sourceId, albumId)
|
||||
|
||||
fun albumSongs(
|
||||
sourceId: Long,
|
||||
albumId: String,
|
||||
|
||||
@@ -53,6 +53,7 @@ object SubsonicXml {
|
||||
songCount = element.longAttr("songCount") ?: 0L,
|
||||
frequentRank = null,
|
||||
recentRank = null,
|
||||
discTitles = element.discTitles(),
|
||||
)
|
||||
|
||||
fun songs(
|
||||
@@ -116,6 +117,20 @@ object SubsonicXml {
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.discTitles(): Map<Long, String> =
|
||||
childElements("discTitles")
|
||||
.mapNotNull { title ->
|
||||
val disc = title.longAttr("disc") ?: return@mapNotNull null
|
||||
title.attr("title").takeIf { it.isNotBlank() }?.let { disc to it }
|
||||
}.toMap()
|
||||
|
||||
private fun Element.childElements(tag: String): List<Element> {
|
||||
val children = childNodes
|
||||
return (0 until children.length).mapNotNull { index ->
|
||||
(children.item(index) as? Element)?.takeIf { it.tagName == tag }
|
||||
}
|
||||
}
|
||||
|
||||
private fun Element.elements(tag: String): List<Element> {
|
||||
val nodes = getElementsByTagName(tag)
|
||||
return (0 until nodes.length).mapNotNull { nodes.item(it) as? Element }
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.sqlite.SQLiteStatement
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.Disc
|
||||
import com.subtracks.data.model.DiscKey
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.Song
|
||||
@@ -25,6 +27,10 @@ class SyncService(
|
||||
val artists = source.getArtists()
|
||||
val albums = source.getAlbums()
|
||||
val songs = source.getSongs()
|
||||
val discs =
|
||||
albums.flatMap { album ->
|
||||
album.discTitles.map { (disc, title) -> Disc(album.sourceId, album.id, disc, title) }
|
||||
}
|
||||
val playlists = source.getPlaylists()
|
||||
val playlistSongs = source.getPlaylistSongs(playlists)
|
||||
|
||||
@@ -32,6 +38,7 @@ class SyncService(
|
||||
transactor.immediateTransaction {
|
||||
upsertChanged("artists", artistColumns, primaryKey, artists) { it.values() }
|
||||
upsertChanged("albums", albumColumns, primaryKey, albums) { it.values() }
|
||||
upsertChanged("discs", discColumns, discKey, discs) { it.values() }
|
||||
upsertChanged("songs", songColumns, primaryKey, songs) { it.values() }
|
||||
upsertChanged("playlists", playlistColumns, primaryKey, playlists) { it.values() }
|
||||
upsertChanged("playlist_songs", playlistSongColumns, playlistSongKey, playlistSongs) { it.values() }
|
||||
@@ -41,12 +48,16 @@ class SyncService(
|
||||
val existingAlbumIds = library.albumIds(source.id)
|
||||
val existingSongIds = library.songIds(source.id)
|
||||
val existingPlaylistIds = library.playlistIds(source.id)
|
||||
val existingDiscKeys = library.discKeys(source.id)
|
||||
|
||||
prune(existingArtistIds, artists.map { it.id }) { library.deleteArtists(source.id, it) }
|
||||
prune(existingAlbumIds, albums.map { it.id }) { library.deleteAlbums(source.id, it) }
|
||||
prune(existingSongIds, songs.map { it.id }) { library.deleteSongs(source.id, it) }
|
||||
prune(existingPlaylistIds, playlists.map { it.id }) { library.deletePlaylists(source.id, it) }
|
||||
|
||||
val currentDiscKeys = discs.mapTo(HashSet()) { DiscKey(it.albumId, it.disc) }
|
||||
existingDiscKeys.filterNot { it in currentDiscKeys }.forEach { library.deleteDisc(source.id, it.albumId, it.disc) }
|
||||
|
||||
val playlistSongCounts = playlistSongs.groupingBy { it.playlistId }.eachCount()
|
||||
playlists.forEach { playlist ->
|
||||
library.deletePlaylistSongsFrom(source.id, playlist.id, (playlistSongCounts[playlist.id] ?: 0).toLong())
|
||||
@@ -90,6 +101,10 @@ private val albumColumns =
|
||||
"recentRank",
|
||||
)
|
||||
|
||||
private val discKey = listOf("sourceId", "albumId", "disc")
|
||||
|
||||
private val discColumns = listOf("sourceId", "albumId", "disc", "title")
|
||||
|
||||
private val songColumns =
|
||||
listOf(
|
||||
"sourceId",
|
||||
@@ -118,6 +133,8 @@ private fun Artist.values() = listOf<Any?>(sourceId, id, name, albumCount, starr
|
||||
private fun Album.values() =
|
||||
listOf<Any?>(sourceId, id, artistId, name, albumArtist, created, coverArt, genre, year, starred, songCount, frequentRank, recentRank)
|
||||
|
||||
private fun Disc.values() = listOf<Any?>(sourceId, albumId, disc, title)
|
||||
|
||||
private fun Song.values() =
|
||||
listOf<Any?>(sourceId, id, albumId, artistId, title, album, artist, duration, track, disc, starred, genre, created)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.data.model.Disc
|
||||
import com.subtracks.data.model.QueueKind
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.playback.PlaybackController
|
||||
@@ -36,12 +37,14 @@ fun AlbumDetailRoute(
|
||||
) {
|
||||
val album by viewModel.album.collectAsStateWithLifecycle(initialValue = null)
|
||||
val songs by viewModel.songs.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val discs by viewModel.discs.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
val playback by playbackController.state.collectAsStateWithLifecycle()
|
||||
val context = playback.context
|
||||
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
|
||||
AlbumDetailScreen(
|
||||
album = album,
|
||||
songs = songs,
|
||||
discs = discs,
|
||||
coverArt = viewModel::coverArt,
|
||||
artwork = rememberArtworkColors(shortcutArt ?: viewModel.coverArt(album?.coverArt, true), THEME_TRANSITION_MS),
|
||||
onBack = onBack,
|
||||
@@ -56,6 +59,7 @@ fun AlbumDetailRoute(
|
||||
fun AlbumDetailScreen(
|
||||
album: Album?,
|
||||
songs: List<Song>,
|
||||
discs: List<Disc> = emptyList(),
|
||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||
artwork: ArtworkColors?,
|
||||
onBack: () -> Unit,
|
||||
@@ -68,6 +72,7 @@ fun AlbumDetailScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val multiDisc = songs.map { it.disc ?: 1L }.distinct().size > 1
|
||||
val discTitles = remember(discs) { discs.associate { it.disc to it.title } }
|
||||
HeroDetailScaffold(
|
||||
artwork = artwork,
|
||||
title = album?.name.orEmpty(),
|
||||
@@ -95,8 +100,9 @@ fun AlbumDetailScreen(
|
||||
var lastDisc: Long? = null
|
||||
songs.forEachIndexed { index, song ->
|
||||
val disc = song.disc ?: 1L
|
||||
if (multiDisc && disc != lastDisc) {
|
||||
item(key = "disc:$disc") { DiscHeader(disc) }
|
||||
val discTitle = discTitles[disc]
|
||||
if (disc != lastDisc && (multiDisc || !discTitle.isNullOrBlank())) {
|
||||
item(key = "disc:$disc") { DiscHeader(disc, discTitle) }
|
||||
}
|
||||
lastDisc = disc
|
||||
item(key = song.id) {
|
||||
@@ -114,9 +120,12 @@ fun AlbumDetailScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DiscHeader(disc: Long) {
|
||||
private fun DiscHeader(
|
||||
disc: Long,
|
||||
title: String?,
|
||||
) {
|
||||
Text(
|
||||
text = "Disc $disc",
|
||||
text = title?.takeIf { it.isNotBlank() } ?: "Disc $disc",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 20.dp, bottom = 4.dp),
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.data.model.Disc
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.repo.LibraryRepository
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
@@ -28,6 +29,8 @@ class AlbumDetailViewModel(
|
||||
|
||||
val songs: Flow<List<Song>> = sourceId.flatMapLatest { libraryRepository.albumSongs(it, albumId) }
|
||||
|
||||
val discs: Flow<List<Disc>> = sourceId.flatMapLatest { libraryRepository.albumDiscs(it, albumId) }
|
||||
|
||||
fun coverArt(
|
||||
coverArt: String?,
|
||||
thumbnail: Boolean,
|
||||
|
||||
@@ -60,6 +60,28 @@ class SubsonicXmlTest {
|
||||
assertEquals(12L, albums[0].songCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mapsAlbumDiscTitles() {
|
||||
val document =
|
||||
parse(
|
||||
"""
|
||||
<subsonic-response status="ok">
|
||||
<albumList2>
|
||||
<album id="al1" name="The Wall" artist="Pink Floyd" artistId="ar1" songCount="26">
|
||||
<discTitles disc="1" title="The Calm"/>
|
||||
<discTitles disc="2" title="The Storm"/>
|
||||
<discTitles disc="3" title=""/>
|
||||
</album>
|
||||
</albumList2>
|
||||
</subsonic-response>
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val albums = SubsonicXml.albums(1, document)
|
||||
|
||||
assertEquals(mapOf(1L to "The Calm", 2L to "The Storm"), albums.single().discTitles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsesTimestampsWithFractionsAndOffsets() {
|
||||
assertEquals(1789906657L, IsoDate.parse("2026-09-20T12:17:37.370827413Z"))
|
||||
|
||||
@@ -204,6 +204,102 @@ class SyncServiceTest {
|
||||
assertEquals(5678L, stored?.starred)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncStoresAlbumDiscTitles() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
albums = listOf(album("al1").copy(discTitles = mapOf(1L to "The Calm", 2L to "The Storm"))),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
|
||||
assertEquals(mapOf(1L to "The Calm", 2L to "The Storm"), storedDiscs("al1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncClearsDiscTitlesWhenTheServerStopsSendingThem() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
albums = listOf(album("al1").copy(discTitles = mapOf(1L to "The Calm"))),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
assertEquals(mapOf(1L to "The Calm"), storedDiscs("al1"))
|
||||
|
||||
source.albums = listOf(album("al1"))
|
||||
|
||||
SyncService(db, source).sync()
|
||||
assertEquals(emptyMap<Long, String>(), storedDiscs("al1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun discTitlesAreKeptPerAlbum() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
albums =
|
||||
listOf(
|
||||
album("al1").copy(discTitles = mapOf(1L to "First Album Disc")),
|
||||
album("al2"),
|
||||
),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
|
||||
assertEquals(mapOf(1L to "First Album Disc"), storedDiscs("al1"))
|
||||
assertEquals(emptyMap<Long, String>(), storedDiscs("al2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removingAnAlbumRemovesItsDiscTitles() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source =
|
||||
FakeMusicSource(
|
||||
albums =
|
||||
listOf(
|
||||
album("al1").copy(discTitles = mapOf(1L to "The Calm")),
|
||||
album("al2").copy(discTitles = mapOf(1L to "The Storm")),
|
||||
),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
|
||||
source.albums = listOf(album("al2").copy(discTitles = mapOf(1L to "The Storm")))
|
||||
|
||||
SyncService(db, source).sync()
|
||||
|
||||
assertEquals(emptyMap<Long, String>(), storedDiscs("al1"))
|
||||
assertEquals(mapOf(1L to "The Storm"), storedDiscs("al2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun syncUpdatesADiscTitle() =
|
||||
runTest {
|
||||
insertSource()
|
||||
val source = FakeMusicSource(albums = listOf(album("al1").copy(discTitles = mapOf(1L to "Old"))))
|
||||
|
||||
SyncService(db, source).sync()
|
||||
assertEquals(mapOf(1L to "Old"), storedDiscs("al1"))
|
||||
|
||||
source.albums = listOf(album("al1").copy(discTitles = mapOf(1L to "New")))
|
||||
|
||||
SyncService(db, source).sync()
|
||||
assertEquals(mapOf(1L to "New"), storedDiscs("al1"))
|
||||
}
|
||||
|
||||
private suspend fun storedDiscs(albumId: String): Map<Long, String> =
|
||||
db
|
||||
.libraryDao()
|
||||
.discs(1, albumId)
|
||||
.first()
|
||||
.associate { it.disc to it.title }
|
||||
|
||||
private suspend fun <T : Any> PagingSource<Int, T>.allRows(): List<T> {
|
||||
val page = load(PagingSource.LoadParams.Refresh(key = null, loadSize = 100, placeholdersEnabled = false))
|
||||
return (page as PagingSource.LoadResult.Page).data
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.Disc
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.ui.library.AlbumDetailScreen
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
@@ -32,6 +33,34 @@ class AlbumDiscsTest {
|
||||
assertEquals(1, nodesWithText("Disc 2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun discHeadersUseTheDiscTitleWhenPresent() {
|
||||
setContent(
|
||||
listOf(song("s-a", disc = 1, track = 1), song("s-b", disc = 2, track = 1)),
|
||||
discs = listOf(disc(1, "The Calm"), disc(2, "The Storm")),
|
||||
)
|
||||
assertEquals(1, nodesWithText("The Calm"))
|
||||
assertEquals(1, nodesWithText("The Storm"))
|
||||
assertEquals(0, nodesWithText("Disc 1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun discHeadersFallBackToTheDiscNumber() {
|
||||
setContent(listOf(song("s-a", disc = 1, track = 1), song("s-b", disc = 2, track = 1)))
|
||||
assertEquals(1, nodesWithText("Disc 1"))
|
||||
assertEquals(1, nodesWithText("Disc 2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleDiscAlbumShowsItsTitleWhenPresent() {
|
||||
setContent(
|
||||
listOf(song("s-a", disc = 1, track = 1), song("s-b", disc = 1, track = 2)),
|
||||
discs = listOf(disc(1, "The Calm")),
|
||||
)
|
||||
assertEquals(1, nodesWithText("The Calm"))
|
||||
assertEquals(0, nodesWithText("Disc 1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumTracksShowTheirTrackNumbers() {
|
||||
setContent(listOf(song("s-a", disc = 1, track = 1), song("s-b", disc = 1, track = 2)))
|
||||
@@ -51,12 +80,16 @@ class AlbumDiscsTest {
|
||||
assertEquals(0, nodesWithText("Disc 1"))
|
||||
}
|
||||
|
||||
private fun setContent(songs: List<Song>) {
|
||||
private fun setContent(
|
||||
songs: List<Song>,
|
||||
discs: List<Disc> = emptyList(),
|
||||
) {
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
AlbumDetailScreen(
|
||||
album = album(),
|
||||
songs = songs,
|
||||
discs = discs,
|
||||
coverArt = { _, _ -> null },
|
||||
artwork = null,
|
||||
onBack = {},
|
||||
@@ -89,6 +122,11 @@ class AlbumDiscsTest {
|
||||
genre = null,
|
||||
)
|
||||
|
||||
private fun disc(
|
||||
disc: Long,
|
||||
title: String,
|
||||
) = Disc(sourceId = 1, albumId = "al-1", disc = disc, title = title)
|
||||
|
||||
private fun album() =
|
||||
Album(
|
||||
sourceId = 1,
|
||||
|
||||
Reference in New Issue
Block a user