ui: rework sorting again

Rework Sort again into a new class that leverages a better Mode design
and static comparator instances.

This somewhat improves efficiency, but is also far easier to work with
and has far less footguns with adding new sorts.
This commit is contained in:
OxygenCobalt 2022-06-22 11:08:58 -06:00
parent 86010e896e
commit 55f9d4c819
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
21 changed files with 346 additions and 348 deletions

View file

@ -22,6 +22,7 @@ design guidelines
- Migrated preferences from shared object to utility
- Removed 2.0.0 compat code
- Updated ExoPlayer to 2.18.0
- Reworked sorting to be even more efficient
## v2.4.0

View file

@ -42,6 +42,7 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuFragment
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.collect
@ -143,11 +144,16 @@ class AlbumDetailFragment :
override fun onShowSortMenu(anchor: View) {
menu(anchor, R.menu.menu_album_sort) {
val sort = detailModel.albumSort
requireNotNull(menu.findItem(sort.itemId)).isChecked = true
requireNotNull(menu.findItem(sort.mode.itemId)).isChecked = true
requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked
detailModel.albumSort = requireNotNull(sort.assignId(item.itemId))
detailModel.albumSort =
if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked)
} else {
sort.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
}
true
}
}

View file

@ -40,6 +40,7 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuFragment
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
@ -138,11 +139,18 @@ class ArtistDetailFragment :
override fun onShowSortMenu(anchor: View) {
menu(anchor, R.menu.menu_artist_sort) {
val sort = detailModel.artistSort
requireNotNull(menu.findItem(sort.itemId)).isChecked = true
requireNotNull(menu.findItem(sort.mode.itemId)).isChecked = true
requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked
detailModel.artistSort = requireNotNull(sort.assignId(item.itemId))
detailModel.artistSort =
if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked)
} else {
sort.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
}
true
}
}

View file

@ -93,6 +93,7 @@ class DetailViewModel(application: Application) :
var artistSort: Sort
get() = settings.detailArtistSort
set(value) {
logD(value)
settings.detailArtistSort = value
currentArtist.value?.let(::refreshArtistData)
}
@ -233,7 +234,7 @@ class DetailViewModel(application: Application) :
logD("Refreshing artist data")
val data = mutableListOf<Item>(artist)
data.add(Header(-2, R.string.lbl_albums))
data.addAll(Sort.ByYear(false).albums(artist.albums))
data.addAll(Sort(Sort.Mode.ByYear, false).albums(artist.albums))
data.add(SortHeader(-3, R.string.lbl_songs))
data.addAll(artistSort.songs(artist.songs))
_artistData.value = data.toList()

View file

@ -41,6 +41,7 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuFragment
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
@ -139,11 +140,16 @@ class GenreDetailFragment :
override fun onShowSortMenu(anchor: View) {
menu(anchor, R.menu.menu_genre_sort) {
val sort = detailModel.genreSort
requireNotNull(menu.findItem(sort.itemId)).isChecked = true
requireNotNull(menu.findItem(sort.mode.itemId)).isChecked = true
requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked
detailModel.genreSort = requireNotNull(sort.assignId(item.itemId))
detailModel.genreSort =
if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked)
} else {
sort.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
}
true
}
}

View file

