Add favorites, cached built-in playlists (#11)
This commit is contained in:
@@ -52,10 +52,6 @@ interface Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transaction
|
|
||||||
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC")
|
|
||||||
fun history(): Flow<List<DetailedSong>>
|
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC")
|
@Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC")
|
||||||
fun favorites(): Flow<List<DetailedSong>>
|
fun favorites(): Flow<List<DetailedSong>>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.enums
|
||||||
|
|
||||||
|
|
||||||
|
enum class BuiltInPlaylist {
|
||||||
|
Favorites,
|
||||||
|
Cached
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import it.vfsfitvnm.route.fastFade
|
|||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
@@ -68,6 +69,7 @@ fun HomeScreen() {
|
|||||||
val intentUriRoute = rememberIntentUriRoute()
|
val intentUriRoute = rememberIntentUriRoute()
|
||||||
val settingsRoute = rememberSettingsRoute()
|
val settingsRoute = rememberSettingsRoute()
|
||||||
val playlistRoute = rememberLocalPlaylistRoute()
|
val playlistRoute = rememberLocalPlaylistRoute()
|
||||||
|
val builtInPlaylistRoute = rememberBuiltInPlaylistRoute()
|
||||||
val searchRoute = rememberSearchRoute()
|
val searchRoute = rememberSearchRoute()
|
||||||
val searchResultRoute = rememberSearchResultRoute()
|
val searchResultRoute = rememberSearchResultRoute()
|
||||||
val albumRoute = rememberAlbumRoute()
|
val albumRoute = rememberAlbumRoute()
|
||||||
@@ -102,6 +104,12 @@ fun HomeScreen() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builtInPlaylistRoute { builtInPlaylist ->
|
||||||
|
BuiltInPlaylistScreen(
|
||||||
|
builtInPlaylist = builtInPlaylist
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
searchResultRoute { query ->
|
searchResultRoute { query ->
|
||||||
SearchResultScreen(
|
SearchResultScreen(
|
||||||
query = query,
|
query = query,
|
||||||
@@ -232,6 +240,18 @@ fun HomeScreen() {
|
|||||||
.padding(horizontal = 8.dp)
|
.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(
|
Image(
|
||||||
painter = painterResource(if (isGridExpanded) R.drawable.grid else R.drawable.grid_single),
|
painter = painterResource(if (isGridExpanded) R.drawable.grid else R.drawable.grid_single),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -256,32 +276,74 @@ fun HomeScreen() {
|
|||||||
.height(124.dp * (if (isGridExpanded) 3 else 1))
|
.height(124.dp * (if (isGridExpanded) 3 else 1))
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
Column(
|
Box(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(all = 8.dp)
|
.padding(all = 8.dp)
|
||||||
.width(108.dp)
|
.clickable(
|
||||||
) {
|
indication = rememberRipple(bounded = true),
|
||||||
Box(
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
contentAlignment = Alignment.Center,
|
onClick = {
|
||||||
modifier = Modifier
|
builtInPlaylistRoute(BuiltInPlaylist.Favorites)
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
isCreatingANewPlaylist = true
|
|
||||||
}
|
}
|
||||||
.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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import it.vfsfitvnm.route.Route0
|
import it.vfsfitvnm.route.Route0
|
||||||
import it.vfsfitvnm.route.Route1
|
import it.vfsfitvnm.route.Route1
|
||||||
|
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberIntentUriRoute(): Route1<Uri?> {
|
fun rememberIntentUriRoute(): Route1<Uri?> {
|
||||||
@@ -50,11 +52,21 @@ fun rememberArtistRoute(): Route1<String?> {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberLocalPlaylistRoute(): Route1<Long?> {
|
fun rememberLocalPlaylistRoute(): Route1<Long?> {
|
||||||
val playlistType = rememberSaveable {
|
val playlistId = rememberSaveable {
|
||||||
mutableStateOf<Long?>(null)
|
mutableStateOf<Long?>(null)
|
||||||
}
|
}
|
||||||
return remember {
|
return remember {
|
||||||
Route1("LocalPlaylistRoute", playlistType)
|
Route1("LocalPlaylistRoute", playlistId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberBuiltInPlaylistRoute(): Route1<BuiltInPlaylist> {
|
||||||
|
val playlistType = rememberSaveable {
|
||||||
|
mutableStateOf(BuiltInPlaylist.Favorites)
|
||||||
|
}
|
||||||
|
return remember {
|
||||||
|
Route1("BuiltInPlaylistRoute", playlistType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user