mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
open an artist's albums from the artists tab
This commit is contained in:
@@ -122,6 +122,18 @@ interface LibraryDao {
|
|||||||
albumId: String,
|
albumId: String,
|
||||||
): Flow<Album?>
|
): Flow<Album?>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM artists WHERE sourceId = :sourceId AND id = :artistId")
|
||||||
|
fun artist(
|
||||||
|
sourceId: Long,
|
||||||
|
artistId: String,
|
||||||
|
): Flow<Artist?>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM albums WHERE sourceId = :sourceId AND artistId = :artistId ORDER BY year DESC, name COLLATE NOCASE")
|
||||||
|
fun albumsForArtist(
|
||||||
|
sourceId: Long,
|
||||||
|
artistId: String,
|
||||||
|
): Flow<List<Album>>
|
||||||
|
|
||||||
@Query("SELECT * FROM playlists WHERE sourceId = :sourceId AND id = :playlistId")
|
@Query("SELECT * FROM playlists WHERE sourceId = :sourceId AND id = :playlistId")
|
||||||
fun playlist(
|
fun playlist(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ class LibraryRepository(
|
|||||||
albumId: String,
|
albumId: String,
|
||||||
): Flow<Album?> = db.libraryDao().album(sourceId, albumId)
|
): Flow<Album?> = db.libraryDao().album(sourceId, albumId)
|
||||||
|
|
||||||
|
fun artist(
|
||||||
|
sourceId: Long,
|
||||||
|
artistId: String,
|
||||||
|
): Flow<Artist?> = db.libraryDao().artist(sourceId, artistId)
|
||||||
|
|
||||||
|
fun artistAlbums(
|
||||||
|
sourceId: Long,
|
||||||
|
artistId: String,
|
||||||
|
): Flow<List<Album>> = db.libraryDao().albumsForArtist(sourceId, artistId)
|
||||||
|
|
||||||
fun albumSongs(
|
fun albumSongs(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
albumId: String,
|
albumId: String,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import com.subtracks.playback.PlaybackController
|
|||||||
import com.subtracks.playback.PlayerConnection
|
import com.subtracks.playback.PlayerConnection
|
||||||
import com.subtracks.ui.RootViewModel
|
import com.subtracks.ui.RootViewModel
|
||||||
import com.subtracks.ui.library.AlbumDetailViewModel
|
import com.subtracks.ui.library.AlbumDetailViewModel
|
||||||
|
import com.subtracks.ui.library.ArtistDetailViewModel
|
||||||
import com.subtracks.ui.library.LibraryViewModel
|
import com.subtracks.ui.library.LibraryViewModel
|
||||||
import com.subtracks.ui.library.PlaylistDetailViewModel
|
import com.subtracks.ui.library.PlaylistDetailViewModel
|
||||||
import com.subtracks.ui.playback.QueueViewModel
|
import com.subtracks.ui.playback.QueueViewModel
|
||||||
@@ -39,6 +40,7 @@ fun appModule(
|
|||||||
viewModel { SettingsViewModel(get(), get()) }
|
viewModel { SettingsViewModel(get(), get()) }
|
||||||
viewModel { AddSourceViewModel(get(), get()) }
|
viewModel { AddSourceViewModel(get(), get()) }
|
||||||
viewModel { params -> AlbumDetailViewModel(get(), get(), get(), params.get()) }
|
viewModel { params -> AlbumDetailViewModel(get(), get(), get(), params.get()) }
|
||||||
|
viewModel { params -> ArtistDetailViewModel(get(), get(), params.get()) }
|
||||||
viewModel { params -> PlaylistDetailViewModel(get(), get(), get(), params.get()) }
|
viewModel { params -> PlaylistDetailViewModel(get(), get(), get(), params.get()) }
|
||||||
viewModel { QueueViewModel(get(), get()) }
|
viewModel { QueueViewModel(get(), get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import com.subtracks.data.repo.SourceRepository
|
|||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
import com.subtracks.ui.library.AlbumDetailRoute
|
import com.subtracks.ui.library.AlbumDetailRoute
|
||||||
|
import com.subtracks.ui.library.ArtistDetailRoute
|
||||||
import com.subtracks.ui.library.LibraryRoute
|
import com.subtracks.ui.library.LibraryRoute
|
||||||
import com.subtracks.ui.library.PlaylistDetailRoute
|
import com.subtracks.ui.library.PlaylistDetailRoute
|
||||||
import com.subtracks.ui.playback.MiniPlayer
|
import com.subtracks.ui.playback.MiniPlayer
|
||||||
@@ -68,10 +69,13 @@ private object Routes {
|
|||||||
const val SETTINGS = "settings"
|
const val SETTINGS = "settings"
|
||||||
const val ADD_SERVER = "add-server"
|
const val ADD_SERVER = "add-server"
|
||||||
const val ALBUM_DETAIL = "album/{albumId}"
|
const val ALBUM_DETAIL = "album/{albumId}"
|
||||||
|
const val ARTIST_DETAIL = "artist/{artistId}"
|
||||||
const val PLAYLIST_DETAIL = "playlist/{playlistId}"
|
const val PLAYLIST_DETAIL = "playlist/{playlistId}"
|
||||||
|
|
||||||
fun album(id: String) = "album/${Uri.encode(id)}"
|
fun album(id: String) = "album/${Uri.encode(id)}"
|
||||||
|
|
||||||
|
fun artist(id: String) = "artist/${Uri.encode(id)}"
|
||||||
|
|
||||||
fun playlist(id: String) = "playlist/${Uri.encode(id)}"
|
fun playlist(id: String) = "playlist/${Uri.encode(id)}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +136,7 @@ private fun MainNavigation() {
|
|||||||
composable(Routes.LIBRARY) {
|
composable(Routes.LIBRARY) {
|
||||||
LibraryRoute(
|
LibraryRoute(
|
||||||
onAlbumClick = { album -> navController.navigate(Routes.album(album.id)) },
|
onAlbumClick = { album -> navController.navigate(Routes.album(album.id)) },
|
||||||
|
onArtistClick = { artist -> navController.navigate(Routes.artist(artist.id)) },
|
||||||
onPlaylistClick = { playlist -> navController.navigate(Routes.playlist(playlist.id)) },
|
onPlaylistClick = { playlist -> navController.navigate(Routes.playlist(playlist.id)) },
|
||||||
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
|
onOpenSettings = { navController.navigate(Routes.SETTINGS) },
|
||||||
bottomInset = if (playerVisible) 0.dp else navBarInset,
|
bottomInset = if (playerVisible) 0.dp else navBarInset,
|
||||||
@@ -152,6 +157,16 @@ private fun MainNavigation() {
|
|||||||
onBack = { navController.popBackStack() },
|
onBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(
|
||||||
|
route = Routes.ARTIST_DETAIL,
|
||||||
|
arguments = listOf(navArgument("artistId") { type = NavType.StringType }),
|
||||||
|
) { entry ->
|
||||||
|
ArtistDetailRoute(
|
||||||
|
artistId = entry.arguments?.getString("artistId").orEmpty(),
|
||||||
|
onBack = { navController.popBackStack() },
|
||||||
|
onAlbumClick = { album -> navController.navigate(Routes.album(album.id)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
composable(
|
composable(
|
||||||
route = Routes.PLAYLIST_DETAIL,
|
route = Routes.PLAYLIST_DETAIL,
|
||||||
arguments = listOf(navArgument("playlistId") { type = NavType.StringType }),
|
arguments = listOf(navArgument("playlistId") { type = NavType.StringType }),
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package com.subtracks.ui.library
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.subtracks.data.model.Album
|
||||||
|
import com.subtracks.data.model.Artist
|
||||||
|
import com.subtracks.data.model.CoverArtRef
|
||||||
|
import com.subtracks.ui.components.CoverArt
|
||||||
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ArtistDetailRoute(
|
||||||
|
artistId: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onAlbumClick: (Album) -> Unit,
|
||||||
|
viewModel: ArtistDetailViewModel = koinViewModel(key = artistId) { parametersOf(artistId) },
|
||||||
|
) {
|
||||||
|
val artist by viewModel.artist.collectAsStateWithLifecycle(initialValue = null)
|
||||||
|
val albums by viewModel.albums.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
ArtistDetailScreen(
|
||||||
|
artist = artist,
|
||||||
|
albums = albums,
|
||||||
|
coverArt = viewModel::coverArt,
|
||||||
|
onBack = onBack,
|
||||||
|
onAlbumClick = onAlbumClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ArtistDetailScreen(
|
||||||
|
artist: Artist?,
|
||||||
|
albums: List<Album>,
|
||||||
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onAlbumClick: (Album) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = modifier,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(artist?.name.orEmpty(), maxLines = 1) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(3),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.padding(padding).fillMaxSize(),
|
||||||
|
) {
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
CoverArt(
|
||||||
|
ref = coverArt(artist?.coverArt, false),
|
||||||
|
name = artist?.name.orEmpty(),
|
||||||
|
modifier = Modifier.size(120.dp).clip(CircleShape),
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(artist?.name.orEmpty(), style = MaterialTheme.typography.titleLarge)
|
||||||
|
Text(
|
||||||
|
text = "${albums.size} albums",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(albums, key = { it.id }) { album ->
|
||||||
|
Column(Modifier.clickable { onAlbumClick(album) }) {
|
||||||
|
CoverArt(
|
||||||
|
ref = coverArt(album.coverArt, true),
|
||||||
|
name = album.name,
|
||||||
|
modifier = Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(2.dp)),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = album.name,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.subtracks.ui.library
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.subtracks.data.model.Album
|
||||||
|
import com.subtracks.data.model.Artist
|
||||||
|
import com.subtracks.data.model.CoverArtRef
|
||||||
|
import com.subtracks.data.repo.LibraryRepository
|
||||||
|
import com.subtracks.data.repo.SourceRepository
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class ArtistDetailViewModel(
|
||||||
|
libraryRepository: LibraryRepository,
|
||||||
|
private val sourceRepository: SourceRepository,
|
||||||
|
private val artistId: String,
|
||||||
|
) : ViewModel() {
|
||||||
|
private val sourceId = libraryRepository.activeSourceId.filterNotNull()
|
||||||
|
|
||||||
|
val artist: Flow<Artist?> = sourceId.flatMapLatest { libraryRepository.artist(it, artistId) }
|
||||||
|
|
||||||
|
val albums: Flow<List<Album>> = sourceId.flatMapLatest { libraryRepository.artistAlbums(it, artistId) }
|
||||||
|
|
||||||
|
fun coverArt(
|
||||||
|
coverArt: String?,
|
||||||
|
thumbnail: Boolean,
|
||||||
|
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.subtracks.ui.library
|
package com.subtracks.ui.library
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -29,6 +30,7 @@ fun ArtistsContent(
|
|||||||
items: LazyPagingItems<Artist>,
|
items: LazyPagingItems<Artist>,
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
|
onArtistClick: (Artist) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
@@ -70,6 +72,7 @@ fun ArtistsContent(
|
|||||||
modifier = Modifier.size(48.dp).clip(CircleShape),
|
modifier = Modifier.size(48.dp).clip(CircleShape),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
modifier = Modifier.clickable { onArtistClick(artist) },
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ enum class LibraryTab(
|
|||||||
@Composable
|
@Composable
|
||||||
fun LibraryRoute(
|
fun LibraryRoute(
|
||||||
onAlbumClick: (Album) -> Unit,
|
onAlbumClick: (Album) -> Unit,
|
||||||
|
onArtistClick: (Artist) -> Unit,
|
||||||
onPlaylistClick: (Playlist) -> Unit,
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
@@ -105,6 +106,7 @@ fun LibraryRoute(
|
|||||||
playlists = viewModel.playlists.collectAsLazyPagingItems(),
|
playlists = viewModel.playlists.collectAsLazyPagingItems(),
|
||||||
coverArt = viewModel::coverArt,
|
coverArt = viewModel::coverArt,
|
||||||
onAlbumClick = onAlbumClick,
|
onAlbumClick = onAlbumClick,
|
||||||
|
onArtistClick = onArtistClick,
|
||||||
onPlaylistClick = onPlaylistClick,
|
onPlaylistClick = onPlaylistClick,
|
||||||
onSongClick = viewModel::playSong,
|
onSongClick = viewModel::playSong,
|
||||||
onSync = viewModel::sync,
|
onSync = viewModel::sync,
|
||||||
@@ -124,6 +126,7 @@ fun LibraryScreen(
|
|||||||
playlists: LazyPagingItems<Playlist>,
|
playlists: LazyPagingItems<Playlist>,
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
onAlbumClick: (Album) -> Unit,
|
onAlbumClick: (Album) -> Unit,
|
||||||
|
onArtistClick: (Artist) -> Unit,
|
||||||
onPlaylistClick: (Playlist) -> Unit,
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
onSongClick: (Int) -> Unit,
|
onSongClick: (Int) -> Unit,
|
||||||
onSync: () -> Unit,
|
onSync: () -> Unit,
|
||||||
@@ -210,7 +213,7 @@ fun LibraryScreen(
|
|||||||
) { page ->
|
) { page ->
|
||||||
when (LibraryTab.entries[page]) {
|
when (LibraryTab.entries[page]) {
|
||||||
LibraryTab.Albums -> AlbumsContent(albums, coverArt, bottomInset, onAlbumClick)
|
LibraryTab.Albums -> AlbumsContent(albums, coverArt, bottomInset, onAlbumClick)
|
||||||
LibraryTab.Artists -> ArtistsContent(artists, coverArt, bottomInset)
|
LibraryTab.Artists -> ArtistsContent(artists, coverArt, bottomInset, onArtistClick)
|
||||||
LibraryTab.Songs -> SongsContent(songs, coverArt, bottomInset, onSongClick, playingSongId)
|
LibraryTab.Songs -> SongsContent(songs, coverArt, bottomInset, onSongClick, playingSongId)
|
||||||
LibraryTab.Playlists -> PlaylistsContent(playlists, coverArt, bottomInset, onPlaylistClick)
|
LibraryTab.Playlists -> PlaylistsContent(playlists, coverArt, bottomInset, onPlaylistClick)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import com.subtracks.playback.PlaybackState
|
|||||||
import com.subtracks.playback.QueueItem
|
import com.subtracks.playback.QueueItem
|
||||||
import com.subtracks.ui.library.ALBUM_COVER_TAG
|
import com.subtracks.ui.library.ALBUM_COVER_TAG
|
||||||
import com.subtracks.ui.library.AlbumDetailScreen
|
import com.subtracks.ui.library.AlbumDetailScreen
|
||||||
|
import com.subtracks.ui.library.ArtistDetailScreen
|
||||||
import com.subtracks.ui.library.LibraryScreen
|
import com.subtracks.ui.library.LibraryScreen
|
||||||
import com.subtracks.ui.library.LibraryTab
|
import com.subtracks.ui.library.LibraryTab
|
||||||
import com.subtracks.ui.library.PlaylistDetailScreen
|
import com.subtracks.ui.library.PlaylistDetailScreen
|
||||||
@@ -130,6 +131,24 @@ class AppScreenshotTest {
|
|||||||
composeRule.onRoot().captureRoboImage("src/test/screenshots/album_detail.png")
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/album_detail.png")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun artistDetail() {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
ArtistDetailScreen(
|
||||||
|
artist = Fixtures.artists.first(),
|
||||||
|
albums = Fixtures.albums.filter { it.artistId == Fixtures.artists.first().id },
|
||||||
|
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
||||||
|
onBack = {},
|
||||||
|
onAlbumClick = {},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
awaitText("Kid A")
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/artist_detail.png")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun playlistDetail() {
|
fun playlistDetail() {
|
||||||
composeRule.setContent {
|
composeRule.setContent {
|
||||||
@@ -276,6 +295,7 @@ class AppScreenshotTest {
|
|||||||
playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) }.collectAsLazyPagingItems(),
|
playlists = remember { flowOf(PagingData.from(Fixtures.playlists)) }.collectAsLazyPagingItems(),
|
||||||
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
coverArt = { id, _ -> id?.let { CoverArtRef(it, "test:$it") } },
|
||||||
onAlbumClick = {},
|
onAlbumClick = {},
|
||||||
|
onArtistClick = {},
|
||||||
onPlaylistClick = {},
|
onPlaylistClick = {},
|
||||||
onSongClick = {},
|
onSongClick = {},
|
||||||
onSync = {},
|
onSync = {},
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class LibraryScreenTest {
|
|||||||
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(),
|
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(),
|
||||||
coverArt = { _, _ -> null },
|
coverArt = { _, _ -> null },
|
||||||
onAlbumClick = {},
|
onAlbumClick = {},
|
||||||
|
onArtistClick = {},
|
||||||
onPlaylistClick = {},
|
onPlaylistClick = {},
|
||||||
onSongClick = {},
|
onSongClick = {},
|
||||||
onSync = {},
|
onSync = {},
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ Goal: a usable app for browsing a server, end to end.
|
|||||||
- App shell: a root gate that shows the add-server flow until a server exists, then the tabbed library screen matching the Flutter app (icon-only tabs for albums, artists, songs and playlists, with sync and settings actions), plus album and playlist detail screens and a monochrome theme.
|
- App shell: a root gate that shows the add-server flow until a server exists, then the tabbed library screen matching the Flutter app (icon-only tabs for albums, artists, songs and playlists, with sync and settings actions), plus album and playlist detail screens and a monochrome theme.
|
||||||
- Source setup: name/address/username/password with token-auth toggle, a connection test, save-and-sync, plus server switching and removal in settings.
|
- Source setup: name/address/username/password with token-auth toggle, a connection test, save-and-sync, plus server switching and removal in settings.
|
||||||
- Library browsing: albums (covers-only grid), artists, songs and playlists paged from Room (`PagingSource`) and rendered with `LazyPagingItems`, with Coil cover art served from stable media URLs.
|
- Library browsing: albums (covers-only grid), artists, songs and playlists paged from Room (`PagingSource`) and rendered with `LazyPagingItems`, with Coil cover art served from stable media URLs.
|
||||||
- Roborazzi screenshots for the eight new screens, rendered on demand for review (not committed).
|
- Artist detail: tapping an artist opens their albums, newest first, and an album can be opened from there.
|
||||||
|
- Roborazzi screenshots for the new screens, rendered on demand for review (not committed).
|
||||||
|
|
||||||
Not done yet in this slice (deliberately): artist detail, cover-art tonal colour extraction, long-press menus, editing an existing server, and per-section sort/filter controls (only album sort is wired).
|
Not done yet in this slice (deliberately): cover-art tonal colour extraction, long-press menus, editing an existing server, and per-section sort/filter controls (only album sort is wired).
|
||||||
|
|
||||||
## Phase 3 - Playback (in progress)
|
## Phase 3 - Playback (in progress)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user