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 - Migrated preferences from shared object to utility
- Removed 2.0.0 compat code - Removed 2.0.0 compat code
- Updated ExoPlayer to 2.18.0 - Updated ExoPlayer to 2.18.0
- Reworked sorting to be even more efficient
## v2.4.0 ## 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.Header
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuFragment import org.oxycblt.auxio.ui.MenuFragment
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
@ -143,11 +144,16 @@ class AlbumDetailFragment :
override fun onShowSortMenu(anchor: View) { override fun onShowSortMenu(anchor: View) {
menu(anchor, R.menu.menu_album_sort) { menu(anchor, R.menu.menu_album_sort) {
val sort = detailModel.albumSort 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 requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked 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 true
} }
} }

View file

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

View file

@ -93,6 +93,7 @@ class DetailViewModel(application: Application) :
var artistSort: Sort var artistSort: Sort
get() = settings.detailArtistSort get() = settings.detailArtistSort
set(value) { set(value) {
logD(value)
settings.detailArtistSort = value settings.detailArtistSort = value
currentArtist.value?.let(::refreshArtistData) currentArtist.value?.let(::refreshArtistData)
} }
@ -233,7 +234,7 @@ class DetailViewModel(application: Application) :
logD("Refreshing artist data") logD("Refreshing artist data")
val data = mutableListOf<Item>(artist) val data = mutableListOf<Item>(artist)
data.add(Header(-2, R.string.lbl_albums)) 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.add(SortHeader(-3, R.string.lbl_songs))
data.addAll(artistSort.songs(artist.songs)) data.addAll(artistSort.songs(artist.songs))
_artistData.value = data.toList() _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.Header
import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.MenuFragment import org.oxycblt.auxio.ui.MenuFragment
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.applySpans import org.oxycblt.auxio.util.applySpans
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
@ -139,11 +140,16 @@ class GenreDetailFragment :
override fun onShowSortMenu(anchor: View) { override fun onShowSortMenu(anchor: View) {
menu(anchor, R.menu.menu_genre_sort) { menu(anchor, R.menu.menu_genre_sort) {
val sort = detailModel.genreSort 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 requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked 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 true
} }
} }

View file

