Improve sorting options for songs and playlists (#104)

This commit is contained in:
vfsfitvnm
2022-07-19 11:16:00 +02:00
parent 3c7d3da639
commit 55e436208f
9 changed files with 272 additions and 123 deletions

View File

@@ -12,10 +12,12 @@ import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteDatabase
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@Dao
@@ -30,6 +32,14 @@ interface Database {
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC")
fun songsByRowIdDesc(): Flow<List<DetailedSong>>
@Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY title ASC")
fun songsByTitleAsc(): Flow<List<DetailedSong>>
@Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY title DESC")
fun songsByTitleDesc(): Flow<List<DetailedSong>>
@Transaction
@Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY totalPlayTimeMs ASC")
fun songsByPlayTimeAsc(): Flow<List<DetailedSong>>
@@ -44,6 +54,10 @@ interface Database {
SortOrder.Ascending -> songsByPlayTimeAsc()
SortOrder.Descending -> songsByPlayTimeDesc()
}
SongSortBy.Title -> when (sortOrder) {
SortOrder.Ascending -> songsByTitleAsc()
SortOrder.Descending -> songsByTitleDesc()
}
SongSortBy.DateAdded -> when (sortOrder) {
SortOrder.Ascending -> songsByRowIdAsc()
SortOrder.Descending -> songsByRowIdDesc()
@@ -81,8 +95,29 @@ interface Database {
fun playlistWithSongs(id: Long): Flow<PlaylistWithSongs?>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist")
fun playlistPreviews(): Flow<List<PlaylistPreview>>
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY name ASC")
fun playlistPreviewsByName(): Flow<List<PlaylistPreview>>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY ROWID ASC")
fun playlistPreviewsByDateAdded(): Flow<List<PlaylistPreview>>
@Transaction
@Query("SELECT id, name, (SELECT COUNT(*) FROM SongPlaylistMap WHERE playlistId = id) as songCount FROM Playlist ORDER BY songCount ASC")
fun playlistPreviewsByDateSongCount(): Flow<List<PlaylistPreview>>
fun playlistPreviews(sortBy: PlaylistSortBy, sortOrder: SortOrder): Flow<List<PlaylistPreview>> {
return when (sortBy) {
PlaylistSortBy.Name -> playlistPreviewsByName()
PlaylistSortBy.DateAdded -> playlistPreviewsByDateAdded()
PlaylistSortBy.SongCount -> playlistPreviewsByDateSongCount()
}.map {
when (sortOrder) {
SortOrder.Ascending -> it
SortOrder.Descending -> it.reversed()
}
}
}
@Query("SELECT thumbnailUrl FROM Song JOIN SongPlaylistMap ON id = songId WHERE playlistId = :id ORDER BY position LIMIT 4")
fun playlistThumbnailUrls(id: Long): Flow<List<String?>>

View File

@@ -0,0 +1,7 @@
package it.vfsfitvnm.vimusic.enums
enum class PlaylistSortBy {
Name,
DateAdded,
SongCount
}

View File

@@ -2,5 +2,6 @@ package it.vfsfitvnm.vimusic.enums
enum class SongSortBy {
PlayTime,
Title,
DateAdded
}

View File

@@ -0,0 +1,99 @@
package it.vfsfitvnm.vimusic.ui.components.themed
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.medium
@Composable
fun DropDownSection(content: @Composable ColumnScope.() -> Unit) {
val (colorPalette) = LocalAppearance.current
Column(
modifier = Modifier
.shadow(
elevation = 2.dp,
shape = RoundedCornerShape(16.dp)
)
.background(colorPalette.elevatedBackground)
.width(IntrinsicSize.Max),
content = content
)
}
@Composable
fun DropDownSectionSpacer() {
Spacer(
modifier = Modifier
.height(4.dp)
)
}
@Composable
fun DropDownTextItem(
text: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val (colorPalette) = LocalAppearance.current
DropDownTextItem(
text = text,
textColor = if (isSelected) {
colorPalette.onPrimaryContainer
} else {
colorPalette.textSecondary
},
backgroundColor = if (isSelected) {
colorPalette.primaryContainer
} else {
colorPalette.elevatedBackground
},
onClick = onClick
)
}
@Composable
fun DropDownTextItem(
text: String,
backgroundColor: Color? = null,
textColor: Color? = null,
onClick: () -> Unit
) {
val (colorPalette, typography) = LocalAppearance.current
BasicText(
text = text,
style = typography.xxs.medium.copy(
color = textColor ?: colorPalette.text,
letterSpacing = 1.sp
),
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = onClick
)
.background(backgroundColor ?: colorPalette.elevatedBackground)
.fillMaxWidth()
.widthIn(min = 124.dp, max = 248.dp)
.padding(
horizontal = 16.dp,
vertical = 8.dp
)
)
}

View File

@@ -22,6 +22,8 @@ import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.route.empty
import it.vfsfitvnm.vimusic.*
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
@@ -276,7 +278,7 @@ fun MediaItemMenu(
onGlobalRouteEmitted: (() -> Unit)? = null,
) {
val playlistPreviews by remember {
Database.playlistPreviews()
Database.playlistPreviews(PlaylistSortBy.DateAdded, SortOrder.Descending)
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
val viewPlaylistsRoute = rememberCreatePlaylistRoute()

View File

@@ -49,7 +49,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
@ExperimentalFoundationApi
@OptIn(ExperimentalFoundationApi::class)
@ExperimentalAnimationApi
@Composable
fun ArtistScreen(

View File

@@ -12,18 +12,16 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@@ -32,25 +30,21 @@ import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
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.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.enums.*
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.DropdownMenu
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
import it.vfsfitvnm.vimusic.ui.styling.*
import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.*
@@ -64,6 +58,7 @@ fun HomeScreen() {
val (colorPalette, typography) = LocalAppearance.current
val lazyListState = rememberLazyListState()
val lazyHorizontalGridState = rememberLazyGridState()
val intentUriRoute = rememberIntentUriRoute()
val settingsRoute = rememberSettingsRoute()
@@ -74,8 +69,12 @@ fun HomeScreen() {
val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute()
val playlistPreviews by remember {
Database.playlistPreviews()
var playlistSortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded)
var playlistSortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending)
var playlistGridExpanded by rememberPreference(playlistGridExpandedKey, false)
val playlistPreviews by remember(playlistSortBy, playlistSortOrder) {
Database.playlistPreviews(playlistSortBy, playlistSortOrder)
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
var songSortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded)
@@ -151,14 +150,9 @@ fun HomeScreen() {
val binder = LocalPlayerServiceBinder.current
val isFirstLaunch by rememberPreference(isFirstLaunchKey, true)
val isCachedPlaylistShown by rememberPreference(isCachedPlaylistShownKey, false)
val thumbnailSize = Dimensions.thumbnails.song.px
var isGridExpanded by remember {
mutableStateOf(false)
}
var isCreatingANewPlaylist by rememberSaveable {
mutableStateOf(false)
}
@@ -262,28 +256,100 @@ fun HomeScreen() {
.size(20.dp)
)
Image(
painter = painterResource(if (isGridExpanded) R.drawable.grid else R.drawable.grid_single),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
modifier = Modifier
.clickable {
isGridExpanded = !isGridExpanded
Box {
var isSortMenuDisplayed by remember {
mutableStateOf(false)
}
Image(
painter = painterResource(R.drawable.sort),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
isSortMenuDisplayed = true
}
.padding(horizontal = 8.dp, vertical = 8.dp)
.size(20.dp)
)
DropdownMenu(
isDisplayed = isSortMenuDisplayed,
onDismissRequest = {
isSortMenuDisplayed = false
}
.padding(all = 10.dp)
.size(16.dp)
)
) {
DropDownSection {
DropDownTextItem(
text = "NAME",
isSelected = playlistSortBy == PlaylistSortBy.Name,
onClick = {
isSortMenuDisplayed = false
playlistSortBy = PlaylistSortBy.Name
}
)
DropDownTextItem(
text = "DATE ADDED",
isSelected = playlistSortBy == PlaylistSortBy.DateAdded,
onClick = {
isSortMenuDisplayed = false
playlistSortBy = PlaylistSortBy.DateAdded
}
)
DropDownTextItem(
text = "SONG COUNT",
isSelected = playlistSortBy == PlaylistSortBy.SongCount,
onClick = {
isSortMenuDisplayed = false
playlistSortBy = PlaylistSortBy.SongCount
}
)
}
DropDownSectionSpacer()
DropDownSection {
DropDownTextItem(
text = when (playlistSortOrder) {
SortOrder.Ascending -> "ASCENDING"
SortOrder.Descending -> "DESCENDING"
},
onClick = {
isSortMenuDisplayed = false
playlistSortOrder = !playlistSortOrder
}
)
}
DropDownSectionSpacer()
DropDownSection {
DropDownTextItem(
text = when (playlistGridExpanded) {
true -> "EXPAND"
false -> "COMPACT"
},
onClick = {
isSortMenuDisplayed = false
playlistGridExpanded = !playlistGridExpanded
}
)
}
}
}
}
}
item {
LazyHorizontalGrid(
rows = GridCells.Fixed(if (isGridExpanded) 3 else 1),
state = lazyHorizontalGridState,
rows = GridCells.Fixed(if (playlistGridExpanded) 3 else 1),
contentPadding = PaddingValues(horizontal = 16.dp),
modifier = Modifier
.animateContentSize()
.fillMaxWidth()
.height(124.dp * (if (isGridExpanded) 3 else 1))
.height(124.dp * (if (playlistGridExpanded) 3 else 1))
) {
item {
Box(
@@ -321,7 +387,7 @@ fun HomeScreen() {
}
}
if (isCachedPlaylistShown) {
if (playlistGridExpanded) {
item {
Box(
modifier = Modifier
@@ -361,6 +427,7 @@ fun HomeScreen() {
items(
items = playlistPreviews,
key = { it.playlist.id },
contentType = { it }
) { playlistPreview ->
PlaylistPreviewItem(
@@ -370,10 +437,9 @@ fun HomeScreen() {
.padding(all = 8.dp)
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() }
) {
playlistRoute(playlistPreview.playlist.id)
}
interactionSource = remember { MutableInteractionSource() },
onClick = { playlistRoute(playlistPreview.playlist.id) }
)
)
}
}
@@ -436,96 +502,45 @@ fun HomeScreen() {
isSortMenuDisplayed = false
}
) {
@Composable
fun Item(
text: String,
textColor: Color,
backgroundColor: Color,
onClick: () -> Unit
) {
BasicText(
text = text,
style = typography.xxs.medium.copy(color = textColor, letterSpacing = 1.sp),
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = {
isSortMenuDisplayed = false
onClick()
}
)
.background(backgroundColor)
.fillMaxWidth()
.widthIn(min = 124.dp, max = 248.dp)
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
@Composable
fun Item(
text: String,
isSelected: Boolean,
onClick: () -> Unit
) {
Item(
text = text,
textColor = if (isSelected) {
colorPalette.onPrimaryContainer
} else {
colorPalette.textSecondary
},
backgroundColor = if (isSelected) {
colorPalette.primaryContainer
} else {
colorPalette.elevatedBackground
},
onClick = onClick
)
}
Column(
modifier = Modifier
.shadow(elevation = 2.dp, shape = RoundedCornerShape(16.dp))
.background(colorPalette.elevatedBackground)
.width(IntrinsicSize.Max),
) {
Item(
DropDownSection {
DropDownTextItem(
text = "PLAY TIME",
isSelected = songSortBy == SongSortBy.PlayTime,
onClick = {
isSortMenuDisplayed = false
songSortBy = SongSortBy.PlayTime
}
)
Item(
DropDownTextItem(
text = "TITLE",
isSelected = songSortBy == SongSortBy.Title,
onClick = {
isSortMenuDisplayed = false
songSortBy = SongSortBy.Title
}
)
DropDownTextItem(
text = "DATE ADDED",
isSelected = songSortBy == SongSortBy.DateAdded,
onClick = {
isSortMenuDisplayed = false
songSortBy = SongSortBy.DateAdded
}
)
}
Spacer(
modifier = Modifier
.height(4.dp)
)
DropDownSectionSpacer()
Column(
modifier = Modifier
.shadow(elevation = 2.dp, shape = RoundedCornerShape(16.dp))
.background(colorPalette.elevatedBackground)
.width(IntrinsicSize.Max),
) {
Item(
DropDownSection {
DropDownTextItem(
text = when (songSortOrder) {
SortOrder.Ascending -> "ASCENDING"
SortOrder.Descending -> "DESCENDING"
},
textColor = colorPalette.text,
backgroundColor = colorPalette.elevatedBackground,
onClick = {
isSortMenuDisplayed = false
songSortOrder = !songSortOrder
}
)

View File

@@ -46,7 +46,6 @@ fun AppearanceSettingsScreen() {
var colorPaletteMode by rememberPreference(colorPaletteModeKey, ColorPaletteMode.System)
var thumbnailRoundness by rememberPreference(thumbnailRoundnessKey, ThumbnailRoundness.Light)
var isCachedPlaylistShown by rememberPreference(isCachedPlaylistShownKey, false)
Column(
modifier = Modifier
@@ -100,17 +99,6 @@ fun AppearanceSettingsScreen() {
thumbnailRoundness = it
}
)
SettingsEntryGroupText(title = "OTHER")
SwitchSettingEntry(
title = "Cached playlist",
text = "Display a playlist whose songs can be played offline",
isChecked = isCachedPlaylistShown,
onCheckedChange = {
isCachedPlaylistShown = it
}
)
}
}
}

View File

@@ -9,13 +9,15 @@ import androidx.core.content.edit
const val colorPaletteModeKey = "colorPaletteMode"
const val thumbnailRoundnessKey = "thumbnailRoundness"
const val isCachedPlaylistShownKey = "isCachedPlaylistShown"
const val coilDiskCacheMaxSizeKey = "coilDiskCacheMaxSize"
const val exoPlayerDiskCacheMaxSizeKey = "exoPlayerDiskCacheMaxSize"
const val isInvincibilityEnabledKey = "isInvincibilityEnabled"
const val isFirstLaunchKey = "isFirstLaunch"
const val songSortOrderKey = "songSortOrder"
const val songSortByKey = "songSortBy"
const val playlistSortOrderKey = "playlistSortOrder"
const val playlistSortByKey = "playlistSortBy"
const val playlistGridExpandedKey = "playlistGridExpanded"
const val searchFilterKey = "searchFilter"
const val repeatModeKey = "repeatMode"
const val skipSilenceKey = "skipSilence"