From e0d0732ad45bba3efd15f7ef2b314497bc7702ea Mon Sep 17 00:00:00 2001 From: Lambada10 <62511588+Lambada10@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:34:34 +0100 Subject: [PATCH] Song sort, closes #68 (#99) --- .../data/remote/UserSettingsController.kt | 28 ++++- .../pl/lambada/songsync/domain/model/Sort.kt | 17 +++ .../songsync/ui/screens/home/HomeScreen.kt | 22 +++- .../songsync/ui/screens/home/HomeViewModel.kt | 15 ++- .../home/components/FilterAndSongCount.kt | 10 ++ .../ui/screens/home/components/SortDialog.kt | 107 ++++++++++++++++++ app/src/main/res/values/strings.xml | 8 ++ 7 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/pl/lambada/songsync/domain/model/Sort.kt create mode 100644 app/src/main/java/pl/lambada/songsync/ui/screens/home/components/SortDialog.kt diff --git a/app/src/main/java/pl/lambada/songsync/data/remote/UserSettingsController.kt b/app/src/main/java/pl/lambada/songsync/data/remote/UserSettingsController.kt index 66d9867..c3900c8 100644 --- a/app/src/main/java/pl/lambada/songsync/data/remote/UserSettingsController.kt +++ b/app/src/main/java/pl/lambada/songsync/data/remote/UserSettingsController.kt @@ -7,6 +7,8 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey +import pl.lambada.songsync.domain.model.SortOrders +import pl.lambada.songsync.domain.model.SortValues import pl.lambada.songsync.util.Providers import pl.lambada.songsync.util.get import pl.lambada.songsync.util.set @@ -50,6 +52,18 @@ class UserSettingsController(private val dataStore: DataStore) { var showPath by mutableStateOf(dataStore.get(showPathKey, false)) private set + var sortOrder by mutableStateOf( + SortOrders.entries + .find { it.queryName == dataStore.get(sortOrderKey, SortOrders.ASCENDING.queryName) }!! + ) + private set + + var sortBy by mutableStateOf( + SortValues.entries + .find { it.name == dataStore.get(sortByKey, SortValues.TITLE.name) }!! + ) + private set + fun updateEmbedLyrics(to: Boolean) { dataStore.set(embedKey, to) embedLyricsIntoFiles = to @@ -104,6 +118,16 @@ class UserSettingsController(private val dataStore: DataStore) { dataStore.set(showPathKey, to) showPath = to } + + fun updateSortOrder(to: SortOrders) { + dataStore.set(sortOrderKey, to.queryName) + sortOrder = to + } + + fun updateSortBy(to: SortValues) { + dataStore.set(sortByKey, to.name) + sortBy = to + } } private val embedKey = booleanPreferencesKey("embed_lyrics") @@ -116,4 +140,6 @@ private val syncedMusixmatchKey = booleanPreferencesKey("synced_lyrics") private val disableMarqueeKey = booleanPreferencesKey("marquee_disable") private val pureBlackKey = booleanPreferencesKey("pure_black") private val sdCardPathKey = stringPreferencesKey("sd_card_path") -private val showPathKey = booleanPreferencesKey("show_path") \ No newline at end of file +private val showPathKey = booleanPreferencesKey("show_path") +private val sortOrderKey = stringPreferencesKey("sort_order") +private val sortByKey = stringPreferencesKey("sort_by") \ No newline at end of file diff --git a/app/src/main/java/pl/lambada/songsync/domain/model/Sort.kt b/app/src/main/java/pl/lambada/songsync/domain/model/Sort.kt new file mode 100644 index 0000000..9847377 --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/domain/model/Sort.kt @@ -0,0 +1,17 @@ +package pl.lambada.songsync.domain.model + +import androidx.annotation.StringRes +import pl.lambada.songsync.R + +enum class SortOrders(val queryName: String, @StringRes val displayName: Int) { + ASCENDING("ASC", R.string.ascending), + DESCENDING("DESC", R.string.descending), +} + +enum class SortValues(@StringRes val displayName: Int) { + TITLE(R.string.title), + ARTIST(R.string.artist), + ALBUM(R.string.album), + YEAR(R.string.year), + DURATION(R.string.duration), +} diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt index ea5c1bb..9a08427 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeScreen.kt @@ -15,13 +15,16 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -36,6 +39,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController +import pl.lambada.songsync.domain.model.SortOrders +import pl.lambada.songsync.domain.model.SortValues import pl.lambada.songsync.ui.LyricsFetchScreen import pl.lambada.songsync.ui.ScreenAbout import pl.lambada.songsync.ui.screens.home.components.BatchDownloadLyrics @@ -45,6 +50,7 @@ import pl.lambada.songsync.ui.screens.home.components.HomeAppBar import pl.lambada.songsync.ui.screens.home.components.HomeSearchBar import pl.lambada.songsync.ui.screens.home.components.HomeSearchThing import pl.lambada.songsync.ui.screens.home.components.SongItem +import pl.lambada.songsync.ui.screens.home.components.SortDialog import pl.lambada.songsync.util.ext.BackPressHandler import pl.lambada.songsync.util.ext.lowercaseWithLocale @@ -65,7 +71,10 @@ fun HomeScreen( var isBatchDownload by remember { mutableStateOf(false) } val context = LocalContext.current - SideEffect { viewModel.updateAllSongs(context) } + LaunchedEffect(viewModel.userSettingsController.sortBy to viewModel.userSettingsController.sortOrder) { + viewModel.cachedSongs = null + viewModel.updateAllSongs(context, viewModel.userSettingsController.sortBy, viewModel.userSettingsController.sortOrder) + } Scaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -209,6 +218,7 @@ fun HomeScreenLoaded( FilterAndSongCount( displaySongsCount = viewModel.displaySongs.size, onFilterClick = { viewModel.showFilters = true }, + onSortClick = { viewModel.showSort = true }, onSearchClick = { viewModel.showSearch = true viewModel.showingSearch = true @@ -217,6 +227,15 @@ fun HomeScreenLoaded( } ) + if (viewModel.showSort) { + SortDialog( + userSettingsController = viewModel.userSettingsController, + onDismiss = { viewModel.showSort = false }, + onSortOrderChange = { viewModel.userSettingsController.updateSortOrder(it) }, + onSortByChange = { viewModel.userSettingsController.updateSortBy(it) } + ) + } + if (viewModel.showFilters) { FiltersDialog( hideLyrics = viewModel.userSettingsController.hideLyrics, @@ -231,6 +250,7 @@ fun HomeScreenLoaded( } } + items(viewModel.displaySongs.size) { index -> val song = viewModel.displaySongs[index] diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeViewModel.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeViewModel.kt index 0090cc3..21d26e4 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeViewModel.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/home/HomeViewModel.kt @@ -25,6 +25,8 @@ import pl.lambada.songsync.data.remote.UserSettingsController import pl.lambada.songsync.data.remote.lyrics_providers.LyricsProviderService import pl.lambada.songsync.domain.model.Song import pl.lambada.songsync.domain.model.SongInfo +import pl.lambada.songsync.domain.model.SortOrders +import pl.lambada.songsync.domain.model.SortValues import pl.lambada.songsync.util.downloadLyrics import pl.lambada.songsync.util.ext.toLrcFile @@ -35,7 +37,7 @@ class HomeViewModel( val userSettingsController: UserSettingsController, private val lyricsProviderService: LyricsProviderService ) : ViewModel() { - private var cachedSongs: List? = null + var cachedSongs: List? = null val selectedSongs = mutableStateListOf() var allSongs by mutableStateOf?>(null) @@ -60,6 +62,7 @@ class HomeViewModel( ) var showFilters by mutableStateOf(false) + var showSort by mutableStateOf(false) var showingSearch by mutableStateOf(false) var showSearch by mutableStateOf(showingSearch) @@ -90,8 +93,8 @@ class HomeViewModel( } } - fun updateAllSongs(context: Context) = viewModelScope.launch(Dispatchers.IO) { - allSongs = getAllSongs(context) + fun updateAllSongs(context: Context, sortBy: SortValues, sortOrder: SortOrders) = viewModelScope.launch(Dispatchers.IO) { + allSongs = getAllSongs(context, sortBy, sortOrder) } /** @@ -99,7 +102,7 @@ class HomeViewModel( * @param context The application context. * @return A list of Song objects representing the songs. */ - private fun getAllSongs(context: Context): List { + private fun getAllSongs(context: Context, sortBy: SortValues, sortOrder: SortOrders): List { return cachedSongs ?: run { val selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0" val projection = arrayOf( @@ -109,7 +112,7 @@ class HomeViewModel( MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.ALBUM_ID, ) - val sortOrder = MediaStore.Audio.Media.TITLE + " ASC" + val sortOrder = sortBy.name + " " + sortOrder.queryName val songs = mutableListOf() val cursor = context.contentResolver.query( @@ -189,7 +192,7 @@ class HomeViewModel( return cachedFolders ?: run { val folders = mutableListOf() - for (song in getAllSongs(context)) { + for (song in getAllSongs(context, SortValues.TITLE, SortOrders.ASCENDING)) { val path = song.filePath val folder = path?.substring(0, path.lastIndexOf("/")) if (folder != null && !folders.contains(folder)) diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/FilterAndSongCount.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/FilterAndSongCount.kt index 2ee0d7d..e54d95a 100644 --- a/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/FilterAndSongCount.kt +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/FilterAndSongCount.kt @@ -4,7 +4,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Sort import androidx.compose.material.icons.outlined.FilterAlt import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -21,6 +23,7 @@ import pl.lambada.songsync.R fun FilterAndSongCount( displaySongsCount: Int, onFilterClick: () -> Unit, + onSortClick: () -> Unit, onSearchClick: () -> Unit ) { Row( @@ -29,6 +32,13 @@ fun FilterAndSongCount( ) { Text(text = "$displaySongsCount songs") Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = onSortClick) { + Icon( + Icons.AutoMirrored.Filled.Sort, + contentDescription = stringResource(R.string.sort), + ) + } + IconButton(onClick = onFilterClick) { Icon( Icons.Outlined.FilterAlt, diff --git a/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/SortDialog.kt b/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/SortDialog.kt new file mode 100644 index 0000000..95f7903 --- /dev/null +++ b/app/src/main/java/pl/lambada/songsync/ui/screens/home/components/SortDialog.kt @@ -0,0 +1,107 @@ +package pl.lambada.songsync.ui.screens.home.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import pl.lambada.songsync.R +import pl.lambada.songsync.data.remote.UserSettingsController +import pl.lambada.songsync.domain.model.SortOrders +import pl.lambada.songsync.domain.model.SortValues + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SortDialog( + userSettingsController: UserSettingsController, + onDismiss: () -> Unit, + onSortByChange: (SortValues) -> Unit, + onSortOrderChange: (SortOrders) -> Unit +) { + BasicAlertDialog(onDismiss) { + Surface( + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column { + Text( + text = stringResource(R.string.sort), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(top = 24.dp, start = 24.dp, bottom = 16.dp) + ) + SortByRadioGroup(userSettingsController, onSortByChange) + HorizontalDivider() + SortOrderRadioGroup(userSettingsController, onSortOrderChange) + Row(modifier = Modifier.padding(16.dp)) { + Spacer(modifier = Modifier.weight(1f)) + TextButton(onDismiss) { + Text(stringResource(R.string.close)) + } + } + } + } + } +} + +@Composable +fun SortOrderRadioGroup(userSettingsController: UserSettingsController, onSortOrderChange: (SortOrders) -> Unit) { + Column { + SortOrders.entries.forEach { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = userSettingsController.sortOrder == it, + onClick = { + onSortOrderChange(it) + } + ) + Text( + stringResource(it.displayName), + modifier = Modifier.clickable { + onSortOrderChange(it) + } + ) + } + } + } +} + +@Composable +fun SortByRadioGroup(userSettingsController: UserSettingsController, onSortByChange: (SortValues) -> Unit) { + Column { + SortValues.entries.forEach { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = userSettingsController.sortBy == it, + onClick = { + onSortByChange(it) + } + ) + Text( + stringResource(it.displayName), + modifier = Modifier.clickable { + onSortByChange(it) + } + ) + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a503e2..c32d78e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,4 +130,12 @@ Show the path of a song file on song list Get Synced Lyrics Toggle to switch between synced and unsynced lyrics from Musixmatch. + Title + Artist + Album + Year + Duration + Ascending + Descending + Sort