diff --git a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt index bdcb9225..6557cc8c 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt @@ -727,6 +727,15 @@ interface LibraryDao { ) suspend fun downloadedSongIds(sourceId: Long): List + @Query( + "SELECT songs.*, albums.coverArt AS coverArt FROM songs " + + "LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + + "JOIN song_downloads d ON d.sourceId = songs.sourceId AND d.songId = songs.id " + + "WHERE songs.sourceId = :sourceId AND d.status = 'Completed' " + + "ORDER BY songs.title COLLATE NOCASE, songs.id", + ) + fun homeDownloadedSongs(sourceId: Long): PagingSource + @Query( "SELECT songs.*, albums.coverArt AS coverArt FROM songs " + "LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + diff --git a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt index bbbcee8c..0b116fb6 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt @@ -94,6 +94,8 @@ class LibraryRepository( suspend fun downloadedSongIds(sourceId: Long): List = db.libraryDao().downloadedSongIds(sourceId) + fun downloadedSongsPage(sourceId: Long): Flow> = pager(60) { db.libraryDao().homeDownloadedSongs(sourceId) } + fun songsByGenre( sourceId: Long, genre: String, diff --git a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt index 4c61d0c3..d7841bba 100644 --- a/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt +++ b/app/src/main/kotlin/com/subtracks/ui/AppRoot.kt @@ -72,6 +72,7 @@ import com.subtracks.ui.downloads.DownloadsRoute import com.subtracks.ui.home.HomeListRequest import com.subtracks.ui.home.HomeListRoute import com.subtracks.ui.home.HomeSection +import com.subtracks.ui.home.OfflineListKind import com.subtracks.ui.library.AlbumDetailRoute import com.subtracks.ui.library.ArtistDetailRoute import com.subtracks.ui.library.LibraryRoute @@ -123,6 +124,7 @@ private object Routes { const val HOME_SECTION = "home/more/{section}" const val HOME_GENRE = "home/genre/{genre}" const val HOME_DECADE = "home/decade/{decade}" + const val HOME_DOWNLOADED = "home/downloaded/{kind}" fun album( id: String, @@ -143,6 +145,8 @@ private object Routes { fun homeGenre(genre: String) = "home/genre/${Uri.encode(genre)}" fun homeDecade(decade: Long) = "home/decade/$decade" + + fun homeDownloaded(kind: OfflineListKind) = "home/downloaded/${kind.name}" } private val DETAIL_ROUTES = @@ -364,6 +368,7 @@ private fun MainNavigation() { navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId)) }, onHomeMore = { section -> navController.navigate(Routes.homeSection(section)) }, + onHomeDownloaded = { kind -> navController.navigate(Routes.homeDownloaded(kind)) }, onGenreClick = { genre -> navController.navigate(Routes.homeGenre(genre)) }, onDecadeClick = { decade -> navController.navigate(Routes.homeDecade(decade)) }, contextMenuHost = contextMenuHost, @@ -526,6 +531,35 @@ private fun MainNavigation() { setStar = libraryRepository::star, ) } + composable( + route = Routes.HOME_DOWNLOADED, + arguments = listOf(navArgument("kind") { type = NavType.StringType }), + ) { entry -> + val kind = + OfflineListKind.entries.firstOrNull { it.name == entry.arguments?.getString("kind") } + ?: return@composable + HomeListRoute( + request = HomeListRequest(title = kind.title, downloaded = kind), + onBack = { navController.popBackStack() }, + onAlbumClick = { album -> + navigateDetail(Routes.ALBUM_DETAIL, "albumId", album.id, Routes.album(album.id, album.coverArt)) + }, + onArtistClick = { artist -> + navigateDetail(Routes.ARTIST_DETAIL, "artistId", artist.id, Routes.artist(artist.id, artist.coverArt)) + }, + onPlaylistClick = { playlist -> + navigateDetail(Routes.PLAYLIST_DETAIL, "playlistId", playlist.id, Routes.playlist(playlist.id)) + }, + onViewAlbum = { albumId -> navigateDetail(Routes.ALBUM_DETAIL, "albumId", albumId, Routes.album(albumId)) }, + onViewArtist = { artistId -> + navigateDetail(Routes.ARTIST_DETAIL, "artistId", artistId, Routes.artist(artistId)) + }, + onGenreClick = { genre -> navController.navigate(Routes.homeGenre(genre)) }, + onDecadeClick = { decade -> navController.navigate(Routes.homeDecade(decade)) }, + contextMenuHost = contextMenuHost, + setStar = libraryRepository::star, + ) + } composable(Routes.ADD_SERVER) { AddSourceRoute( onSaved = { navController.popBackStack() }, diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt index d8c9e36c..e752e581 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt @@ -45,6 +45,7 @@ import com.subtracks.data.model.Album import com.subtracks.data.model.AlbumSongItem import com.subtracks.data.model.Artist import com.subtracks.data.model.CoverArtRef +import com.subtracks.data.model.Playlist import com.subtracks.data.model.Song import com.subtracks.data.source.StarType import com.subtracks.playback.PlaybackController @@ -53,6 +54,7 @@ import com.subtracks.ui.components.CoverArt import com.subtracks.ui.components.ItemActions import com.subtracks.ui.components.LoadingState import com.subtracks.ui.components.MenuTarget +import com.subtracks.ui.library.PlaylistsContent import com.subtracks.ui.library.SongRow import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel @@ -64,6 +66,7 @@ fun HomeListRoute( onBack: () -> Unit, onAlbumClick: (Album) -> Unit, onArtistClick: (Artist) -> Unit, + onPlaylistClick: (Playlist) -> Unit = {}, onViewAlbum: (String) -> Unit, onViewArtist: (String) -> Unit, onGenreClick: (String) -> Unit, @@ -91,6 +94,7 @@ fun HomeListRoute( albums = viewModel.albums.collectAsLazyPagingItems(), artists = viewModel.artists.collectAsLazyPagingItems(), songs = viewModel.songs.collectAsLazyPagingItems(), + playlists = viewModel.playlists.collectAsLazyPagingItems(), genres = genres, decades = decades, coverArt = viewModel::coverArt, @@ -98,6 +102,7 @@ fun HomeListRoute( onBack = onBack, onAlbumClick = onAlbumClick, onArtistClick = onArtistClick, + onPlaylistClick = onPlaylistClick, onSongClick = viewModel::play, onLongClick = { contextMenuHost?.show(it, actions) }, onGenreClick = onGenreClick, @@ -112,6 +117,7 @@ fun HomeListScreen( albums: LazyPagingItems, artists: LazyPagingItems, songs: LazyPagingItems, + playlists: LazyPagingItems, genres: List = emptyList(), decades: List = emptyList(), coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null }, @@ -119,6 +125,7 @@ fun HomeListScreen( onBack: () -> Unit = {}, onAlbumClick: (Album) -> Unit = {}, onArtistClick: (Artist) -> Unit = {}, + onPlaylistClick: (Playlist) -> Unit = {}, onSongClick: (Song) -> Unit = {}, onLongClick: (MenuTarget) -> Unit = {}, onGenreClick: (String) -> Unit = {}, @@ -139,63 +146,10 @@ fun HomeListScreen( ) }, ) { padding -> - when (request.section) { - HomeSection.Genres -> { - PlainList( - items = genres, - label = { it }, - onClick = onGenreClick, - modifier = Modifier.padding(padding), - ) - } - - HomeSection.Decades -> { - PlainList( - items = decades, - label = { "${it}s" }, - onClick = onDecadeClick, - modifier = Modifier.padding(padding), - ) - } - - HomeSection.RecentlyPlayedArtists, HomeSection.MostPlayedArtists -> { - ArtistsList( - artists = artists, - coverArt = coverArt, - info = { artistListInfo(request, it, now) }, - onArtistClick = onArtistClick, - onLongClick = onLongClick, - modifier = Modifier.padding(padding), - ) - } - - HomeSection.RecentlyStarredSongs -> { - SongsList( - songs = songs, - coverArt = coverArt, - playingSongId = playingSongId, - onSongClick = onSongClick, - onLongClick = onLongClick, - modifier = Modifier.padding(padding), - ) - } - - HomeSection.RecentlyPlayedAlbums, - HomeSection.MostPlayedAlbums, - HomeSection.RecentlyAddedAlbums, - HomeSection.Rediscover, - null, - -> { - if (request.genre != null) { - SongsList( - songs = songs, - coverArt = coverArt, - playingSongId = playingSongId, - onSongClick = onSongClick, - onLongClick = onLongClick, - modifier = Modifier.padding(padding), - ) - } else { + val downloaded = request.downloaded + if (downloaded != null) { + when (downloaded) { + OfflineListKind.Albums -> { AlbumsList( albums = albums, coverArt = coverArt, @@ -205,6 +159,108 @@ fun HomeListScreen( modifier = Modifier.padding(padding), ) } + + OfflineListKind.Artists -> { + ArtistsList( + artists = artists, + coverArt = coverArt, + info = { artistListInfo(request, it, now) }, + onArtistClick = onArtistClick, + onLongClick = onLongClick, + modifier = Modifier.padding(padding), + ) + } + + OfflineListKind.Songs -> { + SongsList( + songs = songs, + coverArt = coverArt, + playingSongId = playingSongId, + onSongClick = onSongClick, + onLongClick = onLongClick, + modifier = Modifier.padding(padding), + ) + } + + OfflineListKind.Playlists -> { + PlaylistsContent( + items = playlists, + coverArt = coverArt, + bottomInset = 0.dp, + onPlaylistClick = onPlaylistClick, + onLongClick = onLongClick, + modifier = Modifier.padding(padding), + ) + } + } + } else { + when (request.section) { + HomeSection.Genres -> { + PlainList( + items = genres, + label = { it }, + onClick = onGenreClick, + modifier = Modifier.padding(padding), + ) + } + + HomeSection.Decades -> { + PlainList( + items = decades, + label = { "${it}s" }, + onClick = onDecadeClick, + modifier = Modifier.padding(padding), + ) + } + + HomeSection.RecentlyPlayedArtists, HomeSection.MostPlayedArtists -> { + ArtistsList( + artists = artists, + coverArt = coverArt, + info = { artistListInfo(request, it, now) }, + onArtistClick = onArtistClick, + onLongClick = onLongClick, + modifier = Modifier.padding(padding), + ) + } + + HomeSection.RecentlyStarredSongs -> { + SongsList( + songs = songs, + coverArt = coverArt, + playingSongId = playingSongId, + onSongClick = onSongClick, + onLongClick = onLongClick, + modifier = Modifier.padding(padding), + ) + } + + HomeSection.RecentlyPlayedAlbums, + HomeSection.MostPlayedAlbums, + HomeSection.RecentlyAddedAlbums, + HomeSection.Rediscover, + null, + -> { + if (request.genre != null) { + SongsList( + songs = songs, + coverArt = coverArt, + playingSongId = playingSongId, + onSongClick = onSongClick, + onLongClick = onLongClick, + modifier = Modifier.padding(padding), + ) + } else { + AlbumsList( + albums = albums, + coverArt = coverArt, + info = { albumListInfo(request, it, now) }, + onAlbumClick = onAlbumClick, + onLongClick = onLongClick, + modifier = Modifier.padding(padding), + ) + } + } } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt index 7863aee7..6670309d 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt @@ -8,7 +8,12 @@ import com.subtracks.data.model.Album import com.subtracks.data.model.AlbumSongItem import com.subtracks.data.model.Artist import com.subtracks.data.model.CoverArtRef +import com.subtracks.data.model.Playlist import com.subtracks.data.model.Song +import com.subtracks.data.prefs.AlbumSort +import com.subtracks.data.prefs.ArtistSort +import com.subtracks.data.prefs.PlaylistSort +import com.subtracks.data.prefs.StarredFilter import com.subtracks.data.repo.LibraryRepository import com.subtracks.data.repo.SourceRepository import com.subtracks.playback.PlaybackController @@ -26,8 +31,18 @@ data class HomeListRequest( val section: HomeSection? = null, val genre: String? = null, val decade: Long? = null, + val downloaded: OfflineListKind? = null, ) +enum class OfflineListKind( + val title: String, +) { + Albums("Downloaded albums"), + Artists("Downloaded artists"), + Songs("Downloaded songs"), + Playlists("Downloaded playlists"), +} + @OptIn(ExperimentalCoroutinesApi::class) class HomeListViewModel( private val libraryRepository: LibraryRepository, @@ -41,6 +56,17 @@ class HomeListViewModel( sourceId .flatMapLatest { id -> when { + request.downloaded == OfflineListKind.Albums -> { + libraryRepository.albums( + sourceId = id, + sort = AlbumSort.Added, + descending = false, + starred = StarredFilter.Any, + search = "", + downloaded = true, + ) + } + request.decade != null -> { libraryRepository.albumsByDecade(id, request.decade) } @@ -80,15 +106,43 @@ class HomeListViewModel( } else -> { - flowOf(PagingData.empty()) + if (request.downloaded == OfflineListKind.Artists) { + libraryRepository.artists( + sourceId = id, + sort = ArtistSort.Name, + descending = false, + starred = StarredFilter.Any, + search = "", + downloaded = true, + ) + } else { + flowOf(PagingData.empty()) + } } } }.cachedIn(viewModelScope) + val playlists: Flow> = + sourceId + .flatMapLatest { id -> + if (request.downloaded == OfflineListKind.Playlists) { + libraryRepository.playlists( + sourceId = id, + sort = PlaylistSort.Name, + descending = false, + search = "", + downloaded = true, + ) + } else { + flowOf(PagingData.empty()) + } + }.cachedIn(viewModelScope) + val songs: Flow> = sourceId .flatMapLatest { id -> when { + request.downloaded == OfflineListKind.Songs -> libraryRepository.downloadedSongsPage(id) request.genre != null -> libraryRepository.songsByGenre(id, request.genre) request.section == HomeSection.RecentlyStarredSongs -> libraryRepository.starredSongs(id) else -> flowOf(PagingData.empty()) @@ -117,6 +171,10 @@ class HomeListViewModel( viewModelScope.playGenreList(libraryRepository, playbackController, genre, song) } + request.downloaded == OfflineListKind.Songs -> { + viewModelScope.playDownloadedList(libraryRepository, playbackController, song) + } + request.section == HomeSection.RecentlyStarredSongs -> { viewModelScope.playStarredList( libraryRepository, diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt index 430026ec..5fc870b6 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt @@ -86,6 +86,7 @@ fun HomeRoute( onViewAlbum: (String) -> Unit, onViewArtist: (String) -> Unit, onMore: (HomeSection) -> Unit, + onDownloadedMore: (OfflineListKind) -> Unit, onGenreClick: (String) -> Unit, onDecadeClick: (Long) -> Unit, contextMenuHost: ContextMenuHost? = null, @@ -121,6 +122,7 @@ fun HomeRoute( onPlaylistClick = onPlaylistClick, onSongPlay = viewModel::playDownloaded, onLongClick = onLongClick, + onMore = onDownloadedMore, ) } else { val feed by viewModel.feed.collectAsStateWithLifecycle() @@ -277,6 +279,7 @@ fun OfflineHomeScreen( onPlaylistClick: (Playlist) -> Unit = {}, onSongPlay: (Song) -> Unit = {}, onLongClick: (MenuTarget) -> Unit = {}, + onMore: (OfflineListKind) -> Unit = {}, modifier: Modifier = Modifier, ) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) { @@ -285,9 +288,9 @@ fun OfflineHomeScreen( contentPadding = PaddingValues(top = topInset + 8.dp, bottom = bottomInset + 24.dp), ) { homeTitle() - downloadedAlbumRow(albums, coverArt, onAlbumClick, onLongClick) - downloadedArtistRow(artists, coverArt, onArtistClick, onLongClick) - downloadedPlaylistRow(playlists, coverArt, onPlaylistClick, onLongClick) + downloadedAlbumRow(albums, coverArt, onAlbumClick, onLongClick) { onMore(OfflineListKind.Albums) } + downloadedArtistRow(artists, coverArt, onArtistClick, onLongClick) { onMore(OfflineListKind.Artists) } + downloadedPlaylistRow(playlists, coverArt, onPlaylistClick, onLongClick) { onMore(OfflineListKind.Playlists) } songList( key = "downloaded-songs", title = "Downloaded songs", @@ -296,7 +299,7 @@ fun OfflineHomeScreen( playingSongId = playingSongId, onSongPlay = onSongPlay, onLongClick = onLongClick, - onMore = null, + onMore = { onMore(OfflineListKind.Songs) }, ) } } @@ -307,10 +310,11 @@ private fun LazyListScope.downloadedAlbumRow( coverArt: (String?, Boolean) -> CoverArtRef?, onAlbumClick: (Album) -> Unit, onLongClick: (MenuTarget) -> Unit, + onMore: () -> Unit, ) { if (albums.itemCount == 0) return item(key = "downloaded-albums-header") { - HomeSectionHeader("Downloaded albums", null) + HomeSectionHeader("Downloaded albums") { onMore() } } item(key = "downloaded-albums-row") { LazyRow( @@ -339,10 +343,11 @@ private fun LazyListScope.downloadedArtistRow( coverArt: (String?, Boolean) -> CoverArtRef?, onArtistClick: (Artist) -> Unit, onLongClick: (MenuTarget) -> Unit, + onMore: () -> Unit, ) { if (artists.itemCount == 0) return item(key = "downloaded-artists-header") { - HomeSectionHeader("Downloaded artists", null) + HomeSectionHeader("Downloaded artists") { onMore() } } item(key = "downloaded-artists-row") { LazyRow( @@ -371,10 +376,11 @@ private fun LazyListScope.downloadedPlaylistRow( coverArt: (String?, Boolean) -> CoverArtRef?, onPlaylistClick: (Playlist) -> Unit, onLongClick: (MenuTarget) -> Unit, + onMore: () -> Unit, ) { if (playlists.itemCount == 0) return item(key = "downloaded-playlists-header") { - HomeSectionHeader("Downloaded playlists", null) + HomeSectionHeader("Downloaded playlists") { onMore() } } item(key = "downloaded-playlists-row") { LazyRow( diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt index 73db44a8..13cef5a9 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt @@ -104,7 +104,9 @@ class HomeViewModel( }.cachedIn(viewModelScope) val downloadedSongs: Flow> = - activeSource.flatMapLatest { libraryRepository.downloadedSongs(it) } + activeSource + .flatMapLatest { libraryRepository.downloadedSongs(it) } + .map { it.take(HOME_STARRED_LIMIT) } val feed: StateFlow = libraryRepository.activeSourceId diff --git a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt index 62eb5108..a68257af 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt @@ -127,6 +127,7 @@ import com.subtracks.ui.components.bulkRef import com.subtracks.ui.components.statusBarScrim import com.subtracks.ui.home.HomeRoute import com.subtracks.ui.home.HomeSection +import com.subtracks.ui.home.OfflineListKind import com.subtracks.ui.theme.ArtworkColors import com.subtracks.ui.theme.librarySurfaceColor import com.subtracks.ui.theme.playerSurfaceColor @@ -208,6 +209,7 @@ fun LibraryRoute( onViewAlbum: (String) -> Unit, onViewArtist: (String) -> Unit, onHomeMore: (HomeSection) -> Unit = {}, + onHomeDownloaded: (OfflineListKind) -> Unit = {}, onGenreClick: (String) -> Unit = {}, onDecadeClick: (Long) -> Unit = {}, contextMenuHost: ContextMenuHost? = null, @@ -327,6 +329,7 @@ fun LibraryRoute( onViewAlbum = onViewAlbum, onViewArtist = onViewArtist, onMore = onHomeMore, + onDownloadedMore = onHomeDownloaded, onGenreClick = onGenreClick, onDecadeClick = onDecadeClick, contextMenuHost = contextMenuHost, diff --git a/app/src/test/kotlin/com/subtracks/ui/home/HomeScreenshotTest.kt b/app/src/test/kotlin/com/subtracks/ui/home/HomeScreenshotTest.kt index 501b1145..96059354 100644 --- a/app/src/test/kotlin/com/subtracks/ui/home/HomeScreenshotTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/home/HomeScreenshotTest.kt @@ -96,6 +96,7 @@ class HomeScreenshotTest { albums = remember { flowOf(PagingData.from(albums)) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.empty()) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.empty()) }.collectAsLazyPagingItems(), + playlists = remember { flowOf(PagingData.empty()) }.collectAsLazyPagingItems(), coverArt = { _, _ -> null }, ) } @@ -121,6 +122,7 @@ class HomeScreenshotTest { albums = remember { flowOf(PagingData.empty()) }.collectAsLazyPagingItems(), artists = remember { flowOf(PagingData.from(artists)) }.collectAsLazyPagingItems(), songs = remember { flowOf(PagingData.empty()) }.collectAsLazyPagingItems(), + playlists = remember { flowOf(PagingData.empty()) }.collectAsLazyPagingItems(), coverArt = { _, _ -> null }, ) }