From 2e542d3c1a957a1b7a6338d1b049d4afb369d7bc Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 6 Jul 2022 20:19:27 +0200 Subject: [PATCH] Add favorites, cached built-in playlists (#11) --- .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 4 - .../vimusic/enums/BuiltInPlaylist.kt | 7 + .../ui/screens/BuiltInPlaylistScreen.kt | 242 ++++++++++++++++++ .../vimusic/ui/screens/HomeScreen.kt | 106 ++++++-- .../it/vfsfitvnm/vimusic/ui/screens/routes.kt | 16 +- 5 files changed, 347 insertions(+), 28 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/BuiltInPlaylist.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/BuiltInPlaylistScreen.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 5bc83e2..362cd6d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -52,10 +52,6 @@ interface Database { } } - @Transaction - @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC") - fun history(): Flow> - @Transaction @Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC") fun favorites(): Flow> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/BuiltInPlaylist.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/BuiltInPlaylist.kt new file mode 100644 index 0000000..d920a49 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/BuiltInPlaylist.kt @@ -0,0 +1,7 @@ +package it.vfsfitvnm.vimusic.enums + + +enum class BuiltInPlaylist { + Favorites, + Cached +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/BuiltInPlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/BuiltInPlaylistScreen.kt new file mode 100644 index 0000000..abea773 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/BuiltInPlaylistScreen.kt @@ -0,0 +1,242 @@ +package it.vfsfitvnm.vimusic.ui.screens + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist +import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.ui.components.LocalMenuState +import it.vfsfitvnm.vimusic.ui.components.TopAppBar +import it.vfsfitvnm.vimusic.ui.components.themed.* +import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette +import it.vfsfitvnm.vimusic.ui.styling.LocalTypography +import it.vfsfitvnm.vimusic.ui.views.SongItem +import it.vfsfitvnm.vimusic.utils.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map + + +@ExperimentalAnimationApi +@Composable +fun BuiltInPlaylistScreen( + builtInPlaylist: BuiltInPlaylist, +) { + val lazyListState = rememberLazyListState() + + val albumRoute = rememberAlbumRoute() + val artistRoute = rememberArtistRoute() + + RouteHandler(listenToGlobalEmitter = true) { + albumRoute { browseId -> + AlbumScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } + + artistRoute { browseId -> + ArtistScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } + + host { + val density = LocalDensity.current + val menuState = LocalMenuState.current + + val binder = LocalPlayerServiceBinder.current + val colorPalette = LocalColorPalette.current + val typography = LocalTypography.current + + val thumbnailSize = remember { + density.run { + 54.dp.roundToPx() + } + } + + val songs by remember(binder?.cache, builtInPlaylist) { + when (builtInPlaylist) { + BuiltInPlaylist.Favorites -> Database.favorites() + BuiltInPlaylist.Cached -> Database.songsByRowIdDesc().map { songs -> + songs.filter { song -> + song.song.contentLength?.let { contentLength -> + binder?.cache?.isCached(song.song.id, 0, contentLength) + } ?: false + } + } + } + }.collectAsState(initial = emptyList(), context = Dispatchers.IO) + + LazyColumn( + state = lazyListState, + contentPadding = PaddingValues(bottom = 64.dp), + modifier = Modifier + .background(colorPalette.background) + .fillMaxSize() + ) { + item { + TopAppBar( + modifier = Modifier + .height(52.dp) + ) { + Image( + painter = painterResource(R.drawable.chevron_back), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable(onClick = pop) + .padding(vertical = 8.dp, horizontal = 16.dp) + .size(24.dp) + ) + + Image( + painter = painterResource(R.drawable.ellipsis_horizontal), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + menuState.display { + Menu { + MenuCloseButton(onClick = menuState::hide) + + MenuEntry( + icon = R.drawable.time, + text = "Enqueue", + enabled = songs.isNotEmpty(), + onClick = { + menuState.hide() + binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) + } + ) + } + } + } + .padding(horizontal = 16.dp, vertical = 8.dp) + .size(24.dp) + ) + } + } + + item { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .padding(top = 16.dp, bottom = 32.dp) + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = 16.dp) + ) { + BasicText( + text = when (builtInPlaylist) { + BuiltInPlaylist.Favorites -> "Favorites" + BuiltInPlaylist.Cached -> "Cached" + }, + style = typography.m.semiBold + ) + + BasicText( + text = "${songs.size} songs", + style = typography.xxs.semiBold.secondary + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(horizontal = 16.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning( + songs + .map(DetailedSong::asMediaItem) + .shuffled() + ) + } + .shadow(elevation = 2.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .padding(horizontal = 16.dp, vertical = 16.dp) + .size(20.dp) + ) + + Image( + painter = painterResource(R.drawable.play), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning( + songs.map( + DetailedSong::asMediaItem + ) + ) + } + .shadow(elevation = 2.dp, shape = CircleShape) + .background( + color = colorPalette.elevatedBackground, + shape = CircleShape + ) + .padding(horizontal = 16.dp, vertical = 16.dp) + .size(20.dp) + ) + } + } + } + + itemsIndexed( + items = songs, + key = { _, song -> song.song.id }, + contentType = { _, song -> song }, + ) { index, song -> + SongItem( + song = song, + thumbnailSize = thumbnailSize, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index) + }, + menuContent = { + when (builtInPlaylist) { + BuiltInPlaylist.Favorites -> InFavoritesMediaItemMenu(song = song) + BuiltInPlaylist.Cached -> NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + } + } + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt index 73c4175..a0671f0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt @@ -36,6 +36,7 @@ import it.vfsfitvnm.route.fastFade import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness @@ -68,6 +69,7 @@ fun HomeScreen() { val intentUriRoute = rememberIntentUriRoute() val settingsRoute = rememberSettingsRoute() val playlistRoute = rememberLocalPlaylistRoute() + val builtInPlaylistRoute = rememberBuiltInPlaylistRoute() val searchRoute = rememberSearchRoute() val searchResultRoute = rememberSearchResultRoute() val albumRoute = rememberAlbumRoute() @@ -102,6 +104,12 @@ fun HomeScreen() { ) } + builtInPlaylistRoute { builtInPlaylist -> + BuiltInPlaylistScreen( + builtInPlaylist = builtInPlaylist + ) + } + searchResultRoute { query -> SearchResultScreen( query = query, @@ -232,6 +240,18 @@ fun HomeScreen() { .padding(horizontal = 8.dp) ) + Image( + painter = painterResource(R.drawable.add), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + isCreatingANewPlaylist = true + } + .padding(all = 8.dp) + .size(20.dp) + ) + Image( painter = painterResource(if (isGridExpanded) R.drawable.grid else R.drawable.grid_single), contentDescription = null, @@ -256,32 +276,74 @@ fun HomeScreen() { .height(124.dp * (if (isGridExpanded) 3 else 1)) ) { item { - Column( - horizontalAlignment = Alignment.CenterHorizontally, + Box( modifier = Modifier .padding(all = 8.dp) - .width(108.dp) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() } - ) { - isCreatingANewPlaylist = true + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { + builtInPlaylistRoute(BuiltInPlaylist.Favorites) } - .background(colorPalette.lightBackground) - .size(108.dp) - ) { - Image( - painter = painterResource(R.drawable.add), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .size(24.dp) ) - } + .background(colorPalette.lightBackground) + .size(108.dp) + ) { + Image( + painter = painterResource(R.drawable.heart), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.red), + modifier = Modifier + .align(Alignment.Center) + .size(24.dp) + ) + + BasicText( + text = "Favorites", + style = typography.xxs.semiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomStart) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } + + item { + Box( + modifier = Modifier + .padding(all = 8.dp) + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { + builtInPlaylistRoute(BuiltInPlaylist.Cached) + } + ) + .background(colorPalette.lightBackground) + .size(108.dp) + ) { + Image( + painter = painterResource(R.drawable.download), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.blue), + modifier = Modifier + .align(Alignment.Center) + .size(24.dp) + ) + + BasicText( + text = "Cached", + style = typography.xxs.semiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomStart) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/routes.kt index b705f73..1b1152a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/routes.kt @@ -7,6 +7,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import it.vfsfitvnm.route.Route0 import it.vfsfitvnm.route.Route1 +import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist + @Composable fun rememberIntentUriRoute(): Route1 { @@ -50,11 +52,21 @@ fun rememberArtistRoute(): Route1 { @Composable fun rememberLocalPlaylistRoute(): Route1 { - val playlistType = rememberSaveable { + val playlistId = rememberSaveable { mutableStateOf(null) } return remember { - Route1("LocalPlaylistRoute", playlistType) + Route1("LocalPlaylistRoute", playlistId) + } +} + +@Composable +fun rememberBuiltInPlaylistRoute(): Route1 { + val playlistType = rememberSaveable { + mutableStateOf(BuiltInPlaylist.Favorites) + } + return remember { + Route1("BuiltInPlaylistRoute", playlistType) } }