Browse Source

Drop home page song collections in favor of a sort feature (#11)

vfsfitvnm 3 năm trước cách đây
mục cha
commit
59b6c61bb2

+ 32 - 5
app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt

@@ -8,9 +8,11 @@ import android.os.Parcel
 import androidx.media3.common.MediaItem
 import androidx.room.*
 import androidx.room.migration.AutoMigrationSpec
-import androidx.sqlite.db.SimpleSQLiteQuery
 import androidx.room.migration.Migration
+import androidx.sqlite.db.SimpleSQLiteQuery
 import androidx.sqlite.db.SupportSQLiteDatabase
+import it.vfsfitvnm.vimusic.enums.SongSortBy
+import it.vfsfitvnm.vimusic.enums.SortOrder
 import it.vfsfitvnm.vimusic.models.*
 import it.vfsfitvnm.vimusic.utils.getFloatOrNull
 import it.vfsfitvnm.vimusic.utils.getLongOrNull
@@ -21,6 +23,35 @@ import kotlinx.coroutines.flow.Flow
 interface Database {
     companion object : Database by DatabaseInitializer.Instance.database
 
+    @Transaction
+    @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID ASC")
+    fun songsByRowIdAsc(): Flow<List<DetailedSong>>
+
+    @Transaction
+    @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 totalPlayTimeMs ASC")
+    fun songsByPlayTimeAsc(): Flow<List<DetailedSong>>
+
+    @Transaction
+    @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY totalPlayTimeMs DESC")
+    fun songsByPlayTimeDesc(): Flow<List<DetailedSong>>
+
+    fun songs(sortBy: SongSortBy, sortOrder: SortOrder): Flow<List<DetailedSong>> {
+        return when (sortBy) {
+            SongSortBy.PlayTime -> when (sortOrder) {
+                SortOrder.Ascending -> songsByPlayTimeAsc()
+                SortOrder.Descending -> songsByPlayTimeDesc()
+            }
+            SongSortBy.DateAdded -> when (sortOrder) {
+                SortOrder.Ascending -> songsByRowIdAsc()
+                SortOrder.Descending -> songsByRowIdDesc()
+            }
+        }
+    }
+
     @Transaction
     @Query("SELECT * FROM Song WHERE totalPlayTimeMs > 0 ORDER BY ROWID DESC")
     fun history(): Flow<List<DetailedSong>>
@@ -29,10 +60,6 @@ interface Database {
     @Query("SELECT * FROM Song WHERE likedAt IS NOT NULL ORDER BY likedAt DESC")
     fun favorites(): Flow<List<DetailedSong>>
 
-    @Transaction
-    @Query("SELECT * FROM Song WHERE totalPlayTimeMs >= 60000 ORDER BY totalPlayTimeMs DESC LIMIT 20")
-    fun mostPlayed(): Flow<List<DetailedSong>>
-
     @Query("SELECT * FROM QueuedMediaItem")
     fun queue(): List<QueuedMediaItem>
 

+ 0 - 7
app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/SongCollection.kt

@@ -1,7 +0,0 @@
-package it.vfsfitvnm.vimusic.enums
-
-enum class SongCollection {
-    MostPlayed,
-    Favorites,
-    History
-}

+ 6 - 0
app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/SongSortBy.kt

@@ -0,0 +1,6 @@
+package it.vfsfitvnm.vimusic.enums
+
+enum class SongSortBy {
+    PlayTime,
+    DateAdded
+}

+ 11 - 0
app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/SortOrder.kt

@@ -0,0 +1,11 @@
+package it.vfsfitvnm.vimusic.enums
+
+enum class SortOrder {
+    Ascending,
+    Descending;
+
+    operator fun not() = when (this) {
+        Ascending -> Descending
+        Descending -> Ascending
+    }
+}

+ 191 - 0
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropdownMenu.kt

@@ -0,0 +1,191 @@
+package it.vfsfitvnm.vimusic.ui.components.themed
+
+import androidx.compose.animation.core.*
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.*
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
+import kotlin.math.max
+import kotlin.math.min
+
+
+@Composable
+fun DropdownMenu(
+    isDisplayed: Boolean,
+    onDismissRequest: () -> Unit,
+    modifier: Modifier = Modifier,
+    offset: DpOffset = DpOffset(0.dp, 0.dp),
+    properties: PopupProperties = PopupProperties(focusable = true),
+    content: @Composable ColumnScope.() -> Unit
+) {
+    val expandedStates = remember {
+        MutableTransitionState(false)
+    }.apply { targetState = isDisplayed }
+
+    if (expandedStates.currentState || expandedStates.targetState) {
+        val density = LocalDensity.current
+
+        var transformOrigin by remember {
+            mutableStateOf(TransformOrigin.Center)
+        }
+
+        val popupPositionProvider =
+            DropdownMenuPositionProvider(offset, density) { parentBounds, menuBounds ->
+                transformOrigin = calculateTransformOrigin(parentBounds, menuBounds)
+            }
+
+        Popup(
+            onDismissRequest = onDismissRequest,
+            popupPositionProvider = popupPositionProvider,
+            properties = properties
+        ) {
+            DropdownMenuContent(
+                expandedStates = expandedStates,
+                transformOrigin = transformOrigin,
+                modifier = modifier,
+                content = content
+            )
+        }
+    }
+}
+
+@Composable
+internal fun DropdownMenuContent(
+    expandedStates: MutableTransitionState<Boolean>,
+    transformOrigin: TransformOrigin,
+    modifier: Modifier = Modifier,
+    content: @Composable ColumnScope.() -> Unit
+) {
+    val transition = updateTransition(expandedStates, "DropDownMenu")
+
+    val scale by transition.animateFloat(
+        transitionSpec = {
+            if (false isTransitioningTo true) {
+                // Dismissed to expanded
+                tween(
+                    durationMillis = 128,
+                    easing = LinearOutSlowInEasing
+                )
+            } else {
+                // Expanded to dismissed.
+                tween(
+                    durationMillis = 64,
+                    delayMillis = 64
+                )
+            }
+        }, label = ""
+    ) { isDisplayed ->
+        if (isDisplayed) 1f else 0.9f
+    }
+
+    Column(
+        modifier = modifier
+            .graphicsLayer {
+                scaleX = scale
+                scaleY = scale
+                this.transformOrigin = transformOrigin
+            },
+        content = content,
+    )
+}
+
+@Immutable
+private data class DropdownMenuPositionProvider(
+    val contentOffset: DpOffset,
+    val density: Density,
+    val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }
+) : PopupPositionProvider {
+    override fun calculatePosition(
+        anchorBounds: IntRect,
+        windowSize: IntSize,
+        layoutDirection: LayoutDirection,
+        popupContentSize: IntSize
+    ): IntOffset {
+        // The min margin above and below the menu, relative to the screen.
+        val verticalMargin = with(density) { 48.dp.roundToPx() }
+        // The content offset specified using the dropdown offset parameter.
+        val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
+        val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
+
+        // Compute horizontal position.
+        val toRight = anchorBounds.left + contentOffsetX
+        val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width
+        val toDisplayRight = windowSize.width - popupContentSize.width
+        val toDisplayLeft = 0
+        val x = if (layoutDirection == LayoutDirection.Ltr) {
+            sequenceOf(
+                toRight,
+                toLeft,
+                // If the anchor gets outside of the window on the left, we want to position
+                // toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight.
+                if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft
+            )
+        } else {
+            sequenceOf(
+                toLeft,
+                toRight,
+                // If the anchor gets outside of the window on the right, we want to position
+                // toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft.
+                if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight
+            )
+        }.firstOrNull {
+            it >= 0 && it + popupContentSize.width <= windowSize.width
+        } ?: toLeft
+
+        // Compute vertical position.
+        val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
+        val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
+        val toCenter = anchorBounds.top - popupContentSize.height / 2
+        val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin
+        val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull {
+            it >= verticalMargin &&
+                    it + popupContentSize.height <= windowSize.height - verticalMargin
+        } ?: toTop
+
+        onPositionCalculated(
+            anchorBounds,
+            IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height)
+        )
+        return IntOffset(x, y)
+    }
+}
+
+fun calculateTransformOrigin(
+    parentBounds: IntRect,
+    menuBounds: IntRect
+): TransformOrigin {
+    val pivotX = when {
+        menuBounds.left >= parentBounds.right -> 0f
+        menuBounds.right <= parentBounds.left -> 1f
+        menuBounds.width == 0 -> 0f
+        else -> {
+            val intersectionCenter =
+                (
+                        max(parentBounds.left, menuBounds.left) +
+                                min(parentBounds.right, menuBounds.right)
+                        ) / 2
+            (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
+        }
+    }
+    val pivotY = when {
+        menuBounds.top >= parentBounds.bottom -> 0f
+        menuBounds.bottom <= parentBounds.top -> 1f
+        menuBounds.height == 0 -> 0f
+        else -> {
+            val intersectionCenter =
+                (
+                        max(parentBounds.top, menuBounds.top) +
+                                min(parentBounds.bottom, menuBounds.bottom)
+                        ) / 2
+            (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
+        }
+    }
+    return TransformOrigin(pivotX, pivotY)
+}

+ 137 - 53
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt

@@ -14,12 +14,14 @@ import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
 import androidx.compose.foundation.lazy.grid.items
 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.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorFilter
@@ -27,20 +29,22 @@ import androidx.compose.ui.platform.LocalDensity
 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.route.fastFade
 import it.vfsfitvnm.vimusic.Database
 import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
 import it.vfsfitvnm.vimusic.R
-import it.vfsfitvnm.vimusic.enums.SongCollection
+import it.vfsfitvnm.vimusic.enums.SongSortBy
+import it.vfsfitvnm.vimusic.enums.SortOrder
 import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
+import it.vfsfitvnm.vimusic.models.DetailedSong
 import it.vfsfitvnm.vimusic.models.Playlist
 import it.vfsfitvnm.vimusic.models.SearchQuery
-import it.vfsfitvnm.vimusic.models.DetailedSong
 import it.vfsfitvnm.vimusic.query
 import it.vfsfitvnm.vimusic.ui.components.TopAppBar
-import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu
+import it.vfsfitvnm.vimusic.ui.components.themed.DropdownMenu
 import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
 import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
 import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
@@ -75,12 +79,8 @@ fun HomeScreen() {
 
     val preferences = LocalPreferences.current
 
-    val songCollection by remember(preferences.homePageSongCollection) {
-        when (preferences.homePageSongCollection) {
-            SongCollection.MostPlayed -> Database.mostPlayed()
-            SongCollection.Favorites -> Database.favorites()
-            SongCollection.History -> Database.history()
-        }
+    val songCollection by remember(preferences.songSortBy, preferences.songSortOrder) {
+        Database.songs(preferences.songSortBy, preferences.songSortOrder)
     }.collectAsState(initial = emptyList(), context = Dispatchers.IO)
 
     RouteHandler(
@@ -313,48 +313,13 @@ fun HomeScreen() {
                             .padding(horizontal = 8.dp)
                             .padding(top = 32.dp)
                     ) {
-                        Row(
-                            verticalAlignment = Alignment.Bottom,
+                        BasicText(
+                            text = "Songs",
+                            style = typography.m.semiBold,
                             modifier = Modifier
                                 .weight(1f)
                                 .padding(horizontal = 8.dp)
-                        ) {
-                            BasicText(
-                                text = when (preferences.homePageSongCollection) {
-                                    SongCollection.MostPlayed -> "Most played"
-                                    SongCollection.Favorites -> "Favorites"
-                                    SongCollection.History -> "History"
-                                },
-                                style = typography.m.semiBold,
-                                modifier = Modifier
-                                    .alignByBaseline()
-                                    .animateContentSize()
-                            )
-
-                            val songCollections = enumValues<SongCollection>()
-                            val nextSongCollection =
-                                songCollections[(preferences.homePageSongCollection.ordinal + 1) % songCollections.size]
-
-                            BasicText(
-                                text = when (nextSongCollection) {
-                                    SongCollection.MostPlayed -> "Most played"
-                                    SongCollection.Favorites -> "Favorites"
-                                    SongCollection.History -> "History"
-                                },
-                                style = typography.xxs.secondary.bold,
-                                modifier = Modifier
-                                    .clickable(
-                                        indication = rememberRipple(bounded = true),
-                                        interactionSource = remember { MutableInteractionSource() },
-                                        onClick = {
-                                            preferences.homePageSongCollection = nextSongCollection
-                                        }
-                                    )
-                                    .alignByBaseline()
-                                    .padding(horizontal = 16.dp)
-                                    .animateContentSize()
-                            )
-                        }
+                        )
 
                         Image(
                             painter = painterResource(R.drawable.shuffle),
@@ -372,6 +337,126 @@ fun HomeScreen() {
                                 .padding(horizontal = 8.dp, vertical = 8.dp)
                                 .size(20.dp)
                         )
+
+                        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
+                                }
+                            ) {
+                                @Composable
+                                fun Item(
+                                    text: String,
+                                    textColor: Color,
+                                    backgroundColor: Color,
+                                    onClick: () -> Unit
+                                ) {
+                                    BasicText(
+                                        text = text,
+                                        style = typography.xxs.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
+                                        .clip(RoundedCornerShape(16.dp))
+                                        .background(colorPalette.elevatedBackground)
+                                        .width(IntrinsicSize.Max),
+                                ) {
+                                    Item(
+                                        text = "PLAY TIME",
+                                        isSelected = preferences.songSortBy == SongSortBy.PlayTime,
+                                        onClick = {
+                                            preferences.songSortBy = SongSortBy.PlayTime
+                                        }
+                                    )
+                                    Item(
+                                        text = "DATE ADDED",
+                                        isSelected = preferences.songSortBy == SongSortBy.DateAdded,
+                                        onClick = {
+                                            preferences.songSortBy = SongSortBy.DateAdded
+                                        }
+                                    )
+                                }
+
+                                Spacer(
+                                    modifier = Modifier
+                                        .height(4.dp)
+                                )
+
+                                Column(
+                                    modifier = Modifier
+                                        .clip(RoundedCornerShape(16.dp))
+                                        .background(colorPalette.elevatedBackground)
+                                        .width(IntrinsicSize.Max),
+                                ) {
+                                    Item(
+                                        text = when (preferences.songSortOrder) {
+                                            SortOrder.Ascending -> "ASCENDING"
+                                            SortOrder.Descending -> "DESCENDING"
+                                        },
+                                        textColor = colorPalette.text,
+                                        backgroundColor = colorPalette.elevatedBackground,
+                                        onClick = {
+                                            preferences.songSortOrder = !preferences.songSortOrder
+                                        }
+                                    )
+                                }
+                            }
+                        }
                     }
                 }
 
@@ -393,15 +478,14 @@ fun HomeScreen() {
                             )
                         },
                         menuContent = {
-                            when (preferences.homePageSongCollection) {
-                                SongCollection.MostPlayed -> NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
-                                SongCollection.Favorites -> InFavoritesMediaItemMenu(song = song)
-                                SongCollection.History -> InHistoryMediaItemMenu(song = song)
+                            when (preferences.songSortBy) {
+                                SongSortBy.PlayTime -> NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
+                                SongSortBy.DateAdded -> InHistoryMediaItemMenu(song = song)
                             }
                         },
                         onThumbnailContent = {
                             AnimatedVisibility(
-                                visible = preferences.homePageSongCollection == SongCollection.MostPlayed,
+                                visible = preferences.songSortBy == SongSortBy.PlayTime,
                                 enter = fadeIn(),
                                 exit = fadeOut(),
                                 modifier = Modifier

+ 13 - 9
app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt

@@ -6,19 +6,18 @@ import androidx.compose.runtime.*
 import androidx.compose.ui.platform.LocalContext
 import androidx.core.content.edit
 import androidx.media3.common.Player
-import it.vfsfitvnm.vimusic.enums.ColorPaletteMode
-import it.vfsfitvnm.vimusic.enums.SongCollection
-import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
+import it.vfsfitvnm.vimusic.enums.*
 import it.vfsfitvnm.youtubemusic.YouTube
 
 
 @Stable
 class Preferences(
     private val edit: (action: SharedPreferences.Editor.() -> Unit) -> Unit,
+    initialSongSortBy: SongSortBy,
+    initialSongSortOrder: SortOrder,
     initialColorPaletteMode: ColorPaletteMode,
     initialSearchFilter: String,
     initialRepeatMode: Int,
-    initialHomePageSongCollection: SongCollection,
     initialThumbnailRoundness: ThumbnailRoundness,
     initialCoilDiskCacheMaxSizeBytes: Long,
     initialExoPlayerDiskCacheMaxSizeBytes: Long,
@@ -30,10 +29,11 @@ class Preferences(
         edit = { action: SharedPreferences.Editor.() -> Unit ->
            preferences.edit(action = action)
         },
+        initialSongSortBy = preferences.getEnum(Keys.songSortBy, SongSortBy.DateAdded),
+        initialSongSortOrder = preferences.getEnum(Keys.songSortOrder, SortOrder.Descending),
         initialColorPaletteMode = preferences.getEnum(Keys.colorPaletteMode, ColorPaletteMode.System),
         initialSearchFilter = preferences.getString(Keys.searchFilter, YouTube.Item.Song.Filter.value)!!,
         initialRepeatMode = preferences.getInt(Keys.repeatMode, Player.REPEAT_MODE_OFF),
-        initialHomePageSongCollection = preferences.getEnum(Keys.homePageSongCollection, SongCollection.History),
         initialThumbnailRoundness = preferences.getEnum(Keys.thumbnailRoundness, ThumbnailRoundness.Light),
         initialCoilDiskCacheMaxSizeBytes = preferences.getLong(Keys.coilDiskCacheMaxSizeBytes, 512L * 1024 * 1024),
         initialExoPlayerDiskCacheMaxSizeBytes = preferences.getLong(Keys.exoPlayerDiskCacheMaxSizeBytes, 512L * 1024 * 1024),
@@ -42,6 +42,12 @@ class Preferences(
         initialPersistentQueue = preferences.getBoolean(Keys.persistentQueue, false)
     )
 
+    var songSortBy = initialSongSortBy
+        set(value) = edit { putEnum(Keys.songSortBy, value) }
+
+    var songSortOrder = initialSongSortOrder
+        set(value) = edit { putEnum(Keys.songSortOrder, value) }
+
     var colorPaletteMode = initialColorPaletteMode
         set(value) = edit { putEnum(Keys.colorPaletteMode, value) }
 
@@ -51,9 +57,6 @@ class Preferences(
     var repeatMode = initialRepeatMode
         set(value) = edit { putInt(Keys.repeatMode, value) }
 
-    var homePageSongCollection = initialHomePageSongCollection
-        set(value) = edit { putEnum(Keys.homePageSongCollection, value) }
-
     var thumbnailRoundness = initialThumbnailRoundness
         set(value) = edit { putEnum(Keys.thumbnailRoundness, value) }
 
@@ -73,10 +76,11 @@ class Preferences(
         set(value) = edit { putBoolean(Keys.persistentQueue, value) }
 
     object Keys {
+        const val songSortOrder = "songSortOrder"
+        const val songSortBy = "songSortBy"
         const val colorPaletteMode = "colorPaletteMode"
         const val searchFilter = "searchFilter"
         const val repeatMode = "repeatMode"
-        const val homePageSongCollection = "homePageSongCollection"
         const val thumbnailRoundness = "thumbnailRoundness"
         const val coilDiskCacheMaxSizeBytes = "coilDiskCacheMaxSizeBytes"
         const val exoPlayerDiskCacheMaxSizeBytes = "exoPlayerDiskCacheMaxSizeBytes"

+ 13 - 0
app/src/main/res/drawable/checkmark.xml

@@ -0,0 +1,13 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="512"
+    android:viewportHeight="512">
+  <path
+      android:pathData="M416,128l-224,256l-96,-96"
+      android:strokeLineJoin="round"
+      android:strokeWidth="32"
+      android:fillColor="#00000000"
+      android:strokeColor="#000"
+      android:strokeLineCap="round"/>
+</vector>

+ 15 - 0
app/src/main/res/drawable/sort.xml

@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="512"
+    android:viewportHeight="512">
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M472,168H40a24,24 0,0 1,0 -48H472a24,24 0,0 1,0 48Z"/>
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M312,280H40a24,24 0,0 1,0 -48h272a24,24 0,0 1,0 48z"/>
+  <path
+      android:fillColor="#FF000000"
+      android:pathData="M120,392H40a24,24 0,0 1,0 -48h80a24,24 0,0 1,0 48z"/>
+</vector>