From 06a7d8258bbf97f7451139d1c49fd7407f10c27e Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 21 Nov 2021 15:45:20 -0700 Subject: [PATCH] sort: refactor sorting Refactor sorting again to support free-floating ascending/descending values on every single sort mode. This enables greater freedom in how users can sort their music and allows me to finally get rid of the old legacy sematic sorting modes that chose their ascending/decending order depending on how they wanted it. --- .../org/oxycblt/auxio/coil/AuxioFetcher.kt | 2 + .../java/org/oxycblt/auxio/coil/CoilUtils.kt | 2 +- .../java/org/oxycblt/auxio/coil/Fetchers.kt | 13 +- .../auxio/detail/AlbumDetailFragment.kt | 2 +- .../oxycblt/auxio/detail/DetailFragment.kt | 12 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 10 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 24 +- .../org/oxycblt/auxio/home/HomeViewModel.kt | 8 +- .../auxio/home/list/AlbumListFragment.kt | 8 +- .../auxio/home/list/SongListFragment.kt | 10 +- .../org/oxycblt/auxio/music/MusicLoader.kt | 6 +- .../oxycblt/auxio/settings/SettingsManager.kt | 69 ++--- .../main/java/org/oxycblt/auxio/ui/Sort.kt | 248 ++++++++++++++++++ .../java/org/oxycblt/auxio/ui/SortMode.kt | 230 ---------------- .../java/org/oxycblt/auxio/widgets/Forms.kt | 2 - app/src/main/res/layout/item_detail.xml | 1 - app/src/main/res/menu/menu_detail_sort.xml | 11 +- app/src/main/res/menu/menu_home.xml | 11 +- app/src/main/res/values/settings.xml | 20 ++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/styles_ui.xml | 28 ++ app/src/main/res/xml/prefs_main.xml | 6 +- 22 files changed, 406 insertions(+), 318 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/ui/Sort.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/ui/SortMode.kt diff --git a/app/src/main/java/org/oxycblt/auxio/coil/AuxioFetcher.kt b/app/src/main/java/org/oxycblt/auxio/coil/AuxioFetcher.kt index dedd9fb7b..bf6360ea8 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/AuxioFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/AuxioFetcher.kt @@ -49,6 +49,8 @@ abstract class AuxioFetcher : Fetcher { * https://github.com/kabouzeid/Phonograph */ protected fun createMosaic(context: Context, streams: List): FetchResult? { + logD("idiot") + if (streams.size < 4) { return streams.getOrNull(0)?.let { stream -> return SourceResult( diff --git a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt index 1f5b16096..39a191dfb 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt @@ -79,7 +79,7 @@ fun ImageView.bindArtistImage(artist: Artist?) { fun ImageView.bindGenreImage(genre: Genre?) { dispose() - load(genre?.songs?.get(0)?.album) { + load(genre) { error(R.drawable.ic_genre) } } diff --git a/app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt b/app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt index 95385490b..21bd13060 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt @@ -71,8 +71,7 @@ class ArtistImageFetcher private constructor( private val artist: Artist ) : AuxioFetcher() { override suspend fun fetch(): FetchResult? { - val end = min(4, artist.albums.size) - val results = artist.albums.mapN(end) { album -> + val results = artist.albums.mapAtMost(4) { album -> fetchArt(context, album) } @@ -92,8 +91,7 @@ class GenreImageFetcher private constructor( ) : AuxioFetcher() { override suspend fun fetch(): FetchResult? { val albums = genre.songs.groupBy { it.album }.keys - val end = min(4, albums.size) - val results = albums.mapN(end) { album -> + val results = albums.mapAtMost(4) { album -> fetchArt(context, album) } @@ -108,14 +106,15 @@ class GenreImageFetcher private constructor( } /** - * Map only [n] items from a collection. [transform] is called for each item that is eligible. + * Map at most [n] items from a collection. [transform] is called for each item that is eligible. * If null is returned, then that item will be skipped. */ -private inline fun Iterable.mapN(n: Int, transform: (T) -> R?): List { +private inline fun Collection.mapAtMost(n: Int, transform: (T) -> R?): List { + val until = min(size, n) val out = mutableListOf() for (item in this) { - if (out.size >= n) { + if (out.size >= until) { break } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index e03ac50ff..c538717b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -88,7 +88,7 @@ class AlbumDetailFragment : DetailFragment() { detailModel.showMenu.observe(viewLifecycleOwner) { config -> if (config != null) { showMenu(config) { id -> - id == R.id.option_sort_asc || id == R.id.option_sort_dsc + id == R.id.option_sort_asc } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 5fc2d2af9..9cf59a489 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -32,7 +32,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.ui.SortMode import org.oxycblt.auxio.ui.memberBinding import org.oxycblt.auxio.util.applySpans @@ -123,8 +122,14 @@ abstract class DetailFragment : Fragment() { inflate(R.menu.menu_detail_sort) setOnMenuItemClickListener { item -> - item.isChecked = true - detailModel.finishShowMenu(SortMode.fromId(item.itemId)!!) + if (item.itemId == R.id.option_sort_asc) { + item.isChecked = !item.isChecked + detailModel.finishShowMenu(config.sortMode.ascending(item.isChecked)) + } else { + item.isChecked = true + detailModel.finishShowMenu(config.sortMode.assignId(item.itemId)) + } + true } @@ -139,6 +144,7 @@ abstract class DetailFragment : Fragment() { } menu.findItem(config.sortMode.itemId).isChecked = true + menu.findItem(R.id.option_sort_asc).isChecked = config.sortMode.isAscending show() } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 069477718..8f8a9b05b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.music.HeaderString import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.DisplayMode -import org.oxycblt.auxio.ui.SortMode +import org.oxycblt.auxio.ui.Sort /** * ViewModel that stores data for the [DetailFragment]s. This includes: @@ -64,7 +64,7 @@ class DetailViewModel : ViewModel() { private val mAlbumData = MutableLiveData(listOf()) val albumData: LiveData> get() = mAlbumData - data class MenuConfig(val anchor: View, val sortMode: SortMode) + data class MenuConfig(val anchor: View, val sortMode: Sort) private val mShowMenu = MutableLiveData(null) val showMenu: LiveData = mShowMenu @@ -105,10 +105,10 @@ class DetailViewModel : ViewModel() { } /** - * Mark that the menu process is done with the new [SortMode]. + * Mark that the menu process is done with the new [Sort]. * Pass null if there was no change. */ - fun finishShowMenu(newMode: SortMode?) { + fun finishShowMenu(newMode: Sort?) { mShowMenu.value = null if (newMode != null) { @@ -185,7 +185,7 @@ class DetailViewModel : ViewModel() { ) ) - data.addAll(SortMode.YEAR.sortAlbums(artist.albums)) + data.addAll(Sort.ByYear(false).sortAlbums(artist.albums)) data.add( ActionHeader( diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index db6b65d92..8363f16a2 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -47,7 +47,6 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.DisplayMode -import org.oxycblt.auxio.ui.SortMode import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE @@ -95,13 +94,22 @@ class HomeFragment : Fragment() { R.id.submenu_sorting -> { } + R.id.option_sort_asc -> { + item.isChecked = !item.isChecked + val new = homeModel.getSortForDisplay(homeModel.curTab.value!!) + .ascending(item.isChecked) + + homeModel.updateCurrentSort(new) + } + // Sorting option was selected, mark it as selected and update the mode else -> { item.isChecked = true - homeModel.updateCurrentSort( - requireNotNull(SortMode.fromId(item.itemId)) - ) + val new = homeModel.getSortForDisplay(homeModel.curTab.value!!) + .assignId(item.itemId) + + homeModel.updateCurrentSort(requireNotNull(new)) } } @@ -208,11 +216,11 @@ class HomeFragment : Fragment() { } DisplayMode.SHOW_ARTISTS -> updateSortMenu(sortItem, tab) { id -> - id == R.id.option_sort_asc || id == R.id.option_sort_dsc + id == R.id.option_sort_asc } DisplayMode.SHOW_GENRES -> updateSortMenu(sortItem, tab) { id -> - id == R.id.option_sort_asc || id == R.id.option_sort_dsc + id == R.id.option_sort_asc } } @@ -263,6 +271,10 @@ class HomeFragment : Fragment() { option.isChecked = true } + if (option.itemId == R.id.option_sort_asc) { + option.isChecked = toHighlight.isAscending + } + option.isVisible = isVisible(option.itemId) } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index e610ebeba..bbaa9b0c1 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.tabs.Tab import org.oxycblt.auxio.ui.DisplayMode -import org.oxycblt.auxio.ui.SortMode +import org.oxycblt.auxio.ui.Sort /** * The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state. @@ -87,7 +87,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal mRecreateTabs.value = false } - fun getSortForDisplay(displayMode: DisplayMode): SortMode { + fun getSortForDisplay(displayMode: DisplayMode): Sort { return when (displayMode) { DisplayMode.SHOW_SONGS -> settingsManager.libSongSort DisplayMode.SHOW_ALBUMS -> settingsManager.libAlbumSort @@ -97,9 +97,9 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.MusicCal } /** - * Update the currently displayed item's [SortMode]. + * Update the currently displayed item's [Sort]. */ - fun updateCurrentSort(sort: SortMode) { + fun updateCurrentSort(sort: Sort) { when (mCurTab.value) { DisplayMode.SHOW_SONGS -> { settingsManager.libSongSort = sort diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index a82611d41..894878096 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.home.HomeFragmentDirections import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.ui.AlbumViewHolder import org.oxycblt.auxio.ui.DisplayMode -import org.oxycblt.auxio.ui.SortMode +import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle @@ -59,13 +59,13 @@ class AlbumListFragment : HomeListFragment() { val album = homeModel.albums.value!![idx] when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) { - SortMode.ASCENDING, SortMode.DESCENDING -> album.name.sliceArticle() + is Sort.ByName -> album.name.sliceArticle() .first().uppercase() - SortMode.ARTIST -> album.artist.resolvedName.sliceArticle() + is Sort.ByArtist -> album.artist.resolvedName.sliceArticle() .first().uppercase() - SortMode.YEAR -> album.year.toString() + is Sort.ByYear -> album.year.toString() else -> "" } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 89a01d9d8..8043292b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.SongViewHolder -import org.oxycblt.auxio.ui.SortMode +import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.newMenu import org.oxycblt.auxio.ui.sliceArticle @@ -55,17 +55,17 @@ class SongListFragment : HomeListFragment() { val song = homeModel.songs.value!![idx] when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) { - SortMode.ASCENDING, SortMode.DESCENDING -> song.name.sliceArticle() + is Sort.ByName -> song.name.sliceArticle() .first().uppercase() - SortMode.ARTIST -> + is Sort.ByArtist -> song.album.artist.resolvedName .sliceArticle().first().uppercase() - SortMode.ALBUM -> song.album.name.sliceArticle() + is Sort.ByAlbum -> song.album.name.sliceArticle() .first().uppercase() - SortMode.YEAR -> song.album.year.toString() + is Sort.ByYear -> song.album.year.toString() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt index 50617bcc3..129a3885f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt @@ -26,7 +26,7 @@ import android.provider.MediaStore.Audio.Media import androidx.core.database.getStringOrNull import org.oxycblt.auxio.R import org.oxycblt.auxio.excluded.ExcludedDatabase -import org.oxycblt.auxio.ui.SortMode +import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.util.logD /** @@ -251,7 +251,7 @@ class MusicLoader(private val context: Context) { } albums.removeAll { it.songs.isEmpty() } - albums = SortMode.ASCENDING.sortAlbums(albums).toMutableList() + albums = Sort.ByName(true).sortAlbums(albums).toMutableList() logD("Songs successfully linked into ${albums.size} albums") } @@ -280,7 +280,7 @@ class MusicLoader(private val context: Context) { ) } - artists = SortMode.ASCENDING.sortParents(artists).toMutableList() + artists = Sort.ByName(true).sortParents(artists).toMutableList() logD("Albums successfully linked into ${artists.size} artists") } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt index 009d481ae..575273d73 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.accent.Accent import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.settings.tabs.Tab import org.oxycblt.auxio.ui.DisplayMode -import org.oxycblt.auxio.ui.SortMode +import org.oxycblt.auxio.ui.Sort /** * Wrapper around the [SharedPreferences] class that writes & reads values without a context. @@ -125,9 +125,9 @@ class SettingsManager private constructor(context: Context) : } /** The song sort mode on HomeFragment **/ - var libSongSort: SortMode - get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE)) - ?: SortMode.ASCENDING + var libSongSort: Sort + get() = Sort.fromInt(sharedPrefs.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE)) + ?: Sort.ByName(true) set(value) { sharedPrefs.edit { putInt(KEY_LIB_SONGS_SORT, value.toInt()) @@ -136,9 +136,9 @@ class SettingsManager private constructor(context: Context) : } /** The album sort mode on HomeFragment **/ - var libAlbumSort: SortMode - get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE)) - ?: SortMode.ASCENDING + var libAlbumSort: Sort + get() = Sort.fromInt(sharedPrefs.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE)) + ?: Sort.ByName(true) set(value) { sharedPrefs.edit { putInt(KEY_LIB_ALBUMS_SORT, value.toInt()) @@ -147,9 +147,9 @@ class SettingsManager private constructor(context: Context) : } /** The artist sort mode on HomeFragment **/ - var libArtistSort: SortMode - get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE)) - ?: SortMode.ASCENDING + var libArtistSort: Sort + get() = Sort.fromInt(sharedPrefs.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE)) + ?: Sort.ByName(true) set(value) { sharedPrefs.edit { putInt(KEY_LIB_ARTISTS_SORT, value.toInt()) @@ -158,20 +158,20 @@ class SettingsManager private constructor(context: Context) : } /** The genre sort mode on HomeFragment **/ - var libGenreSort: SortMode - get() = SortMode.fromInt(sharedPrefs.getInt(KEY_LIB_GENRE_SORT, Int.MIN_VALUE)) - ?: SortMode.ASCENDING + var libGenreSort: Sort + get() = Sort.fromInt(sharedPrefs.getInt(KEY_LIB_GENRES_SORT, Int.MIN_VALUE)) + ?: Sort.ByName(true) set(value) { sharedPrefs.edit { - putInt(KEY_LIB_GENRE_SORT, value.toInt()) + putInt(KEY_LIB_GENRES_SORT, value.toInt()) apply() } } /** The detail album sort mode **/ - var detailAlbumSort: SortMode - get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE)) - ?: SortMode.ASCENDING + var detailAlbumSort: Sort + get() = Sort.fromInt(sharedPrefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE)) + ?: Sort.ByName(true) set(value) { sharedPrefs.edit { putInt(KEY_DETAIL_ALBUM_SORT, value.toInt()) @@ -180,9 +180,9 @@ class SettingsManager private constructor(context: Context) : } /** The detail artist sort mode **/ - var detailArtistSort: SortMode - get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE)) - ?: SortMode.YEAR + var detailArtistSort: Sort + get() = Sort.fromInt(sharedPrefs.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE)) + ?: Sort.ByYear(false) set(value) { sharedPrefs.edit { putInt(KEY_DETAIL_ARTIST_SORT, value.toInt()) @@ -191,9 +191,9 @@ class SettingsManager private constructor(context: Context) : } /** The detail genre sort mode **/ - var detailGenreSort: SortMode - get() = SortMode.fromInt(sharedPrefs.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE)) - ?: SortMode.ASCENDING + var detailGenreSort: Sort + get() = Sort.fromInt(sharedPrefs.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE)) + ?: Sort.ByName(true) set(value) { sharedPrefs.edit { putInt(KEY_DETAIL_GENRE_SORT, value.toInt()) @@ -249,11 +249,14 @@ class SettingsManager private constructor(context: Context) : } companion object { + // Preference keys + // The old way of naming keys was to prefix them with KEY_. Now it's to prefix them with + // auxio_. const val KEY_THEME = "KEY_THEME2" const val KEY_BLACK_THEME = "KEY_BLACK_THEME" - const val KEY_ACCENT = "KEY_ACCENT3" + const val KEY_ACCENT = "auxio_accent" - const val KEY_LIB_TABS = "KEY_LIB_TABS" + const val KEY_LIB_TABS = "auxio_lib_tabs" const val KEY_SHOW_COVERS = "KEY_SHOW_COVERS" const val KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS" const val KEY_USE_ALT_NOTIFICATION_ACTION = "KEY_ALT_NOTIF_ACTION" @@ -266,19 +269,19 @@ class SettingsManager private constructor(context: Context) : const val KEY_PREV_REWIND = "KEY_PREV_REWIND" const val KEY_LOOP_PAUSE = "KEY_LOOP_PAUSE" - const val KEY_SAVE_STATE = "KEY_SAVE_STATE" + const val KEY_SAVE_STATE = "auxio_save_state" const val KEY_BLACKLIST = "KEY_BLACKLIST" const val KEY_SEARCH_FILTER_MODE = "KEY_SEARCH_FILTER" - const val KEY_LIB_SONGS_SORT = "KEY_SONGS_SORT" - const val KEY_LIB_ALBUMS_SORT = "KEY_ALBUMS_SORT" - const val KEY_LIB_ARTISTS_SORT = "KEY_ARTISTS_SORT" - const val KEY_LIB_GENRE_SORT = "KEY_GENRE_SORT" + const val KEY_LIB_SONGS_SORT = "auxio_songs_sort" + const val KEY_LIB_ALBUMS_SORT = "auxio_albums_sort" + const val KEY_LIB_ARTISTS_SORT = "auxio_artists_sort" + const val KEY_LIB_GENRES_SORT = "auxio_genres_sort" - const val KEY_DETAIL_ALBUM_SORT = "KEY_ALBUM_SORT" - const val KEY_DETAIL_ARTIST_SORT = "KEY_ARTIST_SORT" - const val KEY_DETAIL_GENRE_SORT = "KEY_GENRE_SORT" + const val KEY_DETAIL_ALBUM_SORT = "auxio_album_sort" + const val KEY_DETAIL_ARTIST_SORT = "auxio_artist_sort" + const val KEY_DETAIL_GENRE_SORT = "auxio_genre_sort" @Volatile private var INSTANCE: SettingsManager? = null diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt new file mode 100644 index 000000000..c1194a714 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt @@ -0,0 +1,248 @@ +/* + * Copyright (c) 2021 Auxio Project + * Sort.kt is part of Auxio. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.ui + +import androidx.annotation.IdRes +import org.oxycblt.auxio.R +import org.oxycblt.auxio.music.Album +import org.oxycblt.auxio.music.Artist +import org.oxycblt.auxio.music.Genre +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.Song + +/** + * A data class representing the sort modes used in Auxio. + * + * Sorting can be done by Name, Artist, Album, or Year. Sorting of names is always case-insensitive + * and article-aware. Certain datatypes may only support a subset of sorts since certain sorts + * cannot be easily applied to them (For Example, [Artist] and [ByYear] or [ByAlbum]). + * + * Internally, sorts are saved as an integer in the following format + * + * 0b(SORT INT)A + * + * Where SORT INT is the corresponding integer value of this specific sort and A is a bit + * representing whether this sort is ascending or descending. + * + * @author OxygenCobalt + */ +sealed class Sort(open val isAscending: Boolean) { + /** Sort by the names of an item */ + class ByName(override val isAscending: Boolean) : Sort(isAscending) + /** Sort by the artist of an item, only supported by [Album] and [Song] */ + class ByArtist(override val isAscending: Boolean) : Sort(isAscending) + /** Sort by the album of an item, only supported by [Song] */ + class ByAlbum(override val isAscending: Boolean) : Sort(isAscending) + /** Sort by the year of an item, only supported by [Album] and [Song] */ + class ByYear(override val isAscending: Boolean) : Sort(isAscending) + + /** + * Get the corresponding item id for this sort. + */ + val itemId: Int get() = when (this) { + is ByName -> R.id.option_sort_name + is ByArtist -> R.id.option_sort_artist + is ByAlbum -> R.id.option_sort_album + is ByYear -> R.id.option_sort_year + } + + /** + * Apply [ascending] to the status of this sort. + * @return A new [Sort] with the value of [ascending] applied. + */ + fun ascending(ascending: Boolean): Sort { + return when (this) { + is ByName -> ByName(ascending) + is ByArtist -> ByArtist(ascending) + is ByAlbum -> ByAlbum(ascending) + is ByYear -> ByYear(ascending) + } + } + + /** + * Assign a new [id] to this sort + * @return A new [Sort] corresponding to the [id] given, null if the ID has no analogue. + */ + fun assignId(@IdRes id: Int): Sort? { + return when (id) { + R.id.option_sort_name -> ByName(isAscending) + R.id.option_sort_artist -> ByArtist(isAscending) + R.id.option_sort_album -> ByAlbum(isAscending) + R.id.option_sort_year -> ByYear(isAscending) + else -> null + } + } + + /** + * Sort a list of [Song] instances to reflect this specific sort. + * + * Albums are sorted by ascending track, artists are sorted with [ByYear] descending. + * + * @return A sorted list of songs + */ + fun sortSongs(songs: Collection): List { + return when (this) { + is ByName -> songs.stringSort { it.name } + + else -> sortAlbums(songs.groupBy { it.album }.keys).flatMap { album -> + album.songs.intSort(true) { it.track } + } + } + } + + /** + * Sort a list of [Album] instances to reflect this specific sort. + * + * Artists are sorted with [ByYear] descending. + * + * @return A sorted list of albums + */ + fun sortAlbums(albums: Collection): List { + return when (this) { + is ByName, is ByAlbum -> albums.stringSort { it.resolvedName } + + is ByArtist -> sortParents(albums.groupBy { it.artist }.keys) + .flatMap { ByYear(false).sortAlbums(it.albums) } + + is ByYear -> albums.intSort { it.year } + } + } + + /** + * Sort a list of [MusicParent] instances to reflect this specific sort. + * + * @return A sorted list of the specific parent + */ + fun sortParents(parents: Collection): List { + return parents.stringSort { it.resolvedName } + } + + /** + * Sort the songs in an album. + * @see sortSongs + */ + fun sortAlbum(album: Album): List { + return album.songs.intSort { it.track } + } + + /** + * Sort the songs in an artist. + * @see sortSongs + */ + fun sortArtist(artist: Artist): List { + return sortSongs(artist.songs) + } + + /** + * Sort the songs in a genre. + * @see sortSongs + */ + fun sortGenre(genre: Genre): List { + return sortSongs(genre.songs) + } + + /** + * Convert this sort to it's integer representation. + */ + fun toInt(): Int { + return when (this) { + is ByName -> CONST_NAME + is ByArtist -> CONST_ARTIST + is ByAlbum -> CONST_ALBUM + is ByYear -> CONST_YEAR + }.shl(1) or if (isAscending) 1 else 0 + } + + private fun Collection.stringSort( + asc: Boolean = isAscending, + selector: (T) -> String + ): List { + // Chain whatever item call with sliceArticle for correctness + val chained: (T) -> String = { + selector(it).sliceArticle() + } + + val comparator = if (asc) { + compareBy(String.CASE_INSENSITIVE_ORDER, chained) + } else { + compareByDescending(String.CASE_INSENSITIVE_ORDER, chained) + } + + return sortedWith(comparator) + } + + private fun Collection.intSort( + asc: Boolean = isAscending, + selector: (T) -> Int, + ): List { + val comparator = if (asc) { + compareBy(selector) + } else { + compareByDescending(selector) + } + + return sortedWith(comparator) + } + + companion object { + private const val CONST_NAME = 0xA10C + private const val CONST_ARTIST = 0xA10d + private const val CONST_ALBUM = 0xA10E + private const val CONST_YEAR = 0xA10F + + /** + * Convert a sort's integer representation into a [Sort] instance. + * + * @return A [Sort] instance, null if the data is malformed. + */ + fun fromInt(value: Int): Sort? { + val ascending = (value and 1) == 1 + + return when (value.shr(1)) { + CONST_NAME -> ByName(ascending) + CONST_ARTIST -> ByArtist(ascending) + CONST_ALBUM -> ByAlbum(ascending) + CONST_YEAR -> ByYear(ascending) + else -> null + } + } + } +} + +/** + * Slice a string so that any preceding articles like The/A(n) are truncated. + * This is hilariously anglo-centric, but its mostly for MediaStore compat and hopefully + * shouldn't run with other languages. + */ +fun String.sliceArticle(): String { + if (length > 5 && startsWith("the ", true)) { + return slice(4..lastIndex) + } + + if (length > 4 && startsWith("an ", true)) { + return slice(3..lastIndex) + } + + if (length > 3 && startsWith("a ", true)) { + return slice(2..lastIndex) + } + + return this +} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/SortMode.kt b/app/src/main/java/org/oxycblt/auxio/ui/SortMode.kt deleted file mode 100644 index 927967855..000000000 --- a/app/src/main/java/org/oxycblt/auxio/ui/SortMode.kt +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (c) 2021 Auxio Project - * SortMode.kt is part of Auxio. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.ui - -import androidx.annotation.IdRes -import org.oxycblt.auxio.R -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.Song - -/** - * The enum for the current sort state. - * This enum is semantic depending on the context it is used. Documentation describing each - * sorting functions behavior can be found in the function definition. - * @param itemId Menu ID associated with this enum - * @author OxygenCobalt - */ -enum class SortMode(@IdRes val itemId: Int) { - ASCENDING(R.id.option_sort_asc), - DESCENDING(R.id.option_sort_dsc), - ARTIST(R.id.option_sort_artist), - ALBUM(R.id.option_sort_album), - YEAR(R.id.option_sort_year); - - /** - * Sort a list of songs. - * - * **Behavior:** - * - [ASCENDING]: By name after article, ascending - * - [DESCENDING]: By name after article, descending - * - [ARTIST]: Grouped by album and then sorted [ASCENDING] based off the artist name. - * - [ALBUM]: Grouped by album and sorted [ASCENDING] - * - [YEAR]: Grouped by album and sorted by year - * - * The grouping mode for songs in an album will be by track, [ASCENDING]. - * @see sortAlbums - */ - fun sortSongs(songs: Collection): List { - return when (this) { - ASCENDING -> songs.sortedWith( - compareBy(String.CASE_INSENSITIVE_ORDER) { song -> - song.name.sliceArticle() - } - ) - - DESCENDING -> songs.sortedWith( - compareByDescending(String.CASE_INSENSITIVE_ORDER) { song -> - song.name.sliceArticle() - } - ) - - else -> sortAlbums(songs.groupBy { it.album }.keys).flatMap { album -> - ASCENDING.sortAlbum(album) - } - } - } - - /** - * Sort a list of albums. - * - * **Behavior:** - * - [ASCENDING]: By name after article, ascending - * - [DESCENDING]: By name after article, descending - * - [ARTIST]: Grouped by artist and sorted [ASCENDING] - * - [ALBUM]: [ASCENDING] - * - [YEAR]: Sorted by year - * - * The grouping mode for albums in an artist will be [YEAR]. - */ - fun sortAlbums(albums: Collection): List { - return when (this) { - ASCENDING, DESCENDING -> sortParents(albums) - - ARTIST -> ASCENDING.sortParents(albums.groupBy { it.artist }.keys) - .flatMap { YEAR.sortAlbums(it.albums) } - - ALBUM -> ASCENDING.sortParents(albums) - - YEAR -> albums.sortedByDescending { it.year } - } - } - - /** - * Sort a generic list of [MusicParent] instances. - * - * **Behavior:** - * - [ASCENDING]: By name after article, ascending - * - [DESCENDING]: By name after article, descending - * - Same parent list is returned otherwise. - */ - fun sortParents(parents: Collection): List { - return when (this) { - ASCENDING -> parents.sortedWith( - compareBy(String.CASE_INSENSITIVE_ORDER) { model -> - model.resolvedName.sliceArticle() - } - ) - - DESCENDING -> parents.sortedWith( - compareByDescending(String.CASE_INSENSITIVE_ORDER) { model -> - model.resolvedName.sliceArticle() - } - ) - - else -> parents.toList() - } - } - - /** - * Sort the songs in an album. - * - * **Behavior:** - * - [ASCENDING]: By track, ascending - * - [DESCENDING]: By track, descending - * - Same song list is returned otherwise. - */ - fun sortAlbum(album: Album): List { - return when (this) { - ASCENDING -> album.songs.sortedBy { it.track } - DESCENDING -> album.songs.sortedByDescending { it.track } - else -> album.songs - } - } - - /** - * Sort the songs in an artist. - * @see sortSongs - */ - fun sortArtist(artist: Artist): List { - return sortSongs(artist.songs) - } - - /** - * Sort the songs in a genre. - * @see sortSongs - */ - fun sortGenre(genre: Genre): List { - return sortSongs(genre.songs) - } - - /** - * Converts this mode into an integer constant. Use this when writing a [SortMode] - * to storage, as it will be more efficent. - */ - fun toInt(): Int { - return when (this) { - ASCENDING -> CONST_ASCENDING - DESCENDING -> CONST_DESCENDING - ARTIST -> CONST_ARTIST - ALBUM -> CONST_ALBUM - YEAR -> CONST_YEAR - } - } - - companion object { - private const val CONST_ASCENDING = 0xA10C - private const val CONST_DESCENDING = 0xA10D - private const val CONST_ARTIST = 0xA10E - private const val CONST_ALBUM = 0xA10F - private const val CONST_YEAR = 0xA110 - - /** - * Returns a [SortMode] depending on the integer constant, use this when restoring - * a [SortMode] from storage. - */ - fun fromInt(value: Int): SortMode? { - return when (value) { - CONST_ASCENDING -> ASCENDING - CONST_DESCENDING -> DESCENDING - CONST_ARTIST -> ARTIST - CONST_ALBUM -> ALBUM - CONST_YEAR -> YEAR - else -> null - } - } - - /** - * Convert a menu [id] to an instance of [SortMode]. - */ - fun fromId(@IdRes id: Int): SortMode? { - return when (id) { - ASCENDING.itemId -> ASCENDING - DESCENDING.itemId -> DESCENDING - ARTIST.itemId -> ARTIST - ALBUM.itemId -> ALBUM - YEAR.itemId -> YEAR - else -> null - } - } - } -} - -/** - * Slice a string so that any preceding articles like The/A(n) are truncated. - * This is hilariously anglo-centric, but its mostly for MediaStore compat and hopefully - * shouldn't run with other languages. - */ -fun String.sliceArticle(): String { - if (length > 5 && startsWith("the ", true)) { - return slice(4..lastIndex) - } - - if (length > 4 && startsWith("an ", true)) { - return slice(3..lastIndex) - } - - if (length > 3 && startsWith("a ", true)) { - return slice(2..lastIndex) - } - - return this -} diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt index 7f0244930..35c0213d2 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt @@ -24,7 +24,6 @@ import androidx.annotation.LayoutRes import org.oxycblt.auxio.R import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.system.PlaybackService -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newBroadcastIntent import org.oxycblt.auxio.util.newMainIntent @@ -58,7 +57,6 @@ private fun RemoteViews.applyCover(context: Context, state: WidgetState): Remote R.id.widget_cover, context.getString(R.string.desc_album_cover, state.song.album.name) ) } else { - logD("WHY ARE YOU NOT WORKING") setImageViewResource(R.id.widget_cover, R.drawable.ic_widget_album) setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover)) } diff --git a/app/src/main/res/layout/item_detail.xml b/app/src/main/res/layout/item_detail.xml index 9d06e7658..21e2f021a 100644 --- a/app/src/main/res/layout/item_detail.xml +++ b/app/src/main/res/layout/item_detail.xml @@ -70,7 +70,6 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_small" android:text="@string/lbl_shuffle" - android:clipToPadding="false" app:layout_constraintBottom_toBottomOf="@+id/detail_play_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/detail_play_button" diff --git a/app/src/main/res/menu/menu_detail_sort.xml b/app/src/main/res/menu/menu_detail_sort.xml index 3b7440e1a..deb101207 100644 --- a/app/src/main/res/menu/menu_detail_sort.xml +++ b/app/src/main/res/menu/menu_detail_sort.xml @@ -2,11 +2,8 @@ - + android:id="@+id/option_sort_name" + android:title="@string/lbl_sort_name" /> @@ -17,4 +14,8 @@ android:id="@+id/option_sort_year" android:title="@string/lbl_sort_year" /> + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_home.xml b/app/src/main/res/menu/menu_home.xml index 17e5a6f2e..e0404228e 100644 --- a/app/src/main/res/menu/menu_home.xml +++ b/app/src/main/res/menu/menu_home.xml @@ -16,11 +16,8 @@ - + android:id="@+id/option_sort_name" + android:title="@string/lbl_sort_name" /> @@ -31,6 +28,10 @@ android:id="@+id/option_sort_year" android:title="@string/lbl_sort_year" /> + + + diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index c00e52ab8..dbdc4cf40 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -6,6 +6,26 @@ + + + + + + + + + + + + + + + + + + + + @string/set_theme_auto @string/set_theme_day diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index def3aed06..a17994a2b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ All Sort + Name Ascending Descending Artist diff --git a/app/src/main/res/values/styles_ui.xml b/app/src/main/res/values/styles_ui.xml index a38ac8aa2..bc59d4d1b 100644 --- a/app/src/main/res/values/styles_ui.xml +++ b/app/src/main/res/values/styles_ui.xml @@ -165,4 +165,32 @@ @dimen/size_play_fab_icon @dimen/size_btn_large + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/prefs_main.xml b/app/src/main/res/xml/prefs_main.xml index b51d62963..fa13139de 100644 --- a/app/src/main/res/xml/prefs_main.xml +++ b/app/src/main/res/xml/prefs_main.xml @@ -16,7 +16,7 @@ @@ -130,7 +130,7 @@