mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
home: swap to downloaded sections in offline mode
This commit is contained in:
@@ -710,6 +710,23 @@ interface LibraryDao {
|
|||||||
)
|
)
|
||||||
suspend fun starredSongIds(sourceId: Long): List<String>
|
suspend fun starredSongIds(sourceId: Long): List<String>
|
||||||
|
|
||||||
|
@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 downloadedSongs(sourceId: Long): Flow<List<AlbumSongItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT songs.id FROM songs " +
|
||||||
|
"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",
|
||||||
|
)
|
||||||
|
suspend fun downloadedSongIds(sourceId: Long): List<String>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ class LibraryRepository(
|
|||||||
|
|
||||||
suspend fun starredSongIds(sourceId: Long): List<String> = db.libraryDao().starredSongIds(sourceId)
|
suspend fun starredSongIds(sourceId: Long): List<String> = db.libraryDao().starredSongIds(sourceId)
|
||||||
|
|
||||||
|
fun downloadedSongs(sourceId: Long): Flow<List<AlbumSongItem>> = db.libraryDao().downloadedSongs(sourceId)
|
||||||
|
|
||||||
|
suspend fun downloadedSongIds(sourceId: Long): List<String> = db.libraryDao().downloadedSongIds(sourceId)
|
||||||
|
|
||||||
fun songsByGenre(
|
fun songsByGenre(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
genre: String,
|
genre: String,
|
||||||
|
|||||||
@@ -37,11 +37,15 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
|
import androidx.paging.compose.itemKey
|
||||||
import com.subtracks.R
|
import com.subtracks.R
|
||||||
import com.subtracks.data.model.Album
|
import com.subtracks.data.model.Album
|
||||||
import com.subtracks.data.model.AlbumSongItem
|
import com.subtracks.data.model.AlbumSongItem
|
||||||
import com.subtracks.data.model.Artist
|
import com.subtracks.data.model.Artist
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
|
import com.subtracks.data.model.Playlist
|
||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
import com.subtracks.data.source.StarType
|
import com.subtracks.data.source.StarType
|
||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
@@ -78,6 +82,7 @@ enum class HomeSection(
|
|||||||
fun HomeRoute(
|
fun HomeRoute(
|
||||||
onAlbumClick: (Album) -> Unit,
|
onAlbumClick: (Album) -> Unit,
|
||||||
onArtistClick: (Artist) -> Unit,
|
onArtistClick: (Artist) -> Unit,
|
||||||
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
onViewAlbum: (String) -> Unit,
|
onViewAlbum: (String) -> Unit,
|
||||||
onViewArtist: (String) -> Unit,
|
onViewArtist: (String) -> Unit,
|
||||||
onMore: (HomeSection) -> Unit,
|
onMore: (HomeSection) -> Unit,
|
||||||
@@ -90,7 +95,7 @@ fun HomeRoute(
|
|||||||
viewModel: HomeViewModel = koinViewModel(),
|
viewModel: HomeViewModel = koinViewModel(),
|
||||||
playbackController: PlaybackController = koinInject(),
|
playbackController: PlaybackController = koinInject(),
|
||||||
) {
|
) {
|
||||||
val feed by viewModel.feed.collectAsStateWithLifecycle()
|
val offline by viewModel.offline.collectAsStateWithLifecycle()
|
||||||
val playback by playbackController.state.collectAsStateWithLifecycle()
|
val playback by playbackController.state.collectAsStateWithLifecycle()
|
||||||
val actions =
|
val actions =
|
||||||
ItemActions(
|
ItemActions(
|
||||||
@@ -100,6 +105,25 @@ fun HomeRoute(
|
|||||||
viewAlbum = onViewAlbum,
|
viewAlbum = onViewAlbum,
|
||||||
viewArtist = onViewArtist,
|
viewArtist = onViewArtist,
|
||||||
)
|
)
|
||||||
|
val onLongClick: (MenuTarget) -> Unit = { contextMenuHost?.show(it, actions) }
|
||||||
|
if (offline) {
|
||||||
|
OfflineHomeScreen(
|
||||||
|
albums = viewModel.downloadedAlbums.collectAsLazyPagingItems(),
|
||||||
|
artists = viewModel.downloadedArtists.collectAsLazyPagingItems(),
|
||||||
|
playlists = viewModel.downloadedPlaylists.collectAsLazyPagingItems(),
|
||||||
|
songs = viewModel.downloadedSongs.collectAsStateWithLifecycle(initialValue = emptyList()).value,
|
||||||
|
coverArt = viewModel::coverArt,
|
||||||
|
playingSongId = playback.item?.id,
|
||||||
|
topInset = topInset,
|
||||||
|
bottomInset = bottomInset,
|
||||||
|
onAlbumClick = onAlbumClick,
|
||||||
|
onArtistClick = onArtistClick,
|
||||||
|
onPlaylistClick = onPlaylistClick,
|
||||||
|
onSongPlay = viewModel::playDownloaded,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val feed by viewModel.feed.collectAsStateWithLifecycle()
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
feed = feed,
|
feed = feed,
|
||||||
coverArt = viewModel::coverArt,
|
coverArt = viewModel::coverArt,
|
||||||
@@ -109,13 +133,14 @@ fun HomeRoute(
|
|||||||
onAlbumClick = onAlbumClick,
|
onAlbumClick = onAlbumClick,
|
||||||
onArtistClick = onArtistClick,
|
onArtistClick = onArtistClick,
|
||||||
onPlayStarred = viewModel::playStarred,
|
onPlayStarred = viewModel::playStarred,
|
||||||
onLongClick = { contextMenuHost?.show(it, actions) },
|
onLongClick = onLongClick,
|
||||||
onMore = onMore,
|
onMore = onMore,
|
||||||
onGenreClick = onGenreClick,
|
onGenreClick = onGenreClick,
|
||||||
onDecadeClick = onDecadeClick,
|
onDecadeClick = onDecadeClick,
|
||||||
onSync = viewModel::sync,
|
onSync = viewModel::sync,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
@@ -152,24 +177,7 @@ fun HomeScreen(
|
|||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(top = topInset + 8.dp, bottom = bottomInset + 24.dp),
|
contentPadding = PaddingValues(top = topInset + 8.dp, bottom = bottomInset + 24.dp),
|
||||||
) {
|
) {
|
||||||
item(key = "home-title") {
|
homeTitle()
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 0.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_subtracks_logo),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(width = 34.dp, height = 25.dp),
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(text = "subtracks", style = MaterialTheme.typography.headlineMedium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
albumRow(
|
albumRow(
|
||||||
HomeSection.RecentlyAddedAlbums,
|
HomeSection.RecentlyAddedAlbums,
|
||||||
feed.recentlyAddedAlbums,
|
feed.recentlyAddedAlbums,
|
||||||
@@ -233,6 +241,188 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.homeTitle() {
|
||||||
|
item(key = "home-title") {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 0.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_subtracks_logo),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(width = 34.dp, height = 25.dp),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(text = "subtracks", style = MaterialTheme.typography.headlineMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OfflineHomeScreen(
|
||||||
|
albums: LazyPagingItems<Album>,
|
||||||
|
artists: LazyPagingItems<Artist>,
|
||||||
|
playlists: LazyPagingItems<Playlist>,
|
||||||
|
songs: List<AlbumSongItem>,
|
||||||
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
|
playingSongId: String?,
|
||||||
|
topInset: Dp = 0.dp,
|
||||||
|
bottomInset: Dp = 0.dp,
|
||||||
|
onAlbumClick: (Album) -> Unit = {},
|
||||||
|
onArtistClick: (Artist) -> Unit = {},
|
||||||
|
onPlaylistClick: (Playlist) -> Unit = {},
|
||||||
|
onSongPlay: (Song) -> Unit = {},
|
||||||
|
onLongClick: (MenuTarget) -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
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)
|
||||||
|
songList(
|
||||||
|
key = "downloaded-songs",
|
||||||
|
title = "Downloaded songs",
|
||||||
|
songs = songs,
|
||||||
|
coverArt = coverArt,
|
||||||
|
playingSongId = playingSongId,
|
||||||
|
onSongPlay = onSongPlay,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
onMore = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.downloadedAlbumRow(
|
||||||
|
albums: LazyPagingItems<Album>,
|
||||||
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
|
onAlbumClick: (Album) -> Unit,
|
||||||
|
onLongClick: (MenuTarget) -> Unit,
|
||||||
|
) {
|
||||||
|
if (albums.itemCount == 0) return
|
||||||
|
item(key = "downloaded-albums-header") {
|
||||||
|
HomeSectionHeader("Downloaded albums", null)
|
||||||
|
}
|
||||||
|
item(key = "downloaded-albums-row") {
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(TILE_SPACING),
|
||||||
|
modifier = Modifier.padding(top = SECTION_CONTENT_TOP),
|
||||||
|
) {
|
||||||
|
items(count = albums.itemCount, key = albums.itemKey { it.id }) { index ->
|
||||||
|
val album = albums[index]
|
||||||
|
if (album != null) {
|
||||||
|
val ref = coverArt(album.coverArt, true)
|
||||||
|
AlbumTile(
|
||||||
|
album = album,
|
||||||
|
ref = ref,
|
||||||
|
onClick = { onAlbumClick(album) },
|
||||||
|
onLongClick = { onLongClick(MenuTarget.Album(album, ref)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.downloadedArtistRow(
|
||||||
|
artists: LazyPagingItems<Artist>,
|
||||||
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
|
onArtistClick: (Artist) -> Unit,
|
||||||
|
onLongClick: (MenuTarget) -> Unit,
|
||||||
|
) {
|
||||||
|
if (artists.itemCount == 0) return
|
||||||
|
item(key = "downloaded-artists-header") {
|
||||||
|
HomeSectionHeader("Downloaded artists", null)
|
||||||
|
}
|
||||||
|
item(key = "downloaded-artists-row") {
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(ARTIST_SPACING),
|
||||||
|
modifier = Modifier.padding(top = SECTION_CONTENT_TOP),
|
||||||
|
) {
|
||||||
|
items(count = artists.itemCount, key = artists.itemKey { it.id }) { index ->
|
||||||
|
val artist = artists[index]
|
||||||
|
if (artist != null) {
|
||||||
|
val ref = coverArt(artist.coverArt, true)
|
||||||
|
ArtistTile(
|
||||||
|
artist = artist,
|
||||||
|
ref = ref,
|
||||||
|
onClick = { onArtistClick(artist) },
|
||||||
|
onLongClick = { onLongClick(MenuTarget.Artist(artist, ref)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.downloadedPlaylistRow(
|
||||||
|
playlists: LazyPagingItems<Playlist>,
|
||||||
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
|
onLongClick: (MenuTarget) -> Unit,
|
||||||
|
) {
|
||||||
|
if (playlists.itemCount == 0) return
|
||||||
|
item(key = "downloaded-playlists-header") {
|
||||||
|
HomeSectionHeader("Downloaded playlists", null)
|
||||||
|
}
|
||||||
|
item(key = "downloaded-playlists-row") {
|
||||||
|
LazyRow(
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(TILE_SPACING),
|
||||||
|
modifier = Modifier.padding(top = SECTION_CONTENT_TOP),
|
||||||
|
) {
|
||||||
|
items(count = playlists.itemCount, key = playlists.itemKey { it.id }) { index ->
|
||||||
|
val playlist = playlists[index]
|
||||||
|
if (playlist != null) {
|
||||||
|
val ref = coverArt(playlist.coverArt, true)
|
||||||
|
PlaylistTile(
|
||||||
|
playlist = playlist,
|
||||||
|
ref = ref,
|
||||||
|
onClick = { onPlaylistClick(playlist) },
|
||||||
|
onLongClick = { onLongClick(MenuTarget.Playlist(playlist, ref)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PlaylistTile(
|
||||||
|
playlist: Playlist,
|
||||||
|
ref: CoverArtRef?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.width(TILE_SIZE).combinedClickable(onClick = onClick, onLongClick = onLongClick),
|
||||||
|
) {
|
||||||
|
CoverArt(
|
||||||
|
ref = ref,
|
||||||
|
name = playlist.name,
|
||||||
|
modifier = Modifier.size(TILE_SIZE).clip(RoundedCornerShape(2.dp)),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
text = playlist.name,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun HomeFeed.isEmpty(): Boolean =
|
private fun HomeFeed.isEmpty(): Boolean =
|
||||||
recentlyPlayedAlbums.isEmpty() &&
|
recentlyPlayedAlbums.isEmpty() &&
|
||||||
recentlyPlayedArtists.isEmpty() &&
|
recentlyPlayedArtists.isEmpty() &&
|
||||||
@@ -400,10 +590,30 @@ private fun LazyListScope.homeSongs(
|
|||||||
onSongPlay: (Song) -> Unit,
|
onSongPlay: (Song) -> Unit,
|
||||||
onLongClick: (MenuTarget) -> Unit,
|
onLongClick: (MenuTarget) -> Unit,
|
||||||
onMore: (HomeSection) -> Unit,
|
onMore: (HomeSection) -> Unit,
|
||||||
|
) = songList(
|
||||||
|
key = section.name,
|
||||||
|
title = section.title,
|
||||||
|
songs = songs,
|
||||||
|
coverArt = coverArt,
|
||||||
|
playingSongId = playingSongId,
|
||||||
|
onSongPlay = onSongPlay,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
onMore = { onMore(section) },
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun LazyListScope.songList(
|
||||||
|
key: String,
|
||||||
|
title: String,
|
||||||
|
songs: List<AlbumSongItem>,
|
||||||
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
|
playingSongId: String?,
|
||||||
|
onSongPlay: (Song) -> Unit,
|
||||||
|
onLongClick: (MenuTarget) -> Unit,
|
||||||
|
onMore: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
if (songs.isEmpty()) return
|
if (songs.isEmpty()) return
|
||||||
item(key = "${section.name}-header") {
|
item(key = "$key-header") {
|
||||||
HomeSectionHeader(section.title) { onMore(section) }
|
HomeSectionHeader(title, onMore)
|
||||||
}
|
}
|
||||||
items(songs, key = { it.song.id }) { item ->
|
items(songs, key = { it.song.id }) { item ->
|
||||||
SongRow(
|
SongRow(
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ package com.subtracks.ui.home
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import androidx.paging.cachedIn
|
||||||
import com.subtracks.data.model.Album
|
import com.subtracks.data.model.Album
|
||||||
import com.subtracks.data.model.AlbumSongItem
|
import com.subtracks.data.model.AlbumSongItem
|
||||||
import com.subtracks.data.model.Artist
|
import com.subtracks.data.model.Artist
|
||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
|
import com.subtracks.data.model.Playlist
|
||||||
import com.subtracks.data.model.Song
|
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.LibraryRepository
|
||||||
import com.subtracks.data.repo.SourceRepository
|
import com.subtracks.data.repo.SourceRepository
|
||||||
import com.subtracks.data.repo.rediscoverCutoff
|
import com.subtracks.data.repo.rediscoverCutoff
|
||||||
@@ -15,6 +22,7 @@ import com.subtracks.data.sync.SyncStatus
|
|||||||
import com.subtracks.playback.PlaybackController
|
import com.subtracks.playback.PlaybackController
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -52,6 +60,52 @@ class HomeViewModel(
|
|||||||
.map { it == SyncStatus.Running }
|
.map { it == SyncStatus.Running }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
||||||
|
|
||||||
|
val offline: StateFlow<Boolean> =
|
||||||
|
sourceRepository.offline.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
||||||
|
|
||||||
|
private val activeSource = libraryRepository.activeSourceId.filterNotNull()
|
||||||
|
|
||||||
|
val downloadedAlbums: Flow<PagingData<Album>> =
|
||||||
|
activeSource
|
||||||
|
.flatMapLatest { id ->
|
||||||
|
libraryRepository.albums(
|
||||||
|
sourceId = id,
|
||||||
|
sort = AlbumSort.Added,
|
||||||
|
descending = false,
|
||||||
|
starred = StarredFilter.Any,
|
||||||
|
search = "",
|
||||||
|
downloaded = true,
|
||||||
|
)
|
||||||
|
}.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
val downloadedArtists: Flow<PagingData<Artist>> =
|
||||||
|
activeSource
|
||||||
|
.flatMapLatest { id ->
|
||||||
|
libraryRepository.artists(
|
||||||
|
sourceId = id,
|
||||||
|
sort = ArtistSort.Name,
|
||||||
|
descending = false,
|
||||||
|
starred = StarredFilter.Any,
|
||||||
|
search = "",
|
||||||
|
downloaded = true,
|
||||||
|
)
|
||||||
|
}.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
val downloadedPlaylists: Flow<PagingData<Playlist>> =
|
||||||
|
activeSource
|
||||||
|
.flatMapLatest { id ->
|
||||||
|
libraryRepository.playlists(
|
||||||
|
sourceId = id,
|
||||||
|
sort = PlaylistSort.Name,
|
||||||
|
descending = false,
|
||||||
|
search = "",
|
||||||
|
downloaded = true,
|
||||||
|
)
|
||||||
|
}.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
val downloadedSongs: Flow<List<AlbumSongItem>> =
|
||||||
|
activeSource.flatMapLatest { libraryRepository.downloadedSongs(it) }
|
||||||
|
|
||||||
val feed: StateFlow<HomeFeed> =
|
val feed: StateFlow<HomeFeed> =
|
||||||
libraryRepository.activeSourceId
|
libraryRepository.activeSourceId
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
@@ -105,6 +159,10 @@ class HomeViewModel(
|
|||||||
viewModelScope.playStarredList(libraryRepository, playbackController, song)
|
viewModelScope.playStarredList(libraryRepository, playbackController, song)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun playDownloaded(song: Song) {
|
||||||
|
viewModelScope.playDownloadedList(libraryRepository, playbackController, song)
|
||||||
|
}
|
||||||
|
|
||||||
fun sync() = syncManager.requestSync()
|
fun sync() = syncManager.requestSync()
|
||||||
|
|
||||||
private data class PlayRows(
|
private data class PlayRows(
|
||||||
@@ -185,3 +243,14 @@ internal fun CoroutineScope.playGenreList(
|
|||||||
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun CoroutineScope.playDownloadedList(
|
||||||
|
libraryRepository: LibraryRepository,
|
||||||
|
playbackController: PlaybackController,
|
||||||
|
song: Song,
|
||||||
|
) {
|
||||||
|
launch {
|
||||||
|
val ids = libraryRepository.downloadedSongIds(song.sourceId)
|
||||||
|
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ fun LibraryRoute(
|
|||||||
HomeRoute(
|
HomeRoute(
|
||||||
onAlbumClick = onAlbumClick,
|
onAlbumClick = onAlbumClick,
|
||||||
onArtistClick = onArtistClick,
|
onArtistClick = onArtistClick,
|
||||||
|
onPlaylistClick = onPlaylistClick,
|
||||||
onViewAlbum = onViewAlbum,
|
onViewAlbum = onViewAlbum,
|
||||||
onViewArtist = onViewArtist,
|
onViewArtist = onViewArtist,
|
||||||
onMore = onHomeMore,
|
onMore = onHomeMore,
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import androidx.test.core.app.ApplicationProvider
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.subtracks.data.model.Album
|
import com.subtracks.data.model.Album
|
||||||
import com.subtracks.data.model.Artist
|
import com.subtracks.data.model.Artist
|
||||||
|
import com.subtracks.data.model.DownloadStatus
|
||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
|
import com.subtracks.data.model.SongDownload
|
||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -163,6 +165,30 @@ class LibraryHomeDaoTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun downloadedSongsOnlyIncludeCompletedDownloadsWithCoverArt() =
|
||||||
|
runTest {
|
||||||
|
val sourceId = source()
|
||||||
|
val dao = db.libraryDao()
|
||||||
|
dao.upsertAlbums(listOf(album(sourceId, "al1", "Album", coverArt = "cover-1")))
|
||||||
|
dao.upsertSongs(
|
||||||
|
listOf(
|
||||||
|
song(sourceId, "s1", "One"),
|
||||||
|
song(sourceId, "s2", "Two"),
|
||||||
|
song(sourceId, "s3", "Three"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val downloads = db.downloadDao()
|
||||||
|
downloads.upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed))
|
||||||
|
downloads.upsert(SongDownload(sourceId, "s2", DownloadStatus.Queued))
|
||||||
|
downloads.upsert(SongDownload(sourceId, "s3", DownloadStatus.Completed))
|
||||||
|
|
||||||
|
val downloaded = dao.downloadedSongs(sourceId).first()
|
||||||
|
assertEquals(listOf("s1", "s3"), downloaded.map { it.song.id })
|
||||||
|
assertEquals("cover-1", downloaded.first().coverArt)
|
||||||
|
assertEquals(listOf("s1", "s3"), dao.downloadedSongIds(sourceId))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun songsByGenreMatchesTheGenreRelationsWithFallback() =
|
fun songsByGenreMatchesTheGenreRelationsWithFallback() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import com.github.takahirom.roborazzi.captureRoboImage
|
|||||||
import com.subtracks.data.model.Album
|
import com.subtracks.data.model.Album
|
||||||
import com.subtracks.data.model.AlbumSongItem
|
import com.subtracks.data.model.AlbumSongItem
|
||||||
import com.subtracks.data.model.Artist
|
import com.subtracks.data.model.Artist
|
||||||
|
import com.subtracks.data.model.Playlist
|
||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
import com.subtracks.ui.theme.SubtracksTheme
|
import com.subtracks.ui.theme.SubtracksTheme
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
@@ -143,6 +144,28 @@ class HomeScreenshotTest {
|
|||||||
played = played,
|
played = played,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun homeOffline() {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
|
OfflineHomeScreen(
|
||||||
|
albums = remember { flowOf(PagingData.from(listOf(listAlbum("Offline Album", 0)))) }.collectAsLazyPagingItems(),
|
||||||
|
artists = remember { flowOf(PagingData.from(listOf(listArtist("Offline Artist", 0)))) }.collectAsLazyPagingItems(),
|
||||||
|
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(),
|
||||||
|
songs = songs(),
|
||||||
|
coverArt = { _, _ -> null },
|
||||||
|
playingSongId = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composeRule.waitUntil(timeoutMillis = 5_000) {
|
||||||
|
composeRule.onAllNodesWithText("Downloaded albums").fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
|
composeRule.onRoot().captureRoboImage("src/test/screenshots/home_offline.png")
|
||||||
|
}
|
||||||
|
|
||||||
private fun listAlbum(
|
private fun listAlbum(
|
||||||
name: String,
|
name: String,
|
||||||
created: Long,
|
created: Long,
|
||||||
|
|||||||
Reference in New Issue
Block a user