@ -54,6 +54,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collect
@ -65,7 +66,6 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logTraceOrThrow
import org.oxycblt.auxio.util.textSafe
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each
@ -173,19 +173,17 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
R.id.option_sort_asc -> {
item.isChecked = !item.isChecked
homeModel.updateCurrentSort(
unlikelyToBeNull(
homeModel
.getSortForDisplay(homeModel.currentTab.value)
.ascending(item.isChecked)))
homeModel
.getSortForDisplay(homeModel.currentTab.value)
.withAscending(item.isChecked))
}
else -> {
// Sorting option was selected, mark it as selected and update the mode
item.isChecked = true
homeModel.updateCurrentSort(
unlikelyToBeNull(
homeModel
.getSortForDisplay(homeModel.currentTab.value)
.assignId(item.itemId)))
homeModel
.getSortForDisplay(homeModel.currentTab.value)
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
}
}
@ -248,7 +246,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
val toHighlight = homeModel.getSortForDisplay(displayMode)
for (option in sortMenu) {
if (option.itemId == toHighlight.itemId) {
if (option.itemId == toHighlight.mode.itemId) {
option.isChecked = true
}

View file

@ -56,21 +56,21 @@ class AlbumListFragment : HomeListFragment<Album>() {
val album = homeModel.albums.value[pos]
// Change how we display the popup depending on the mode.
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS).mode) {
// By Name -> Use Name
is Sort.ByName -> album.sortName.first().uppercase()
is Sort.Mode.ByName -> album.sortName.first().uppercase()
// By Artist -> Use Artist Name
is Sort.ByArtist -> album.artist.sortName?.run { first().uppercase() }
is Sort.Mode.ByArtist -> album.artist.sortName?.run { first().uppercase() }
// Year -> Use Full Year
is Sort.ByYear -> album.year?.toString()
is Sort.Mode.ByYear -> album.year?.toString()
// Duration -> Use formatted duration
is Sort.ByDuration -> album.durationSecs.formatDuration(false)
is Sort.Mode.ByDuration -> album.durationSecs.formatDuration(false)
// Count -> Use song count
is Sort.ByCount -> album.songs.size.toString()
is Sort.Mode.ByCount -> album.songs.size.toString()
// Unsupported sort, error gracefully
else -> null

View file

@ -56,15 +56,15 @@ class ArtistListFragment : HomeListFragment<Artist>() {
val artist = homeModel.artists.value[pos]
// Change how we display the popup depending on the mode.
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ARTISTS)) {
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ARTISTS).mode) {
// By Name -> Use Name
is Sort.ByName -> artist.sortName?.run { first().uppercase() }
is Sort.Mode.ByName -> artist.sortName?.run { first().uppercase() }
// Duration -> Use formatted duration
is Sort.ByDuration -> artist.durationSecs.formatDuration(false)
is Sort.Mode.ByDuration -> artist.durationSecs.formatDuration(false)
// Count -> Use song count
is Sort.ByCount -> artist.songs.size.toString()
is Sort.Mode.ByCount -> artist.songs.size.toString()
// Unsupported sort, error gracefully
else -> null

View file

@ -56,15 +56,15 @@ class GenreListFragment : HomeListFragment<Genre>() {
val genre = homeModel.genres.value[pos]
// Change how we display the popup depending on the mode.
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_GENRES)) {
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_GENRES).mode) {
// By Name -> Use Name
is Sort.ByName -> genre.sortName?.run { first().uppercase() }
is Sort.Mode.ByName -> genre.sortName?.run { first().uppercase() }
// Duration -> Use formatted duration
is Sort.ByDuration -> genre.durationSecs.formatDuration(false)
is Sort.Mode.ByDuration -> genre.durationSecs.formatDuration(false)
// Count -> Use song count
is Sort.ByCount -> genre.songs.size.toString()
is Sort.Mode.ByCount -> genre.songs.size.toString()
// Unsupported sort, error gracefully
else -> null

View file

@ -27,8 +27,8 @@ import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.ui.SyncBackingData
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
@ -60,21 +60,21 @@ class SongListFragment : HomeListFragment<Song>() {
// Change how we display the popup depending on the mode.
// Note: We don't use the more correct individual artist name here, as sorts are largely
// based off the names of the parent objects and not the child objects.
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS).mode) {
// Name -> Use name
is Sort.ByName -> song.sortName.first().uppercase()
is Sort.Mode.ByName -> song.sortName.first().uppercase()
// Artist -> Use Artist Name
is Sort.ByArtist -> song.album.artist.sortName?.run { first().uppercase() }
is Sort.Mode.ByArtist -> song.album.artist.sortName?.run { first().uppercase() }
// Album -> Use Album Name
is Sort.ByAlbum -> song.album.sortName.first().uppercase()
is Sort.Mode.ByAlbum -> song.album.sortName.first().uppercase()
// Year -> Use Full Year
is Sort.ByYear -> song.album.year?.toString()
is Sort.Mode.ByYear -> song.album.year?.toString()
// Duration -> Use formatted duration
is Sort.ByDuration -> song.durationSecs.formatDuration(false)
is Sort.Mode.ByDuration -> song.durationSecs.formatDuration(false)
// Unsupported sort, error gracefully
else -> null

View file

@ -87,7 +87,7 @@ private constructor(
private val artist: Artist,
) : BaseFetcher() {
override suspend fun fetch(): FetchResult? {
val albums = Sort.ByName(true).albums(artist.albums)
val albums = Sort(Sort.Mode.ByName, true).albums(artist.albums)
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
return createMosaic(context, results, size)
}
@ -116,7 +116,7 @@ private constructor(
// albums normally.
val artists = genre.songs.groupBy { it.album.artist.id }.keys
val albums =
Sort.ByName(true).albums(genre.songs.groupBy { it.album }.keys).run {
Sort(Sort.Mode.ByName, true).albums(genre.songs.groupBy { it.album }.keys).run {
if (artists.size > 4) {
distinctBy { it.artist.rawName }
} else {

View file

@ -257,7 +257,7 @@ class Indexer {
.toMutableList()
// Ensure that sorting order is consistent so that grouping is also consistent.
Sort.ByName(true).songsInPlace(songs)
Sort(Sort.Mode.ByName, true).songsInPlace(songs)
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
@ -287,16 +287,8 @@ class Indexer {
// Use the song with the latest year as our metadata song.
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
// weird years like "0" wont show up if there are alternatives.
// Note: Normally we could want to use something like maxByWith, but apparently
// that does not exist in the kotlin stdlib yet.
val comparator = Sort.NullableComparator<Int>()
var templateSong = albumSongs[0]
for (i in 1..albumSongs.lastIndex) {
val candidate = albumSongs[i]
if (comparator.compare(templateSong.track, candidate.track) < 0) {
templateSong = candidate
}
}
val templateSong =
albumSongs.maxWith(compareBy(Sort.Mode.NULLABLE_INT_COMPARATOR) { it._year })
albums.add(
Album(

View file

@ -37,8 +37,10 @@ class MediaButtonReceiver : BroadcastReceiver() {
val playbackManager = PlaybackStateManager.getInstance()
if (playbackManager.song != null) {
// We have a song, so we can assume that the service will start a foreground state.
// At least, I hope. Again, *this is why we don't do this, I cannot describe how
// stupid this is with the state of foreground services on modern android*
// At least, I hope. Again, *this is why we don't do this*. I cannot describe how
// stupid this is with the state of foreground services on modern android. One
// wrong action at the wrong time will result in the app crashing, and there is
// nothing I can do about it.
intent.component = ComponentName(context, PlaybackService::class.java)
ContextCompat.startForegroundService(context, intent)
}

View file

@ -38,8 +38,8 @@ import org.oxycblt.auxio.util.logD
/**
* The component managing the [MediaSessionCompat] instance.
*
* I really don't like how I have to do this, but until I can feasibly work with the ExoPlayer queue
* system using something like MediaSessionConnector is more or less impossible.
* I really don't like how I have to do this, but until I can work with the ExoPlayer queue system
* using something like MediaSessionConnector is more or less impossible.
*
* @author OxygenCobalt
*/
@ -242,6 +242,12 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
val state =
PlaybackStateCompat.Builder()
.setActions(ACTIONS)
.addCustomAction(
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INC_REPEAT_MODE,
context.getString(R.string.desc_change_repeat),
R.drawable.ic_remote_repeat_off)
.build())
.setBufferedPosition(player.bufferedPosition)
state.setState(PlaybackStateCompat.STATE_NONE, player.bufferedPosition, 1.0f)

View file

@ -64,8 +64,8 @@ import org.oxycblt.auxio.widgets.WidgetProvider
* - Headset management
* - Widgets
*
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], so
* therefore there's no need to bind to it to deliver commands.
* This service relies on [PlaybackStateManager.Callback] and [Settings.Callback], so therefore
* there's no need to bind to it to deliver commands.
*
* TODO: Android Auto
*

View file

@ -74,7 +74,7 @@ class SearchViewModel(application: Application) :
// Searching can be quite expensive, so get on a co-routine
viewModelScope.launch {
val sort = Sort.ByName(true)
val sort = Sort(Sort.Mode.ByName, true)
val results = mutableListOf<Item>()
// Note: a filter mode of null means to not filter at all.

View file

@ -239,7 +239,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE))
?: Sort.ByName(true)
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode)
@ -252,7 +252,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE))
?: Sort.ByName(true)
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode)
@ -265,7 +265,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE))
?: Sort.ByName(true)
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode)
@ -278,7 +278,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE))
?: Sort.ByName(true)
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode)
@ -291,19 +291,20 @@ class Settings(private val context: Context, private val callback: Callback? = n
get() {
var sort =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_album_sort), Int.MIN_VALUE))
?: Sort.ByDisc(true)
inner.getInt(
context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByDisc, true)
// Correct legacy album sort modes to Disc
if (sort is Sort.ByName) {
sort = Sort.ByDisc(sort.isAscending)
if (sort.mode is Sort.Mode.ByName) {
sort = sort.withMode(Sort.Mode.ByDisc)
}
return sort
}
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_album_sort), value.intCode)
putInt(context.getString(R.string.set_key_detail_album_sort), value.intCode)
apply()
}
}
@ -312,11 +313,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
var detailArtistSort: Sort
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_artist_sort), Int.MIN_VALUE))
?: Sort.ByYear(false)
inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByYear, false)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode)
putInt(context.getString(R.string.set_key_detail_artist_sort), value.intCode)
apply()
}
}
@ -325,11 +326,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
var detailGenreSort: Sort
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_genre_sort), Int.MIN_VALUE))
?: Sort.ByName(true)
inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_genre_sort), value.intCode)
putInt(context.getString(R.string.set_key_detail_genre_sort), value.intCode)
apply()
}
}

