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) .withAscending(item.isChecked))
.ascending(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) .withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
.assignId(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,336 +75,312 @@ 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
/** Sort by the names of an item */ open fun getSongComparator(ascending: Boolean): Comparator<Song> {
class ByName(override val isAscending: Boolean) : Sort(isAscending) { throw UnsupportedOperationException()
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 })
} }
override fun albumsInPlace(albums: MutableList<Album>) { open fun getAlbumComparator(ascending: Boolean): Comparator<Album> {
albums.sortWith(compareByDynamic(NameComparator()) { it }) throw UnsupportedOperationException()
} }
override fun artistsInPlace(artists: MutableList<Artist>) { open fun getArtistComparator(ascending: Boolean): Comparator<Artist> {
artists.sortWith(compareByDynamic(NameComparator()) { it }) throw UnsupportedOperationException()
} }
override fun genresInPlace(genres: MutableList<Genre>) { open fun getGenreComparator(ascending: Boolean): Comparator<Genre> {
genres.sortWith(compareByDynamic(NameComparator()) { it }) 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] */ override val itemId: Int
class ByAlbum(override val isAscending: Boolean) : Sort(isAscending) { get() = R.id.option_sort_name
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_ALBUM
override val itemId: Int override fun getSongComparator(ascending: Boolean) =
get() = R.id.option_sort_album compareByDynamic(ascending, BasicComparator.SONG)
override fun songsInPlace(songs: MutableList<Song>) { override fun getAlbumComparator(ascending: Boolean) =
songs.sortWith( 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( 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] */
} 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] */ override val itemId: Int
class ByArtist(override val isAscending: Boolean) : Sort(isAscending) { get() = R.id.option_sort_artist
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_ARTIST
override val itemId: Int override fun getSongComparator(ascending: Boolean): Comparator<Song> =
get() = R.id.option_sort_artist
override fun songsInPlace(songs: MutableList<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] */
} 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] */ override val itemId: Int
class ByYear(override val isAscending: Boolean) : Sort(isAscending) { get() = R.id.option_sort_year
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_YEAR
override val itemId: Int override fun getSongComparator(ascending: Boolean): Comparator<Song> =
get() = R.id.option_sort_year
override fun songsInPlace(songs: MutableList<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. */
} object ByDuration : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_DURATION
/** Sort by the duration of the item. Supports all items. */ override val itemId: Int
class ByDuration(override val isAscending: Boolean) : Sort(isAscending) { get() = R.id.option_sort_duration
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_DURATION
override val itemId: Int override fun getSongComparator(ascending: Boolean): Comparator<Song> =
get() = R.id.option_sort_duration
override fun songsInPlace(songs: MutableList<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. */
} object ByCount : Mode() {
override val intCode: Int
get() = IntegerTable.SORT_BY_COUNT
/** Sort by the amount of songs. Only applicable to music parents. */ override val itemId: Int
class ByCount(override val isAscending: Boolean) : Sort(isAscending) { get() = R.id.option_sort_count
override val sortIntCode: Int
get() = IntegerTable.SORT_BY_COUNT
override val itemId: Int override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
get() = R.id.option_sort_count
override fun albumsInPlace(albums: MutableList<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
get() = IntegerTable.SORT_BY_DISC
/** override val itemId: Int
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use this get() = R.id.option_sort_disc
* in a main sorting view, as it is not assigned to a particular item ID override fun getSongComparator(ascending: Boolean): Comparator<Song> =
*/
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(
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 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
/** override val itemId: Int
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use this get() = R.id.option_sort_track
* 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 override fun getSongComparator(ascending: Boolean): Comparator<Song> =
get() = R.id.option_sort_track
override fun songsInPlace(songs: MutableList<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) 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 protected fun <T : Music> compareByDynamic(
get() = sortIntCode.shl(1) or if (isAscending) 1 else 0 ascending: Boolean,
comparator: Comparator<in T>
): Comparator<T> = compareByDynamic(ascending, comparator) { it }
/** protected inline fun <T : Music, K : Comparable<K>> compareByDynamic(
* Assign a new [id] to this sort ascending: Boolean,
* @return A new [Sort] corresponding to the [id] given, null if the ID has no analogue. crossinline selector: (T) -> K
*/ ) =
fun assignId(@IdRes id: Int): Sort? { if (ascending) {
return when (id) { compareBy(selector)
R.id.option_sort_asc -> ascending(!isAscending) } else {
R.id.option_sort_name -> ByName(isAscending) compareByDescending(selector)
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 fun <T : Music> compareBy(comparator: Comparator<T>): Comparator<T> =
comparator: Comparator<in K>, compareBy(comparator) { it }
crossinline selector: (T) -> K
): Comparator<T> {
return if (isAscending) {
compareBy(comparator, selector)
} else {
compareByDescending(comparator, selector)
}
}
protected inline fun <T : Music, K : Comparable<K>> compareByDynamic( /**
crossinline selector: (T) -> K * Chains the given comparators together to form one comparator.
): Comparator<T> { *
return if (isAscending) { * Sorts often need to compare multiple things at once across several hierarchies, with this
compareBy(selector) * class doing such in a more efficient manner than resorting at multiple intervals or
} else { * grouping items up. Comparators are checked from first to last, with the first comparator
compareByDescending(selector) * 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 {
override fun compare(a: T, b: T): Int { for (comparator in _comparators) {
val aSortName = a.sortName val result = comparator.compare(a, b)
val bSortName = b.sortName if (result != 0) {
return when { return result
aSortName != null && bSortName != null -> }
aSortName.compareTo(bSortName, ignoreCase = true) }
aSortName == null && bSortName != null -> -1 // a < b
aSortName == null && bSortName == null -> 0 // a = b return 0
aSortName != null && bSortName == null -> 1 // a < b
else -> error("Unreachable")
} }
} }
}
class NullableComparator<T : Comparable<T>> : Comparator<T?> { private class BasicComparator<T : Music> private constructor() : Comparator<T> {
override fun compare(a: T?, b: T?): Int { override fun compare(a: T, b: T): Int {
return when { val aSortName = a.sortName
a != null && b != null -> a.compareTo(b) val bSortName = b.sortName
a == null && b != null -> -1 // a < b return when {
a == null && b == null -> 0 // a = b aSortName != null && bSortName != null ->
a != null && b == null -> 1 // a < b aSortName.compareTo(bSortName, ignoreCase = true)
else -> error("Unreachable") aSortName == null && bSortName != null -> -1 // a < b
} aSortName == null && bSortName == null -> 0 // a = b
} aSortName != null && bSortName == 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
} }
} }
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 { 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 =
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)) { return Sort(mode, isAscending)
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
}
} }
} }
} }

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>