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.
This commit is contained in:
OxygenCobalt 2021-11-21 15:45:20 -07:00
parent e697908a2f
commit 06a7d8258b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
22 changed files with 406 additions and 318 deletions

View file

@ -49,6 +49,8 @@ abstract class AuxioFetcher : Fetcher {
* https://github.com/kabouzeid/Phonograph
*/
protected fun createMosaic(context: Context, streams: List<InputStream>): FetchResult? {
logD("idiot")
if (streams.size < 4) {
return streams.getOrNull(0)?.let { stream ->
return SourceResult(

View file

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

View file

@ -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 <T : Any, R : Any> Iterable<T>.mapN(n: Int, transform: (T) -> R?): List<R> {
private inline fun <T : Any, R : Any> Collection<T>.mapAtMost(n: Int, transform: (T) -> R?): List<R> {
val until = min(size, n)
val out = mutableListOf<R>()
for (item in this) {
if (out.size >= n) {
if (out.size >= until) {
break
}

View file

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

View file

@ -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()
}

View file

@ -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<BaseModel>())
val albumData: LiveData<List<BaseModel>> get() = mAlbumData
data class MenuConfig(val anchor: View, val sortMode: SortMode)
data class MenuConfig(val anchor: View, val sortMode: Sort)
private val mShowMenu = MutableLiveData<MenuConfig?>(null)
val showMenu: LiveData<MenuConfig?> = 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(

View file

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

View file

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

View file

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

View file

@ -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()
}
}

View file

@ -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")
}

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Song>): List<Song> {
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<Album>): List<Album> {
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 <T : MusicParent> sortParents(parents: Collection<T>): List<T> {
return parents.stringSort { it.resolvedName }
}
/**
* Sort the songs in an album.
* @see sortSongs
*/
fun sortAlbum(album: Album): List<Song> {
return album.songs.intSort { it.track }
}
/**
* Sort the songs in an artist.
* @see sortSongs
*/
fun sortArtist(artist: Artist): List<Song> {
return sortSongs(artist.songs)
}
/**
* Sort the songs in a genre.
* @see sortSongs
*/
fun sortGenre(genre: Genre): List<Song> {
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 <T : Music> Collection<T>.stringSort(
asc: Boolean = isAscending,
selector: (T) -> String
): List<T> {
// 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 <T : Music> Collection<T>.intSort(
asc: Boolean = isAscending,
selector: (T) -> Int,
): List<T> {
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Song>): List<Song> {
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<Album>): List<Album> {
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 <T : MusicParent> sortParents(parents: Collection<T>): List<T> {
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<Song> {
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<Song> {
return sortSongs(artist.songs)
}
/**
* Sort the songs in a genre.
* @see sortSongs
*/
fun sortGenre(genre: Genre): List<Song> {
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
}

View file

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

View file

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

View file

@ -2,11 +2,8 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/option_sort_asc"
android:title="@string/lbl_sort_asc" />
<item
android:id="@+id/option_sort_dsc"
android:title="@string/lbl_sort_dsc" />
android:id="@+id/option_sort_name"
android:title="@string/lbl_sort_name" />
<item
android:id="@+id/option_sort_artist"
android:title="@string/lbl_sort_artist" />
@ -17,4 +14,8 @@
android:id="@+id/option_sort_year"
android:title="@string/lbl_sort_year" />
</group>
<group android:checkableBehavior="all">
<item android:id="@+id/option_sort_asc"
android:title="@string/lbl_sort_asc" />
</group>
</menu>

View file

@ -16,11 +16,8 @@
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/option_sort_asc"
android:title="@string/lbl_sort_asc" />
<item
android:id="@+id/option_sort_dsc"
android:title="@string/lbl_sort_dsc" />
android:id="@+id/option_sort_name"
android:title="@string/lbl_sort_name" />
<item
android:id="@+id/option_sort_artist"
android:title="@string/lbl_sort_artist" />
@ -31,6 +28,10 @@
android:id="@+id/option_sort_year"
android:title="@string/lbl_sort_year" />
</group>
<group android:checkableBehavior="all">
<item android:id="@+id/option_sort_asc"
android:title="@string/lbl_sort_asc" />
</group>
</menu>
</item>

View file

@ -6,6 +6,26 @@
<attr name="entryValues" format="reference" />
</declare-styleable>
<declare-styleable name="SlidingUpPanelLayout">
<attr name="umanoPanelHeight" format="dimension" />
<attr name="umanoShadowHeight" format="dimension" />
<attr name="umanoParallaxOffset" format="dimension" />
<attr name="umanoFadeColor" format="color" />
<attr name="umanoFlingVelocity" format="integer" />
<attr name="umanoDragView" format="reference" />
<attr name="umanoScrollableView" format="reference" />
<attr name="umanoOverlay" format="boolean"/>
<attr name="umanoClipPanel" format="boolean"/>
<attr name="umanoAnchorPoint" format="float" />
<attr name="umanoInitialState" format="enum">
<enum name="expanded" value="0" />
<enum name="collapsed" value="1" />
<enum name="anchored" value="2" />
<enum name="hidden" value="3" />
</attr>
<attr name="umanoScrollInterpolator" format="reference" />
</declare-styleable>
<string-array name="entires_theme">
<item>@string/set_theme_auto</item>
<item>@string/set_theme_day</item>

View file

@ -22,6 +22,7 @@
<string name="lbl_filter_all">All</string>
<string name="lbl_sort">Sort</string>
<string name="lbl_sort_name">Name</string>
<string name="lbl_sort_asc">Ascending</string>
<string name="lbl_sort_dsc">Descending</string>
<string name="lbl_sort_artist">Artist</string>

View file

@ -165,4 +165,32 @@
<item name="maxImageSize">@dimen/size_play_fab_icon</item>
<item name="fabCustomSize">@dimen/size_btn_large</item>
</style>
<style name="Widget.MaterialComponents.Button.IconOnly">
<item name="iconPadding">0dp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:paddingLeft">12dp</item>
<item name="android:paddingRight">12dp</item>
<item name="android:minWidth">@dimen/size_btn_small</item>
<item name="android:minHeight">@dimen/size_btn_small</item>
<item name="iconGravity">textStart</item>
</style>
<style name="Widget.MaterialComponents.Button.UnelevatedButton.IconOnly" parent="Widget.MaterialComponents.Button.IconOnly">
<item name="android:stateListAnimator">@animator/mtrl_btn_unelevated_state_list_anim</item>
<item name="elevation">0dp</item>
</style>
<style name="Widget.MaterialComponents.Button.TextButton.IconOnly" parent="Widget.MaterialComponents.Button.UnelevatedButton.IconOnly">
<item name="android:textColor">@color/mtrl_text_btn_text_color_selector</item>
<item name="iconTint">?attr/colorControlNormal</item>
<item name="backgroundTint">@color/mtrl_btn_text_btn_bg_color_selector</item>
<item name="rippleColor">?attr/colorControlHighlight</item>
</style>
<style name="Widget.MaterialComponents.Button.OutlinedButton.IconOnly" parent="Widget.MaterialComponents.Button.TextButton.IconOnly">
<item name="strokeColor">@color/mtrl_btn_stroke_color_selector</item>
<item name="strokeWidth">@dimen/mtrl_btn_stroke_size</item>
</style>
</resources>

View file

@ -16,7 +16,7 @@
<Preference
app:icon="@drawable/ic_accent"
app:key="KEY_ACCENT3"
app:key="auxio_accent"
app:title="@string/set_accent" />
<SwitchPreferenceCompat
@ -35,7 +35,7 @@
<Preference
app:iconSpaceReserved="false"
app:key="KEY_LIB_TABS"
app:key="auxio_lib_tabs"
app:summary="@string/set_lib_tabs_desc"
app:title="@string/set_lib_tabs" />
@ -130,7 +130,7 @@
<Preference
app:iconSpaceReserved="false"
app:key="KEY_SAVE_STATE"
app:key="auxio_save_state"
app:summary="@string/set_save_desc"
app:title="@string/set_save" />