View file

@ -18,6 +18,7 @@
package org.oxycblt.auxio.ui
import androidx.annotation.IdRes
import kotlin.UnsupportedOperationException
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album
@ -25,14 +26,14 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logEOrThrow
/**
* A data class representing the sort modes used in Auxio.
*
* Sorting can be done by Name, Artist, Album, and others. 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]).
* certain sorts cannot be easily applied to them (For Example, [Mode.ByArtist] and [Mode.ByYear] or
* [Mode.ByAlbum]).
*
* Internally, sorts are saved as an integer in the following format
*
@ -42,14 +43,13 @@ import org.oxycblt.auxio.util.logEOrThrow
* representing whether this sort is ascending or descending.
*
* @author OxygenCobalt
*
* TODO: Make comparators static instances
*
* TODO: Separate sort mode and ascending state
*/
sealed class Sort(open val isAscending: Boolean) {
protected abstract val sortIntCode: Int
abstract val itemId: Int
data class Sort(val mode: Mode, val isAscending: Boolean) {
fun withAscending(new: Boolean) = Sort(mode, new)
fun withMode(new: Mode) = Sort(new, isAscending)
val intCode: Int
get() = mode.intCode.shl(1) or if (isAscending) 1 else 0
fun songs(songs: Collection<Song>): List<Song> {
val mutable = songs.toMutableList()
@ -75,336 +75,312 @@ sealed class Sort(open val isAscending: Boolean) {
return mutable
}
open fun songsInPlace(songs: MutableList<Song>) {
logEOrThrow("This sort is not supported for songs")
fun songsInPlace(songs: MutableList<Song>) {
songs.sortWith(mode.getSongComparator(isAscending))
}
open fun albumsInPlace(albums: MutableList<Album>) {
logEOrThrow("This sort is not supported for albums")
fun albumsInPlace(albums: MutableList<Album>) {
albums.sortWith(mode.getAlbumComparator(isAscending))
}
open fun artistsInPlace(artists: MutableList<Artist>) {
logEOrThrow("This sort is not supported for artists")
fun artistsInPlace(artists: MutableList<Artist>) {
artists.sortWith(mode.getArtistComparator(isAscending))
}
open fun genresInPlace(genres: MutableList<Genre>) {
logEOrThrow("This sort is not supported for genres")
fun genresInPlace(genres: MutableList<Genre>) {
genres.sortWith(mode.getGenreComparator(isAscending))
}
/**
* Apply [newIsAscending] to the status of this sort.
* @return A new [Sort] with the value of [newIsAscending] applied.
*/
abstract fun ascending(newIsAscending: Boolean): Sort
sealed class Mode {
abstract val intCode: Int
abstract val itemId: Int
/** Sort by the names of an item */
class ByName(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_NAME
override val itemId: Int
get() = R.id.option_sort_name
override fun songsInPlace(songs: MutableList<Song>) {
songs.sortWith(compareByDynamic(NameComparator()) { it })
open fun getSongComparator(ascending: Boolean): Comparator<Song> {
throw UnsupportedOperationException()
}
override fun albumsInPlace(albums: MutableList<Album>) {
albums.sortWith(compareByDynamic(NameComparator()) { it })
open fun getAlbumComparator(ascending: Boolean): Comparator<Album> {
throw UnsupportedOperationException()
}
override fun artistsInPlace(artists: MutableList<Artist>) {
artists.sortWith(compareByDynamic(NameComparator()) { it })
open fun getArtistComparator(ascending: Boolean): Comparator<Artist> {
throw UnsupportedOperationException()
}
override fun genresInPlace(genres: MutableList<Genre>) {
genres.sortWith(compareByDynamic(NameComparator()) { it })
open fun getGenreComparator(ascending: Boolean): Comparator<Genre> {
throw UnsupportedOperationException()
}
override fun ascending(newIsAscending: Boolean) = ByName(newIsAscending)
}
/** Sort by the names of an item */
object ByName : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_NAME
/** Sort by the album of an item, only supported by [Song] */
class ByAlbum(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_ALBUM
override val itemId: Int
get() = R.id.option_sort_name
override val itemId: Int
get() = R.id.option_sort_album
override fun getSongComparator(ascending: Boolean) =
compareByDynamic(ascending, BasicComparator.SONG)
override fun songsInPlace(songs: MutableList<Song>) {
songs.sortWith(
override fun getAlbumComparator(ascending: Boolean) =
compareByDynamic(ascending, BasicComparator.ALBUM)
override fun getArtistComparator(ascending: Boolean) =
compareByDynamic(ascending, BasicComparator.ARTIST)
override fun getGenreComparator(ascending: Boolean) =
compareByDynamic(ascending, BasicComparator.GENRE)
}
/** Sort by the album of an item, only supported by [Song] */
object ByAlbum : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_ALBUM
override val itemId: Int
get() = R.id.option_sort_album
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator(
compareByDynamic(NameComparator()) { it.album },
compareBy(NullableComparator()) { it.disc },
compareBy(NullableComparator()) { it.track },
compareBy(NameComparator()) { it }))
compareByDynamic(ascending, BasicComparator.ALBUM) { it.album },
compareBy(NULLABLE_INT_COMPARATOR) { it.disc },
compareBy(NULLABLE_INT_COMPARATOR) { it.track },
compareBy(BasicComparator.SONG))
}
override fun ascending(newIsAscending: Boolean) = ByAlbum(newIsAscending)
}
/** Sort by the artist of an item, only supported by [Album] and [Song] */
object ByArtist : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_ARTIST
/** Sort by the artist of an item, only supported by [Album] and [Song] */
class ByArtist(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_ARTIST
override val itemId: Int
get() = R.id.option_sort_artist
override val itemId: Int
get() = R.id.option_sort_artist
override fun songsInPlace(songs: MutableList<Song>) {
songs.sortWith(
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator(
compareByDynamic(NameComparator()) { it.album.artist },
compareByDescending(NullableComparator()) { it.album.year },
compareByDescending(NameComparator()) { it.album },
compareBy(NullableComparator()) { it.disc },
compareBy(NullableComparator()) { it.track },
compareBy(NameComparator()) { it }))
}
compareByDynamic(ascending, BasicComparator.ARTIST) { it.album.artist },
compareByDescending(NULLABLE_INT_COMPARATOR) { it.album.year },
compareByDescending(BasicComparator.ALBUM) { it.album },
compareBy(NULLABLE_INT_COMPARATOR) { it.disc },
compareBy(NULLABLE_INT_COMPARATOR) { it.track },
compareBy(BasicComparator.SONG))
override fun albumsInPlace(albums: MutableList<Album>) {
albums.sortWith(
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator(
compareByDynamic(NameComparator()) { it.artist },
compareByDescending(NullableComparator()) { it.year },
compareBy(NameComparator()) { it }))
compareByDynamic(ascending, BasicComparator.ARTIST) { it.artist },
compareByDescending(NULLABLE_INT_COMPARATOR) { it.year },
compareBy(BasicComparator.ALBUM))
}
override fun ascending(newIsAscending: Boolean) = ByArtist(newIsAscending)
}
/** Sort by the year of an item, only supported by [Album] and [Song] */
object ByYear : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_YEAR
/** Sort by the year of an item, only supported by [Album] and [Song] */
class ByYear(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_YEAR
override val itemId: Int
get() = R.id.option_sort_year
override val itemId: Int
get() = R.id.option_sort_year
override fun songsInPlace(songs: MutableList<Song>) {
songs.sortWith(
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator(
compareByDynamic(NullableComparator()) { it.album.year },
compareByDescending(NameComparator()) { it.album },
compareBy(NullableComparator()) { it.disc },
compareBy(NullableComparator()) { it.track },
compareBy(NameComparator()) { it }))
}
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.album.year },
compareByDescending(BasicComparator.ALBUM) { it.album },
compareBy(NULLABLE_INT_COMPARATOR) { it.disc },
compareBy(NULLABLE_INT_COMPARATOR) { it.track },
compareBy(BasicComparator.SONG))
override fun albumsInPlace(albums: MutableList<Album>) {
albums.sortWith(
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator(
compareByDynamic(NullableComparator()) { it.year },
compareBy(NameComparator()) { it }))
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.year },
compareBy(BasicComparator.ALBUM))
}
override fun ascending(newIsAscending: Boolean) = ByYear(newIsAscending)
}
/** Sort by the duration of the item. Supports all items. */
object ByDuration : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_DURATION
/** Sort by the duration of the item. Supports all items. */
class ByDuration(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_DURATION
override val itemId: Int
get() = R.id.option_sort_duration
override val itemId: Int
get() = R.id.option_sort_duration
override fun songsInPlace(songs: MutableList<Song>) {
songs.sortWith(
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator(
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
}
compareByDynamic(ascending) { it.durationSecs },
compareBy(BasicComparator.SONG))
override fun albumsInPlace(albums: MutableList<Album>) {
albums.sortWith(
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator(
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
}
compareByDynamic(ascending) { it.durationSecs },
compareBy(BasicComparator.ALBUM))
override fun artistsInPlace(artists: MutableList<Artist>) {
artists.sortWith(
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
MultiComparator(
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
}
compareByDynamic(ascending) { it.durationSecs },
compareBy(BasicComparator.ARTIST))
override fun genresInPlace(genres: MutableList<Genre>) {
genres.sortWith(
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
MultiComparator(
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
compareByDynamic(ascending) { it.durationSecs },
compareBy(BasicComparator.GENRE))
}
override fun ascending(newIsAscending: Boolean) = ByDuration(newIsAscending)
}
/** Sort by the amount of songs. Only applicable to music parents. */
object ByCount : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_COUNT
/** Sort by the amount of songs. Only applicable to music parents. */
class ByCount(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_COUNT
override val itemId: Int
get() = R.id.option_sort_count
override val itemId: Int
get() = R.id.option_sort_count
override fun albumsInPlace(albums: MutableList<Album>) {
albums.sortWith(
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
MultiComparator(
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
}
compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.ALBUM))
override fun artistsInPlace(artists: MutableList<Artist>) {
artists.sortWith(
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
MultiComparator(
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
}
compareByDynamic(ascending) { it.songs.size },
compareBy(BasicComparator.ARTIST))
override fun genresInPlace(genres: MutableList<Genre>) {
genres.sortWith(
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
MultiComparator(
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.GENRE))
}
override fun ascending(newIsAscending: Boolean) = ByCount(newIsAscending)
}
/** Sort by the disc, and then track number of an item. Only supported by [Song]. */
object ByDisc : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_DISC
/**
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use this
* in a main sorting view, as it is not assigned to a particular item ID
*/
class ByDisc(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_DISC
// Not an available option, so no ID is set
override val itemId: Int
get() = R.id.option_sort_disc
override fun songsInPlace(songs: MutableList<Song>) {
songs.sortWith(
override val itemId: Int
get() = R.id.option_sort_disc
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator(
compareByDynamic(NullableComparator()) { it.disc },
compareBy(NullableComparator()) { it.track },
compareBy(NameComparator()) { it }))
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.disc },
compareBy(NULLABLE_INT_COMPARATOR) { it.track },
compareBy(BasicComparator.SONG))
}
override fun ascending(newIsAscending: Boolean) = ByDisc(newIsAscending)
}
/**
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use
* this in a main sorting view, as it is not assigned to a particular item ID
*/
object ByTrack : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_TRACK
/**
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use this
* in a main sorting view, as it is not assigned to a particular item ID
*/
class ByTrack(override val isAscending: Boolean) : Sort(isAscending) {
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_TRACK
override val itemId: Int
get() = R.id.option_sort_track
override val itemId: Int
get() = R.id.option_sort_track
override fun songsInPlace(songs: MutableList<Song>) {
songs.sortWith(
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
MultiComparator(
compareBy(NullableComparator()) { it.disc },
compareByDynamic(NullableComparator()) { it.track },
compareBy(NameComparator()) { it }))
compareBy(NULLABLE_INT_COMPARATOR) { it.disc },
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.track },
compareBy(BasicComparator.SONG))
}
override fun ascending(newIsAscending: Boolean) = ByTrack(newIsAscending)
}
protected inline fun <T : Music, K> compareByDynamic(
ascending: Boolean,
comparator: Comparator<in K>,
crossinline selector: (T) -> K
) =
if (ascending) {
compareBy(comparator, selector)
} else {
compareByDescending(comparator, selector)
}
val intCode: Int
get() = sortIntCode.shl(1) or if (isAscending) 1 else 0
protected fun <T : Music> compareByDynamic(
ascending: Boolean,
comparator: Comparator<in T>
): Comparator<T> = compareByDynamic(ascending, comparator) { it }
/**
* 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_asc -> ascending(!isAscending)
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)
R.id.option_sort_duration -> ByDuration(isAscending)
R.id.option_sort_count -> ByCount(isAscending)
R.id.option_sort_disc -> ByDisc(isAscending)
R.id.option_sort_track -> ByTrack(isAscending)
else -> null
}
}
protected inline fun <T : Music, K : Comparable<K>> compareByDynamic(
ascending: Boolean,
crossinline selector: (T) -> K
) =
if (ascending) {
compareBy(selector)
} else {
compareByDescending(selector)
}
protected inline fun <T : Music, K> compareByDynamic(
comparator: Comparator<in K>,
crossinline selector: (T) -> K
): Comparator<T> {
return if (isAscending) {
compareBy(comparator, selector)
} else {
compareByDescending(comparator, selector)
}
}
protected fun <T : Music> compareBy(comparator: Comparator<T>): Comparator<T> =
compareBy(comparator) { it }
protected inline fun <T : Music, K : Comparable<K>> compareByDynamic(
crossinline selector: (T) -> K
): Comparator<T> {
return if (isAscending) {
compareBy(selector)
} else {
compareByDescending(selector)
}
}
/**
* Chains the given comparators together to form one comparator.
*
* Sorts often need to compare multiple things at once across several hierarchies, with this
* class doing such in a more efficient manner than resorting at multiple intervals or
* grouping items up. Comparators are checked from first to last, with the first comparator
* that returns a non-equal result being propagated upwards.
*/
private class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
private val _comparators = comparators
class NameComparator<T : Music> : Comparator<T> {
override fun compare(a: T, b: T): Int {
val aSortName = a.sortName
val bSortName = b.sortName
return when {
aSortName != null && bSortName != null ->
aSortName.compareTo(bSortName, ignoreCase = true)
aSortName == null && bSortName != null -> -1 // a < b
aSortName == null && bSortName == null -> 0 // a = b
aSortName != null && bSortName == null -> 1 // a < b
else -> error("Unreachable")
override fun compare(a: T?, b: T?): Int {
for (comparator in _comparators) {
val result = comparator.compare(a, b)
if (result != 0) {
return result
}
}
return 0
}
}
}
class NullableComparator<T : Comparable<T>> : Comparator<T?> {
override fun compare(a: T?, b: T?): Int {
return when {
a != null && b != null -> a.compareTo(b)
a == null && b != null -> -1 // a < b
a == null && b == null -> 0 // a = b
a != null && b == null -> 1 // a < b
else -> error("Unreachable")
}
}
}
/**
* Chains the given comparators together to form one comparator.
*
* Sorts often need to compare multiple things at once across several hierarchies, with this
* class doing such in a more efficient manner than resorting at multiple intervals or grouping
* items up. Comparators are checked from first to last, with the first comparator that returns
* a non-equal result being propagated upwards.
*/
class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
private val _comparators = comparators
override fun compare(a: T?, b: T?): Int {
for (comparator in _comparators) {
val result = comparator.compare(a, b)
if (result != 0) {
return result
private class BasicComparator<T : Music> private constructor() : Comparator<T> {
override fun compare(a: T, b: T): Int {
val aSortName = a.sortName
val bSortName = b.sortName
return when {
aSortName != null && bSortName != null ->
aSortName.compareTo(bSortName, ignoreCase = true)
aSortName == null && bSortName != null -> -1 // a < b
aSortName == null && bSortName == null -> 0 // a = b
aSortName != null && bSortName == null -> 1 // a < b
else -> error("Unreachable")
}
}
return 0
companion object {
val SONG: Comparator<Song> = BasicComparator()
val ALBUM: Comparator<Album> = BasicComparator()
val ARTIST: Comparator<Artist> = BasicComparator()
val GENRE: Comparator<Genre> = BasicComparator()
}
}
companion object {
// Exposed as Indexer relies on it at points
val NULLABLE_INT_COMPARATOR =
Comparator<Int?> { a, b ->
when {
a != null && b != null -> a.compareTo(b)
a == null && b != null -> -1 // a < b
a == null && b == null -> 0 // a = b
a != null && b == null -> 1 // a < b
else -> error("Unreachable")
}
}
fun fromItemId(@IdRes itemId: Int) =
when (itemId) {
ByName.itemId -> ByName
ByAlbum.itemId -> ByAlbum
ByArtist.itemId -> ByArtist
ByYear.itemId -> ByYear
ByDuration.itemId -> ByDuration
ByCount.itemId -> ByCount
ByDisc.itemId -> ByDisc
ByTrack.itemId -> ByTrack
else -> null
}
}
}
companion object {
/**
* Convert a sort's integer representation into a [Sort] instance.
*
@ -412,18 +388,20 @@ sealed class Sort(open val isAscending: Boolean) {
*/
fun fromIntCode(value: Int): Sort? {
val isAscending = (value and 1) == 1
val mode =
when (value.shr(1)) {
Mode.ByName.intCode -> Mode.ByName
Mode.ByArtist.intCode -> Mode.ByArtist
Mode.ByAlbum.intCode -> Mode.ByAlbum
Mode.ByYear.intCode -> Mode.ByYear
Mode.ByDuration.intCode -> Mode.ByDuration
Mode.ByCount.intCode -> Mode.ByCount
Mode.ByDisc.intCode -> Mode.ByDisc
Mode.ByTrack.intCode -> Mode.ByTrack
else -> return null
}
return when (value.shr(1)) {
IntegerTable.SORT_BY_NAME -> ByName(isAscending)
IntegerTable.SORT_BY_ARTIST -> ByArtist(isAscending)
IntegerTable.SORT_BY_ALBUM -> ByAlbum(isAscending)
IntegerTable.SORT_BY_YEAR -> ByYear(isAscending)
IntegerTable.SORT_BY_DURATION -> ByDuration(isAscending)
IntegerTable.SORT_BY_COUNT -> ByCount(isAscending)
IntegerTable.SORT_BY_DISC -> ByDisc(isAscending)
IntegerTable.SORT_BY_TRACK -> ByTrack(isAscending)
else -> null
}
return Sort(mode, isAscending)
}
}
}

View file

@ -46,7 +46,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import org.oxycblt.auxio.R

View file

@ -37,8 +37,8 @@ import org.oxycblt.auxio.util.logD
/**
* A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the
* widget state based off of that. This cannot be rolled into [WidgetProvider] directly, as it may
* result in memory leaks if [PlaybackStateManager]/[SettingsManager] gets created and bound to
* without being released.
* result in memory leaks if [PlaybackStateManager]/[Settings] gets created and bound to without
* being released.
* @author OxygenCobalt
*/
class WidgetComponent(private val context: Context) :

View file

@ -35,9 +35,9 @@
<string name="set_key_lib_artists_sort" translatable="false">auxio_artists_sort</string>
<string name="set_key_lib_genres_sort" translatable="false">auxio_genres_sort</string>
<string name="set_key_lib_album_sort" translatable="false">auxio_album_sort</string>
<string name="set_key_lib_artist_sort" translatable="false">auxio_artist_sort</string>
<string name="set_key_lib_genre_sort" translatable="false">auxio_genre_sort</string>
<string name="set_key_detail_album_sort" translatable="false">auxio_album_sort</string>
<string name="set_key_detail_artist_sort" translatable="false">auxio_artist_sort</string>
<string name="set_key_detail_genre_sort" translatable="false">auxio_genre_sort</string>
<string-array name="entries_theme">
<item>@string/set_theme_auto</item>