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 @@