Add favorites, cached built-in playlists (#11)

This commit is contained in:
vfsfitvnm
2022-07-06 20:19:27 +02:00
parent 59b6c61bb2
commit 2e542d3c1a
5 changed files with 347 additions and 28 deletions

View File

@@ -52,10 +52,6 @@ interface Database {
}
}
@Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC")
fun history(): Flow<List<DetailedSong>>
@Transaction
@Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC")
fun favorites(): Flow<List<DetailedSong>>

View File

@@ -0,0 +1,7 @@
package it.vfsfitvnm.vimusic.enums
enum class BuiltInPlaylist {
Favorites,
Cached
}

View File

@@ -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)
}
}
)
}
}
}
}
}

View File

@@ -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)
)
}
}

View File

@@ -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<Uri?> {
@@ -50,11 +52,21 @@ fun rememberArtistRoute(): Route1<String?> {
@Composable
fun rememberLocalPlaylistRoute(): Route1<Long?> {
val playlistType = rememberSaveable {
val playlistId = rememberSaveable {
mutableStateOf<Long?>(null)
}
return remember {
Route1("LocalPlaylistRoute", playlistType)
Route1("LocalPlaylistRoute", playlistId)
}
}
@Composable
fun rememberBuiltInPlaylistRoute(): Route1<BuiltInPlaylist> {
val playlistType = rememberSaveable {
mutableStateOf(BuiltInPlaylist.Favorites)
}
return remember {
Route1("BuiltInPlaylistRoute", playlistType)
}
}