mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
ui: split artist album credits from track appearances
Put the artist type and total album count on the single meta row beside the star and more buttons, then list albums where the artist is an album artist separately from albums where they only have a track credit. The hero shuffle button and page-level actions stay where they were. The split comes from an albumCredit flag on the shared credit rows, so the online and downloaded album lists are partitioned by the same rule and the two sections still sum to the stored album count.
This commit is contained in:
@@ -93,20 +93,21 @@ internal const val CREDITED_SONG_ROWS =
|
||||
"WHERE album_artists.sourceId = :sourceId"
|
||||
|
||||
// May repeat an (artistId, albumId) pair when several credit paths reach it; consumers dedupe.
|
||||
// albumCredit is 1 for album-level credits and 0 for track-level ones.
|
||||
internal const val CREDITED_ALBUM_ROWS =
|
||||
"SELECT albums.artistId AS artistId, albums.id AS albumId " +
|
||||
"SELECT albums.artistId AS artistId, albums.id AS albumId, 1 AS albumCredit " +
|
||||
"FROM albums WHERE albums.sourceId = :sourceId AND albums.artistId IS NOT NULL " +
|
||||
"UNION ALL " +
|
||||
"SELECT album_artists.artistId, albums.id " +
|
||||
"SELECT album_artists.artistId, albums.id, 1 " +
|
||||
"FROM album_artists JOIN albums " +
|
||||
"ON albums.sourceId = album_artists.sourceId AND albums.id = album_artists.albumId " +
|
||||
"WHERE album_artists.sourceId = :sourceId " +
|
||||
"UNION ALL " +
|
||||
"SELECT songs.artistId, songs.albumId " +
|
||||
"SELECT songs.artistId, songs.albumId, 0 " +
|
||||
"FROM songs JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
"WHERE songs.sourceId = :sourceId AND songs.artistId IS NOT NULL " +
|
||||
"UNION ALL " +
|
||||
"SELECT song_artists.artistId, songs.albumId " +
|
||||
"SELECT song_artists.artistId, songs.albumId, 0 " +
|
||||
"FROM song_artists JOIN songs " +
|
||||
"ON songs.sourceId = song_artists.sourceId AND songs.id = song_artists.songId " +
|
||||
"JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
@@ -1065,6 +1066,15 @@ interface LibraryDao {
|
||||
artistId: String,
|
||||
): Flow<List<Album>>
|
||||
|
||||
@Query(
|
||||
"SELECT DISTINCT albumId FROM ($CREDITED_ALBUM_ROWS) " +
|
||||
"WHERE artistId = :artistId AND albumCredit = 1",
|
||||
)
|
||||
fun albumCreditAlbumIds(
|
||||
sourceId: Long,
|
||||
artistId: String,
|
||||
): Flow<List<String>>
|
||||
|
||||
@Query("SELECT * FROM discs WHERE sourceId = :sourceId AND albumId = :albumId ORDER BY disc")
|
||||
fun discs(
|
||||
sourceId: Long,
|
||||
|
||||
@@ -167,6 +167,13 @@ data class Album(
|
||||
}
|
||||
}
|
||||
|
||||
data class ArtistAlbums(
|
||||
val albumCredits: List<Album> = emptyList(),
|
||||
val appearances: List<Album> = emptyList(),
|
||||
) {
|
||||
val count: Int get() = albumCredits.size + appearances.size
|
||||
}
|
||||
|
||||
@Entity(
|
||||
tableName = "discs",
|
||||
primaryKeys = ["sourceId", "albumId", "disc"],
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.subtracks.data.db.rediscoverAlbumsQuery
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.AlbumSongItem
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.ArtistAlbums
|
||||
import com.subtracks.data.model.Disc
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSongItem
|
||||
@@ -31,6 +32,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -376,12 +378,21 @@ class LibraryRepository(
|
||||
sourceId: Long,
|
||||
artistId: String,
|
||||
downloaded: Boolean = false,
|
||||
): Flow<List<Album>> =
|
||||
if (downloaded) {
|
||||
db.libraryDao().albumsForArtistDownloaded(sourceId, artistId)
|
||||
} else {
|
||||
db.libraryDao().albumsForArtist(sourceId, artistId)
|
||||
): Flow<ArtistAlbums> {
|
||||
val albums =
|
||||
if (downloaded) {
|
||||
db.libraryDao().albumsForArtistDownloaded(sourceId, artistId)
|
||||
} else {
|
||||
db.libraryDao().albumsForArtist(sourceId, artistId)
|
||||
}
|
||||
return combine(albums, db.libraryDao().albumCreditAlbumIds(sourceId, artistId)) { list, creditIds ->
|
||||
val credits = creditIds.toHashSet()
|
||||
ArtistAlbums(
|
||||
albumCredits = list.filter { it.id in credits },
|
||||
appearances = list.filterNot { it.id in credits },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun albumDiscs(
|
||||
sourceId: Long,
|
||||
|
||||
@@ -80,6 +80,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.subtracks.R
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.ArtistAlbums
|
||||
import com.subtracks.data.model.BulkDownloadAction
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.data.model.ListDownloadStatus
|
||||
@@ -140,11 +141,6 @@ private const val THEME_TRANSITION_MS = 100
|
||||
private const val SCRIM_FADE_MS = 180
|
||||
internal const val ARTIST_NAME_SCRIM_TAG = "artistNameScrim"
|
||||
|
||||
internal fun isAlbumArtist(
|
||||
albums: List<Album>,
|
||||
artistId: String,
|
||||
): Boolean = albums.any { album -> album.artistId == artistId || album.artistRefs.any { it.id == artistId } }
|
||||
|
||||
@Composable
|
||||
fun ArtistDetailRoute(
|
||||
artistId: String,
|
||||
@@ -192,8 +188,7 @@ fun ArtistDetailRoute(
|
||||
ArtistDetailScreen(
|
||||
artist = artist,
|
||||
artistName = artistName.orEmpty(),
|
||||
isAlbumArtist = albums?.let { isAlbumArtist(it, artistId) },
|
||||
albums = albums.orEmpty(),
|
||||
albums = albums ?: ArtistAlbums(),
|
||||
albumsLoaded = albums != null,
|
||||
art = art,
|
||||
artThumbnail = artThumbnail,
|
||||
@@ -225,8 +220,7 @@ fun ArtistDetailScreen(
|
||||
artist: Artist?,
|
||||
modifier: Modifier = Modifier,
|
||||
artistName: String = "",
|
||||
isAlbumArtist: Boolean? = null,
|
||||
albums: List<Album>,
|
||||
albums: ArtistAlbums = ArtistAlbums(),
|
||||
albumsLoaded: Boolean = true,
|
||||
art: CoverArtRef?,
|
||||
artThumbnail: CoverArtRef? = null,
|
||||
@@ -252,9 +246,19 @@ fun ArtistDetailScreen(
|
||||
val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||
val navBarBottom = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
|
||||
val barHeight = statusBarTop + TopAppBarDefaults.TopAppBarExpandedHeight
|
||||
val albumCount = albums.size.toLong()
|
||||
val albumCount = albums.count.toLong()
|
||||
val displayName = artistName.ifBlank { artist?.name.orEmpty() }
|
||||
val artistType = stringResource(if (isAlbumArtist == true) R.string.artist_type_album_artist else R.string.artist_type_artist)
|
||||
val artistType =
|
||||
stringResource(
|
||||
if (albums.albumCredits.isNotEmpty()) R.string.artist_type_album_artist else R.string.artist_type_artist,
|
||||
)
|
||||
val albumCountText =
|
||||
if (albumsLoaded) {
|
||||
pluralStringResource(R.plurals.resources_album_count, albumCount.toInt(), albumCount)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val metaText = if (albumsLoaded) "$artistType · $albumCountText" else ""
|
||||
var titleHeightPx by remember { mutableFloatStateOf(0f) }
|
||||
val titleHeight = if (titleHeightPx > 0f) with(density) { titleHeightPx.toDp() } else lineHeight
|
||||
val titleMaxLines =
|
||||
@@ -386,24 +390,12 @@ fun ArtistDetailScreen(
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = TITLE_INSET),
|
||||
) {
|
||||
Text(
|
||||
text = if (albumsLoaded) artistType else "",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
// Hold the row at the button height even without the buttons, so the
|
||||
// label-to-count gap is the same for every artist type.
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().defaultMinSize(minHeight = 48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text =
|
||||
if (albumsLoaded) {
|
||||
pluralStringResource(R.plurals.resources_album_count, albumCount.toInt(), albumCount)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
text = metaText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -435,49 +427,39 @@ fun ArtistDetailScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
itemsIndexed(albums, key = { _, album -> album.id }) { index, album ->
|
||||
val art = coverArt(album.coverArt, false)
|
||||
val thumbnail = coverArt(album.coverArt, true)
|
||||
PrefetchArtworkSeeds(thumbnail)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = if (index % 2 == 0) 16.dp else 0.dp,
|
||||
end = if (index % 2 == 1) 16.dp else 0.dp,
|
||||
).combinedClickable(
|
||||
onClick = { onAlbumClick(album) },
|
||||
onLongClick = { onAlbumLongClick(MenuTarget.Album(album, thumbnail, albumDownloads[album.id])) },
|
||||
).onSizeChanged { albumHeightPx = it.height.toFloat() },
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(2.dp)),
|
||||
) {
|
||||
CoverArt(
|
||||
ref = art,
|
||||
name = album.name,
|
||||
thumbnailRef = thumbnail,
|
||||
showPlaceholder = art == null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
ListDownloadIndicator(
|
||||
status = albumDownloads[album.id],
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(4.dp),
|
||||
scrim = true,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = album.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
if (albums.albumCredits.isNotEmpty()) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
ArtistSectionHeader(stringResource(R.string.artist_section_albums, albums.albumCredits.size))
|
||||
}
|
||||
itemsIndexed(albums.albumCredits, key = { _, album -> album.id }) { index, album ->
|
||||
ArtistAlbumCard(
|
||||
album = album,
|
||||
index = index,
|
||||
coverArt = coverArt,
|
||||
downloadStatus = albumDownloads[album.id],
|
||||
onClick = { onAlbumClick(album) },
|
||||
onLongClick = { thumbnail ->
|
||||
onAlbumLongClick(MenuTarget.Album(album, thumbnail, albumDownloads[album.id]))
|
||||
},
|
||||
onSizeChanged = { albumHeightPx = it },
|
||||
)
|
||||
Text(
|
||||
text = album.year?.takeIf { it > 0 }?.toString() ?: "\u00A0",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
}
|
||||
}
|
||||
if (albums.appearances.isNotEmpty()) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
ArtistSectionHeader(stringResource(R.string.artist_section_appearances, albums.appearances.size))
|
||||
}
|
||||
itemsIndexed(albums.appearances, key = { _, album -> album.id }) { index, album ->
|
||||
ArtistAlbumCard(
|
||||
album = album,
|
||||
index = index,
|
||||
coverArt = coverArt,
|
||||
downloadStatus = albumDownloads[album.id],
|
||||
onClick = { onAlbumClick(album) },
|
||||
onLongClick = { thumbnail ->
|
||||
onAlbumLongClick(MenuTarget.Album(album, thumbnail, albumDownloads[album.id]))
|
||||
},
|
||||
onSizeChanged = { albumHeightPx = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -523,6 +505,72 @@ fun ArtistDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtistSectionHeader(text: String) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtistAlbumCard(
|
||||
album: Album,
|
||||
index: Int,
|
||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||
downloadStatus: ListDownloadStatus?,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (CoverArtRef?) -> Unit,
|
||||
onSizeChanged: (Float) -> Unit,
|
||||
) {
|
||||
val art = coverArt(album.coverArt, false)
|
||||
val thumbnail = coverArt(album.coverArt, true)
|
||||
PrefetchArtworkSeeds(thumbnail)
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
start = if (index % 2 == 0) 16.dp else 0.dp,
|
||||
end = if (index % 2 == 1) 16.dp else 0.dp,
|
||||
).combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = { onLongClick(thumbnail) },
|
||||
).onSizeChanged { onSizeChanged(it.height.toFloat()) },
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(2.dp)),
|
||||
) {
|
||||
CoverArt(
|
||||
ref = art,
|
||||
name = album.name,
|
||||
thumbnailRef = thumbnail,
|
||||
showPlaceholder = art == null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
ListDownloadIndicator(
|
||||
status = downloadStatus,
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(4.dp),
|
||||
scrim = true,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = album.name,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
)
|
||||
Text(
|
||||
text = album.year?.takeIf { it > 0 }?.toString() ?: "\u00A0",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArtistTitle(
|
||||
text: String,
|
||||
|
||||
@@ -2,8 +2,8 @@ package com.subtracks.ui.library
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.ArtistAlbums
|
||||
import com.subtracks.data.model.BulkDownloadAction
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.data.model.DownloadList
|
||||
@@ -50,10 +50,10 @@ class ArtistDetailViewModel(
|
||||
|
||||
val offline: StateFlow<Boolean> = sourceRepository.offline
|
||||
|
||||
val albums: Flow<List<Album>?> =
|
||||
val albums: Flow<ArtistAlbums?> =
|
||||
combine(sourceId, sourceRepository.offline) { id, offline -> id to offline }
|
||||
.flatMapLatest { (id, offline) -> libraryRepository.artistAlbums(id, artistId, offline) }
|
||||
.map<List<Album>, List<Album>?> { it }
|
||||
.map<ArtistAlbums, ArtistAlbums?> { it }
|
||||
.onStart { emit(null) }
|
||||
|
||||
val downloadStatus: StateFlow<ListDownloadStatus> =
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
<string name="resources_filter_artist">Artist</string>
|
||||
<string name="artist_type_artist">Artist</string>
|
||||
<string name="artist_type_album_artist">Album artist</string>
|
||||
<string name="artist_section_albums">Albums (%1$d)</string>
|
||||
<string name="artist_section_appearances">Appears on (%1$d)</string>
|
||||
<string name="resources_filter_genre">Genre</string>
|
||||
<string name="resources_filter_starred">Starred</string>
|
||||
<string name="resources_song_list_delete_all_content">%1$s\n\nDeleting will free %2$s</string>
|
||||
|
||||
@@ -515,6 +515,54 @@ class LibraryDaoTest {
|
||||
assertEquals(listOf("al-1"), dao.albumsForArtistDownloaded(sourceId, "ar-guest").first().map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumCreditsAreSeparatedFromTrackAppearances() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertAlbums(
|
||||
listOf(
|
||||
album(sourceId, "al-1", "Own", year = null, starred = null),
|
||||
album(sourceId, "al-2", "Other", year = null, starred = null).copy(artistId = "ar-2"),
|
||||
),
|
||||
)
|
||||
dao.upsertSongs(
|
||||
listOf(
|
||||
song(sourceId, "s1", "One", starred = null),
|
||||
song(sourceId, "s2", "Two", starred = null).copy(albumId = "al-2", artistId = "ar-1"),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(listOf("al-1"), dao.albumCreditAlbumIds(sourceId, "ar-1").first())
|
||||
assertEquals(listOf("al-2"), dao.albumCreditAlbumIds(sourceId, "ar-2").first())
|
||||
assertEquals(
|
||||
listOf("al-1", "al-2"),
|
||||
dao
|
||||
.albumsForArtist(sourceId, "ar-1")
|
||||
.first()
|
||||
.map { it.id }
|
||||
.sorted(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun albumCreditIdsFindCreditsThroughArtistIndexes() =
|
||||
runTest {
|
||||
val plan =
|
||||
plan(
|
||||
"SELECT DISTINCT albumId FROM ($CREDITED_ALBUM_ROWS) " +
|
||||
"WHERE artistId = :artistId AND albumCredit = 1",
|
||||
)
|
||||
|
||||
assertTrue(plan, plan.contains("USING INDEX index_albums_sourceId_artistId (sourceId=? AND artistId=?)"))
|
||||
assertTrue(
|
||||
plan,
|
||||
plan.contains("USING INDEX index_album_artists_sourceId_artistId (sourceId=? AND artistId=?)"),
|
||||
)
|
||||
assertFalse(plan, plan.contains("SCAN albums"))
|
||||
assertFalse(plan, plan.contains("SCAN songs"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadedArtistsIncludeOneCreditedOnlyOnADownloadedTrack() =
|
||||
runTest {
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.download.ArtworkStore
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.prefs.AlbumSort
|
||||
import com.subtracks.data.prefs.StarredFilter
|
||||
import com.subtracks.data.prefs.fakeUserPreferences
|
||||
@@ -203,6 +204,33 @@ class LibraryRepositoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun artistAlbumsSeparatesAlbumCreditsFromTrackAppearances() =
|
||||
runBlocking {
|
||||
val sourceId = 1L
|
||||
db.sourcesDao().upsertSource(
|
||||
Source(id = sourceId, name = "test", address = "http://localhost", isActive = true, createdAt = 0),
|
||||
)
|
||||
db.libraryDao().upsertAlbums(
|
||||
listOf(
|
||||
seedAlbum(sourceId, 1),
|
||||
seedAlbum(sourceId, 2).copy(artistId = "ar2"),
|
||||
),
|
||||
)
|
||||
db.libraryDao().upsertSongs(
|
||||
listOf(
|
||||
seedSong(sourceId).copy(albumId = "al-0001"),
|
||||
seedSong(sourceId).copy(id = "s2", albumId = "al-0002"),
|
||||
),
|
||||
)
|
||||
|
||||
val albums = repository.artistAlbums(sourceId, "ar1").first()
|
||||
|
||||
assertEquals(listOf("al-0001"), albums.albumCredits.map { it.id })
|
||||
assertEquals(listOf("al-0002"), albums.appearances.map { it.id })
|
||||
assertEquals(2, albums.count)
|
||||
}
|
||||
|
||||
private suspend fun awaitRefreshes(
|
||||
presenter: RecordingPresenter,
|
||||
target: Int,
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.ArtistAlbums
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.data.model.ListDownloadStatus
|
||||
import com.subtracks.data.model.Playlist
|
||||
@@ -306,7 +307,10 @@ class AppScreenshotTest {
|
||||
SubtracksTheme {
|
||||
ArtistDetailScreen(
|
||||
artist = Fixtures.artists.first(),
|
||||
albums = Fixtures.albums.filter { it.artistId == Fixtures.artists.first().id },
|
||||
albums =
|
||||
ArtistAlbums(
|
||||
albumCredits = Fixtures.albums.filter { it.artistId == Fixtures.artists.first().id },
|
||||
),
|
||||
art = CoverArtRef("art-ar-radiohead", "test:art-ar-radiohead"),
|
||||
artwork = artworkColorsFromSeed(Color.rgb(120, 80, 200)),
|
||||
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.ArtistAlbums
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.Song
|
||||
@@ -177,8 +178,19 @@ class DemoScreenshotTest {
|
||||
ArtistDetailScreen(
|
||||
artist = demo.artist,
|
||||
artistName = demo.artist.name,
|
||||
isAlbumArtist = demo.artistAlbums.any { album -> album.artistRefs.any { it.id == demo.artist.id } },
|
||||
albums = demo.artistAlbums,
|
||||
albums =
|
||||
ArtistAlbums(
|
||||
albumCredits =
|
||||
demo.artistAlbums.filter { album ->
|
||||
album.artistId == demo.artist.id ||
|
||||
album.artistRefs.any { it.id == demo.artist.id }
|
||||
},
|
||||
appearances =
|
||||
demo.artistAlbums.filterNot { album ->
|
||||
album.artistId == demo.artist.id ||
|
||||
album.artistRefs.any { it.id == demo.artist.id }
|
||||
},
|
||||
),
|
||||
art = demo.coverArt(demo.artist.coverArt, false),
|
||||
artThumbnail = demo.coverArt(demo.artist.coverArt, true),
|
||||
artwork = demo.artwork(demo.artist.coverArt),
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.subtracks.ui.library
|
||||
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.ArtistRef
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class AlbumArtistLabelTest {
|
||||
@Test
|
||||
fun creditsBothTheSingularIdAndEveryPluralRef() {
|
||||
val album =
|
||||
album(
|
||||
artistId = "a1",
|
||||
artists = listOf(ArtistRef("a2", "B")),
|
||||
)
|
||||
|
||||
assertTrue(isAlbumArtist(listOf(album), "a1"))
|
||||
assertTrue(isAlbumArtist(listOf(album), "a2"))
|
||||
assertFalse(isAlbumArtist(listOf(album), "a3"))
|
||||
}
|
||||
|
||||
private fun album(
|
||||
artistId: String?,
|
||||
artists: List<ArtistRef>,
|
||||
) = Album(
|
||||
sourceId = 1,
|
||||
id = "al1",
|
||||
artistId = artistId,
|
||||
name = "Album",
|
||||
albumArtist = "Various Artists",
|
||||
created = 0,
|
||||
coverArt = null,
|
||||
genre = null,
|
||||
year = null,
|
||||
starred = null,
|
||||
songCount = 1,
|
||||
artists = artists,
|
||||
)
|
||||
}
|
||||
@@ -2,13 +2,18 @@ package com.subtracks.ui.library
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.hasScrollAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.ArtistAlbums
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -24,7 +29,7 @@ class ArtistDetailScreenTest {
|
||||
private fun render(
|
||||
artist: Artist?,
|
||||
artistName: String,
|
||||
isAlbumArtist: Boolean? = null,
|
||||
albums: ArtistAlbums = ArtistAlbums(),
|
||||
albumsLoaded: Boolean = true,
|
||||
) {
|
||||
composeRule.setContent {
|
||||
@@ -32,8 +37,7 @@ class ArtistDetailScreenTest {
|
||||
ArtistDetailScreen(
|
||||
artist = artist,
|
||||
artistName = artistName,
|
||||
isAlbumArtist = isAlbumArtist,
|
||||
albums = emptyList(),
|
||||
albums = albums,
|
||||
albumsLoaded = albumsLoaded,
|
||||
art = null,
|
||||
coverArt = { _, _ -> null },
|
||||
@@ -46,10 +50,14 @@ class ArtistDetailScreenTest {
|
||||
|
||||
@Test
|
||||
fun aCreditedTrackArtistShowsItsDerivedNameAndIsLabelledArtist() {
|
||||
render(artist = null, artistName = "Guest", isAlbumArtist = false)
|
||||
render(
|
||||
artist = null,
|
||||
artistName = "Guest",
|
||||
albums = ArtistAlbums(appearances = listOf(album("al-1", "Guest Album"))),
|
||||
)
|
||||
|
||||
composeRule.onAllNodesWithText("Guest")[0].assertExists()
|
||||
composeRule.onNodeWithText("Artist").assertExists()
|
||||
composeRule.onNodeWithText("Artist · 1 album").assertExists()
|
||||
composeRule.onAllNodesWithContentDescription("More options").assertCountEquals(0)
|
||||
}
|
||||
|
||||
@@ -58,13 +66,36 @@ class ArtistDetailScreenTest {
|
||||
render(
|
||||
artist = Artist(sourceId = 1, id = "ar-1", name = "Radiohead", albumCount = 1, starred = null),
|
||||
artistName = "Radiohead",
|
||||
isAlbumArtist = true,
|
||||
albums = ArtistAlbums(albumCredits = listOf(album("al-1", "Kid A"))),
|
||||
)
|
||||
|
||||
composeRule.onNodeWithText("Album artist").assertExists()
|
||||
composeRule.onNodeWithText("Album artist · 1 album").assertExists()
|
||||
composeRule.onNodeWithContentDescription("More options").assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mixedCreditsShowAlbumCreditsAndAppearancesInSeparateSections() {
|
||||
render(
|
||||
artist = Artist(sourceId = 1, id = "ar-1", name = "Radiohead", albumCount = 2, starred = null),
|
||||
artistName = "Radiohead",
|
||||
albums =
|
||||
ArtistAlbums(
|
||||
albumCredits = listOf(album("al-1", "Kid A")),
|
||||
appearances = listOf(album("al-2", "Guest Album")),
|
||||
),
|
||||
)
|
||||
|
||||
composeRule.onNodeWithText("Album artist · 2 albums").assertExists()
|
||||
|
||||
composeRule.onNode(hasScrollAction()).performScrollToNode(hasText("Albums (1)"))
|
||||
composeRule.onNodeWithText("Albums (1)").assertExists()
|
||||
composeRule.onNodeWithText("Kid A").assertExists()
|
||||
|
||||
composeRule.onNode(hasScrollAction()).performScrollToNode(hasText("Appears on (1)"))
|
||||
composeRule.onNodeWithText("Appears on (1)").assertExists()
|
||||
composeRule.onNodeWithText("Guest Album").assertExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theTypeAndCountStayBlankUntilTheAlbumsLoad() {
|
||||
render(
|
||||
@@ -77,4 +108,21 @@ class ArtistDetailScreenTest {
|
||||
composeRule.onNodeWithText("Album artist").assertDoesNotExist()
|
||||
composeRule.onNodeWithText("0 albums").assertDoesNotExist()
|
||||
}
|
||||
|
||||
private fun album(
|
||||
id: String,
|
||||
name: String,
|
||||
) = Album(
|
||||
sourceId = 1,
|
||||
id = id,
|
||||
artistId = "ar-1",
|
||||
name = name,
|
||||
albumArtist = "Radiohead",
|
||||
created = 0,
|
||||
coverArt = null,
|
||||
genre = null,
|
||||
year = null,
|
||||
starred = null,
|
||||
songCount = 1,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user