@ -54,6 +54,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.DisplayMode
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collect 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.logE
import org.oxycblt.auxio.util.logTraceOrThrow import org.oxycblt.auxio.util.logTraceOrThrow
import org.oxycblt.auxio.util.textSafe 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 * 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 -> { R.id.option_sort_asc -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
homeModel.updateCurrentSort( homeModel.updateCurrentSort(
unlikelyToBeNull(
homeModel homeModel
.getSortForDisplay(homeModel.currentTab.value) .getSortForDisplay(homeModel.currentTab.value)
.ascending(item.isChecked))) .withAscending(item.isChecked))
} }
else -> { else -> {
// Sorting option was selected, mark it as selected and update the mode // Sorting option was selected, mark it as selected and update the mode
item.isChecked = true item.isChecked = true
homeModel.updateCurrentSort( homeModel.updateCurrentSort(
unlikelyToBeNull(
homeModel homeModel
.getSortForDisplay(homeModel.currentTab.value) .getSortForDisplay(homeModel.currentTab.value)
.assignId(item.itemId))) .withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
} }
} }
@ -248,7 +246,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
val toHighlight = homeModel.getSortForDisplay(displayMode) val toHighlight = homeModel.getSortForDisplay(displayMode)
for (option in sortMenu) { for (option in sortMenu) {
if (option.itemId == toHighlight.itemId) { if (option.itemId == toHighlight.mode.itemId) {
option.isChecked = true option.isChecked = true
} }

View file

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

View file

@ -56,15 +56,15 @@ class ArtistListFragment : HomeListFragment<Artist>() {
val artist = homeModel.artists.value[pos] val artist = homeModel.artists.value[pos]
// Change how we display the popup depending on the mode. // 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 // 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 // Duration -> Use formatted duration
is Sort.ByDuration -> artist.durationSecs.formatDuration(false) is Sort.Mode.ByDuration -> artist.durationSecs.formatDuration(false)
// Count -> Use song count // Count -> Use song count
is Sort.ByCount -> artist.songs.size.toString() is Sort.Mode.ByCount -> artist.songs.size.toString()
// Unsupported sort, error gracefully // Unsupported sort, error gracefully
else -> null else -> null

View file

@ -56,15 +56,15 @@ class GenreListFragment : HomeListFragment<Genre>() {
val genre = homeModel.genres.value[pos] val genre = homeModel.genres.value[pos]
// Change how we display the popup depending on the mode. // 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 // 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 // Duration -> Use formatted duration
is Sort.ByDuration -> genre.durationSecs.formatDuration(false) is Sort.Mode.ByDuration -> genre.durationSecs.formatDuration(false)
// Count -> Use song count // Count -> Use song count
is Sort.ByCount -> genre.songs.size.toString() is Sort.Mode.ByCount -> genre.songs.size.toString()
// Unsupported sort, error gracefully // Unsupported sort, error gracefully
else -> null 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.Item
import org.oxycblt.auxio.ui.MenuItemListener import org.oxycblt.auxio.ui.MenuItemListener
import org.oxycblt.auxio.ui.MonoAdapter import org.oxycblt.auxio.ui.MonoAdapter
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.SongViewHolder
import org.oxycblt.auxio.ui.SyncBackingData import org.oxycblt.auxio.ui.SyncBackingData
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
@ -60,21 +60,21 @@ class SongListFragment : HomeListFragment<Song>() {
// Change how we display the popup depending on the mode. // 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 // 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. // 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 // Name -> Use name
is Sort.ByName -> song.sortName.first().uppercase() is Sort.Mode.ByName -> song.sortName.first().uppercase()
// Artist -> Use Artist Name // 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 // 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 // Year -> Use Full Year
is Sort.ByYear -> song.album.year?.toString() is Sort.Mode.ByYear -> song.album.year?.toString()
// Duration -> Use formatted duration // Duration -> Use formatted duration
is Sort.ByDuration -> song.durationSecs.formatDuration(false) is Sort.Mode.ByDuration -> song.durationSecs.formatDuration(false)
// Unsupported sort, error gracefully // Unsupported sort, error gracefully
else -> null else -> null

View file

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

View file

@ -257,7 +257,7 @@ class Indexer {
.toMutableList() .toMutableList()
// Ensure that sorting order is consistent so that grouping is also consistent. // 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") 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. // 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 // 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. // weird years like "0" wont show up if there are alternatives.
// Note: Normally we could want to use something like maxByWith, but apparently val templateSong =
// that does not exist in the kotlin stdlib yet. albumSongs.maxWith(compareBy(Sort.Mode.NULLABLE_INT_COMPARATOR) { it._year })
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
}
}
albums.add( albums.add(
Album( Album(

View file

@ -37,8 +37,10 @@ class MediaButtonReceiver : BroadcastReceiver() {
val playbackManager = PlaybackStateManager.getInstance() val playbackManager = PlaybackStateManager.getInstance()
if (playbackManager.song != null) { if (playbackManager.song != null) {
// We have a song, so we can assume that the service will start a foreground state. // 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 // 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* // 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) intent.component = ComponentName(context, PlaybackService::class.java)
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }

View file

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

View file

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

View file

@ -74,7 +74,7 @@ class SearchViewModel(application: Application) :
// Searching can be quite expensive, so get on a co-routine // Searching can be quite expensive, so get on a co-routine
viewModelScope.launch { viewModelScope.launch {
val sort = Sort.ByName(true) val sort = Sort(Sort.Mode.ByName, true)
val results = mutableListOf<Item>() val results = mutableListOf<Item>()
// Note: a filter mode of null means to not filter at all. // 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() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE))
?: Sort.ByName(true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode) 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() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE))
?: Sort.ByName(true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode) 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() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE))
?: Sort.ByName(true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode) 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() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE))
?: Sort.ByName(true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { inner.edit {
putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode) 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() { get() {
var sort = var sort =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_album_sort), Int.MIN_VALUE)) inner.getInt(
?: Sort.ByDisc(true) context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByDisc, true)
// Correct legacy album sort modes to Disc // Correct legacy album sort modes to Disc
if (sort is Sort.ByName) { if (sort.mode is Sort.Mode.ByName) {
sort = Sort.ByDisc(sort.isAscending) sort = sort.withMode(Sort.Mode.ByDisc)
} }
return sort return sort
} }
set(value) { set(value) {
inner.edit { 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() apply()
} }
} }
@ -312,11 +313,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
var detailArtistSort: Sort var detailArtistSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_artist_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE))
?: Sort.ByYear(false) ?: Sort(Sort.Mode.ByYear, false)
set(value) { set(value) {
inner.edit { 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() apply()
} }
} }
@ -325,11 +326,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
var detailGenreSort: Sort var detailGenreSort: Sort
get() = get() =
Sort.fromIntCode( Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_genre_sort), Int.MIN_VALUE)) inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE))
?: Sort.ByName(true) ?: Sort(Sort.Mode.ByName, true)
set(value) { set(value) {
inner.edit { 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() apply()
} }
} }

View file

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

View file

@ -46,7 +46,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.R 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 * 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 * 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 * result in memory leaks if [PlaybackStateManager]/[Settings] gets created and bound to without
* without being released. * being released.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class WidgetComponent(private val context: Context) : 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_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_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_detail_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_detail_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_genre_sort" translatable="false">auxio_genre_sort</string>
<string-array name="entries_theme"> <string-array name="entries_theme">
<item>@string/set_theme_auto</item> <item>@string/set_theme_auto</item>