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:
parent
86010e896e
commit
55f9d4c819
21 changed files with 346 additions and 348 deletions
|
@ -22,6 +22,7 @@ design guidelines
|
|||
- Migrated preferences from shared object to utility
|
||||
- Removed 2.0.0 compat code
|
||||
- Updated ExoPlayer to 2.18.0
|
||||
- Reworked sorting to be even more efficient
|
||||
|
||||
## v2.4.0
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ import org.oxycblt.auxio.settings.Settings
|
|||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuFragment
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.canScroll
|
||||
import org.oxycblt.auxio.util.collect
|
||||
|
@ -143,11 +144,16 @@ class AlbumDetailFragment :
|
|||
override fun onShowSortMenu(anchor: View) {
|
||||
menu(anchor, R.menu.menu_album_sort) {
|
||||
val sort = detailModel.albumSort
|
||||
requireNotNull(menu.findItem(sort.itemId)).isChecked = true
|
||||
requireNotNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||
setOnMenuItemClickListener { item ->
|
||||
item.isChecked = !item.isChecked
|
||||
detailModel.albumSort = requireNotNull(sort.assignId(item.itemId))
|
||||
detailModel.albumSort =
|
||||
if (item.itemId == R.id.option_sort_asc) {
|
||||
sort.withAscending(item.isChecked)
|
||||
} else {
|
||||
sort.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.oxycblt.auxio.settings.Settings
|
|||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuFragment
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
@ -138,11 +139,18 @@ class ArtistDetailFragment :
|
|||
override fun onShowSortMenu(anchor: View) {
|
||||
menu(anchor, R.menu.menu_artist_sort) {
|
||||
val sort = detailModel.artistSort
|
||||
requireNotNull(menu.findItem(sort.itemId)).isChecked = true
|
||||
requireNotNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||
setOnMenuItemClickListener { item ->
|
||||
item.isChecked = !item.isChecked
|
||||
detailModel.artistSort = requireNotNull(sort.assignId(item.itemId))
|
||||
|
||||
detailModel.artistSort =
|
||||
if (item.itemId == R.id.option_sort_asc) {
|
||||
sort.withAscending(item.isChecked)
|
||||
} else {
|
||||
sort.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@ class DetailViewModel(application: Application) :
|
|||
var artistSort: Sort
|
||||
get() = settings.detailArtistSort
|
||||
set(value) {
|
||||
logD(value)
|
||||
settings.detailArtistSort = value
|
||||
currentArtist.value?.let(::refreshArtistData)
|
||||
}
|
||||
|
@ -233,7 +234,7 @@ class DetailViewModel(application: Application) :
|
|||
logD("Refreshing artist data")
|
||||
val data = mutableListOf<Item>(artist)
|
||||
data.add(Header(-2, R.string.lbl_albums))
|
||||
data.addAll(Sort.ByYear(false).albums(artist.albums))
|
||||
data.addAll(Sort(Sort.Mode.ByYear, false).albums(artist.albums))
|
||||
data.add(SortHeader(-3, R.string.lbl_songs))
|
||||
data.addAll(artistSort.songs(artist.songs))
|
||||
_artistData.value = data.toList()
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.oxycblt.auxio.settings.Settings
|
|||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuFragment
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
@ -139,11 +140,16 @@ class GenreDetailFragment :
|
|||
override fun onShowSortMenu(anchor: View) {
|
||||
menu(anchor, R.menu.menu_genre_sort) {
|
||||
val sort = detailModel.genreSort
|
||||
requireNotNull(menu.findItem(sort.itemId)).isChecked = true
|
||||
requireNotNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
requireNotNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||
setOnMenuItemClickListener { item ->
|
||||
item.isChecked = !item.isChecked
|
||||
detailModel.genreSort = requireNotNull(sort.assignId(item.itemId))
|
||||
detailModel.genreSort =
|
||||
if (item.itemId == R.id.option_sort_asc) {
|
||||
sort.withAscending(item.isChecked)
|
||||
} else {
|
||||
sort.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
|||
import org.oxycblt.auxio.ui.DisplayMode
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collect
|
||||
|
@ -65,7 +66,6 @@ import org.oxycblt.auxio.util.logD
|
|||
import org.oxycblt.auxio.util.logE
|
||||
import org.oxycblt.auxio.util.logTraceOrThrow
|
||||
import org.oxycblt.auxio.util.textSafe
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
* The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each
|
||||
|
@ -173,19 +173,17 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
R.id.option_sort_asc -> {
|
||||
item.isChecked = !item.isChecked
|
||||
homeModel.updateCurrentSort(
|
||||
unlikelyToBeNull(
|
||||
homeModel
|
||||
.getSortForDisplay(homeModel.currentTab.value)
|
||||
.ascending(item.isChecked)))
|
||||
homeModel
|
||||
.getSortForDisplay(homeModel.currentTab.value)
|
||||
.withAscending(item.isChecked))
|
||||
}
|
||||
else -> {
|
||||
// Sorting option was selected, mark it as selected and update the mode
|
||||
item.isChecked = true
|
||||
homeModel.updateCurrentSort(
|
||||
unlikelyToBeNull(
|
||||
homeModel
|
||||
.getSortForDisplay(homeModel.currentTab.value)
|
||||
.assignId(item.itemId)))
|
||||
homeModel
|
||||
.getSortForDisplay(homeModel.currentTab.value)
|
||||
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,7 +246,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
val toHighlight = homeModel.getSortForDisplay(displayMode)
|
||||
|
||||
for (option in sortMenu) {
|
||||
if (option.itemId == toHighlight.itemId) {
|
||||
if (option.itemId == toHighlight.mode.itemId) {
|
||||
option.isChecked = true
|
||||
}
|
||||
|
||||
|
|
|
@ -56,21 +56,21 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
val album = homeModel.albums.value[pos]
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) {
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS).mode) {
|
||||
// By Name -> Use Name
|
||||
is Sort.ByName -> album.sortName.first().uppercase()
|
||||
is Sort.Mode.ByName -> album.sortName.first().uppercase()
|
||||
|
||||
// By Artist -> Use Artist Name
|
||||
is Sort.ByArtist -> album.artist.sortName?.run { first().uppercase() }
|
||||
is Sort.Mode.ByArtist -> album.artist.sortName?.run { first().uppercase() }
|
||||
|
||||
// Year -> Use Full Year
|
||||
is Sort.ByYear -> album.year?.toString()
|
||||
is Sort.Mode.ByYear -> album.year?.toString()
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.ByDuration -> album.durationSecs.formatDuration(false)
|
||||
is Sort.Mode.ByDuration -> album.durationSecs.formatDuration(false)
|
||||
|
||||
// Count -> Use song count
|
||||
is Sort.ByCount -> album.songs.size.toString()
|
||||
is Sort.Mode.ByCount -> album.songs.size.toString()
|
||||
|
||||
// Unsupported sort, error gracefully
|
||||
else -> null
|
||||
|
|
|
@ -56,15 +56,15 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
val artist = homeModel.artists.value[pos]
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ARTISTS)) {
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ARTISTS).mode) {
|
||||
// By Name -> Use Name
|
||||
is Sort.ByName -> artist.sortName?.run { first().uppercase() }
|
||||
is Sort.Mode.ByName -> artist.sortName?.run { first().uppercase() }
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.ByDuration -> artist.durationSecs.formatDuration(false)
|
||||
is Sort.Mode.ByDuration -> artist.durationSecs.formatDuration(false)
|
||||
|
||||
// Count -> Use song count
|
||||
is Sort.ByCount -> artist.songs.size.toString()
|
||||
is Sort.Mode.ByCount -> artist.songs.size.toString()
|
||||
|
||||
// Unsupported sort, error gracefully
|
||||
else -> null
|
||||
|
|
|
@ -56,15 +56,15 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
val genre = homeModel.genres.value[pos]
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_GENRES)) {
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_GENRES).mode) {
|
||||
// By Name -> Use Name
|
||||
is Sort.ByName -> genre.sortName?.run { first().uppercase() }
|
||||
is Sort.Mode.ByName -> genre.sortName?.run { first().uppercase() }
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.ByDuration -> genre.durationSecs.formatDuration(false)
|
||||
is Sort.Mode.ByDuration -> genre.durationSecs.formatDuration(false)
|
||||
|
||||
// Count -> Use song count
|
||||
is Sort.ByCount -> genre.songs.size.toString()
|
||||
is Sort.Mode.ByCount -> genre.songs.size.toString()
|
||||
|
||||
// Unsupported sort, error gracefully
|
||||
else -> null
|
||||
|
|
|
@ -27,8 +27,8 @@ import org.oxycblt.auxio.ui.DisplayMode
|
|||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.MonoAdapter
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.SongViewHolder
|
||||
import org.oxycblt.auxio.ui.SyncBackingData
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
@ -60,21 +60,21 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
// Change how we display the popup depending on the mode.
|
||||
// Note: We don't use the more correct individual artist name here, as sorts are largely
|
||||
// based off the names of the parent objects and not the child objects.
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) {
|
||||
return when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS).mode) {
|
||||
// Name -> Use name
|
||||
is Sort.ByName -> song.sortName.first().uppercase()
|
||||
is Sort.Mode.ByName -> song.sortName.first().uppercase()
|
||||
|
||||
// Artist -> Use Artist Name
|
||||
is Sort.ByArtist -> song.album.artist.sortName?.run { first().uppercase() }
|
||||
is Sort.Mode.ByArtist -> song.album.artist.sortName?.run { first().uppercase() }
|
||||
|
||||
// Album -> Use Album Name
|
||||
is Sort.ByAlbum -> song.album.sortName.first().uppercase()
|
||||
is Sort.Mode.ByAlbum -> song.album.sortName.first().uppercase()
|
||||
|
||||
// Year -> Use Full Year
|
||||
is Sort.ByYear -> song.album.year?.toString()
|
||||
is Sort.Mode.ByYear -> song.album.year?.toString()
|
||||
|
||||
// Duration -> Use formatted duration
|
||||
is Sort.ByDuration -> song.durationSecs.formatDuration(false)
|
||||
is Sort.Mode.ByDuration -> song.durationSecs.formatDuration(false)
|
||||
|
||||
// Unsupported sort, error gracefully
|
||||
else -> null
|
||||
|
|
|
@ -87,7 +87,7 @@ private constructor(
|
|||
private val artist: Artist,
|
||||
) : BaseFetcher() {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
val albums = Sort.ByName(true).albums(artist.albums)
|
||||
val albums = Sort(Sort.Mode.ByName, true).albums(artist.albums)
|
||||
val results = albums.mapAtMost(4) { album -> fetchArt(context, album) }
|
||||
return createMosaic(context, results, size)
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ private constructor(
|
|||
// albums normally.
|
||||
val artists = genre.songs.groupBy { it.album.artist.id }.keys
|
||||
val albums =
|
||||
Sort.ByName(true).albums(genre.songs.groupBy { it.album }.keys).run {
|
||||
Sort(Sort.Mode.ByName, true).albums(genre.songs.groupBy { it.album }.keys).run {
|
||||
if (artists.size > 4) {
|
||||
distinctBy { it.artist.rawName }
|
||||
} else {
|
||||
|
|
|
@ -257,7 +257,7 @@ class Indexer {
|
|||
.toMutableList()
|
||||
|
||||
// Ensure that sorting order is consistent so that grouping is also consistent.
|
||||
Sort.ByName(true).songsInPlace(songs)
|
||||
Sort(Sort.Mode.ByName, true).songsInPlace(songs)
|
||||
|
||||
logD("Successfully built ${songs.size} songs in ${System.currentTimeMillis() - start}ms")
|
||||
|
||||
|
@ -287,16 +287,8 @@ class Indexer {
|
|||
// Use the song with the latest year as our metadata song.
|
||||
// This allows us to replicate the LAST_YEAR field, which is useful as it means that
|
||||
// weird years like "0" wont show up if there are alternatives.
|
||||
// Note: Normally we could want to use something like maxByWith, but apparently
|
||||
// that does not exist in the kotlin stdlib yet.
|
||||
val comparator = Sort.NullableComparator<Int>()
|
||||
var templateSong = albumSongs[0]
|
||||
for (i in 1..albumSongs.lastIndex) {
|
||||
val candidate = albumSongs[i]
|
||||
if (comparator.compare(templateSong.track, candidate.track) < 0) {
|
||||
templateSong = candidate
|
||||
}
|
||||
}
|
||||
val templateSong =
|
||||
albumSongs.maxWith(compareBy(Sort.Mode.NULLABLE_INT_COMPARATOR) { it._year })
|
||||
|
||||
albums.add(
|
||||
Album(
|
||||
|
|
|
@ -37,8 +37,10 @@ class MediaButtonReceiver : BroadcastReceiver() {
|
|||
val playbackManager = PlaybackStateManager.getInstance()
|
||||
if (playbackManager.song != null) {
|
||||
// We have a song, so we can assume that the service will start a foreground state.
|
||||
// At least, I hope. Again, *this is why we don't do this, I cannot describe how
|
||||
// stupid this is with the state of foreground services on modern android*
|
||||
// At least, I hope. Again, *this is why we don't do this*. I cannot describe how
|
||||
// stupid this is with the state of foreground services on modern android. One
|
||||
// wrong action at the wrong time will result in the app crashing, and there is
|
||||
// nothing I can do about it.
|
||||
intent.component = ComponentName(context, PlaybackService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ import org.oxycblt.auxio.util.logD
|
|||
/**
|
||||
* The component managing the [MediaSessionCompat] instance.
|
||||
*
|
||||
* I really don't like how I have to do this, but until I can feasibly work with the ExoPlayer queue
|
||||
* system using something like MediaSessionConnector is more or less impossible.
|
||||
* I really don't like how I have to do this, but until I can work with the ExoPlayer queue system
|
||||
* using something like MediaSessionConnector is more or less impossible.
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
|
@ -242,6 +242,12 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
|
|||
val state =
|
||||
PlaybackStateCompat.Builder()
|
||||
.setActions(ACTIONS)
|
||||
.addCustomAction(
|
||||
PlaybackStateCompat.CustomAction.Builder(
|
||||
PlaybackService.ACTION_INC_REPEAT_MODE,
|
||||
context.getString(R.string.desc_change_repeat),
|
||||
R.drawable.ic_remote_repeat_off)
|
||||
.build())
|
||||
.setBufferedPosition(player.bufferedPosition)
|
||||
|
||||
state.setState(PlaybackStateCompat.STATE_NONE, player.bufferedPosition, 1.0f)
|
||||
|
|
|
@ -64,8 +64,8 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
|||
* - Headset management
|
||||
* - Widgets
|
||||
*
|
||||
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], so
|
||||
* therefore there's no need to bind to it to deliver commands.
|
||||
* This service relies on [PlaybackStateManager.Callback] and [Settings.Callback], so therefore
|
||||
* there's no need to bind to it to deliver commands.
|
||||
*
|
||||
* TODO: Android Auto
|
||||
*
|
||||
|
|
|
@ -74,7 +74,7 @@ class SearchViewModel(application: Application) :
|
|||
|
||||
// Searching can be quite expensive, so get on a co-routine
|
||||
viewModelScope.launch {
|
||||
val sort = Sort.ByName(true)
|
||||
val sort = Sort(Sort.Mode.ByName, true)
|
||||
val results = mutableListOf<Item>()
|
||||
|
||||
// Note: a filter mode of null means to not filter at all.
|
||||
|
|
|
@ -239,7 +239,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
|||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE))
|
||||
?: Sort.ByName(true)
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode)
|
||||
|
@ -252,7 +252,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
|||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE))
|
||||
?: Sort.ByName(true)
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode)
|
||||
|
@ -265,7 +265,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
|||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE))
|
||||
?: Sort.ByName(true)
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode)
|
||||
|
@ -278,7 +278,7 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
|||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE))
|
||||
?: Sort.ByName(true)
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode)
|
||||
|
@ -291,19 +291,20 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
|||
get() {
|
||||
var sort =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_album_sort), Int.MIN_VALUE))
|
||||
?: Sort.ByDisc(true)
|
||||
inner.getInt(
|
||||
context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByDisc, true)
|
||||
|
||||
// Correct legacy album sort modes to Disc
|
||||
if (sort is Sort.ByName) {
|
||||
sort = Sort.ByDisc(sort.isAscending)
|
||||
if (sort.mode is Sort.Mode.ByName) {
|
||||
sort = sort.withMode(Sort.Mode.ByDisc)
|
||||
}
|
||||
|
||||
return sort
|
||||
}
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_album_sort), value.intCode)
|
||||
putInt(context.getString(R.string.set_key_detail_album_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
@ -312,11 +313,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
|||
var detailArtistSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_artist_sort), Int.MIN_VALUE))
|
||||
?: Sort.ByYear(false)
|
||||
inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByYear, false)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode)
|
||||
putInt(context.getString(R.string.set_key_detail_artist_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
@ -325,11 +326,11 @@ class Settings(private val context: Context, private val callback: Callback? = n
|
|||
var detailGenreSort: Sort
|
||||
get() =
|
||||
Sort.fromIntCode(
|
||||
inner.getInt(context.getString(R.string.set_key_lib_genre_sort), Int.MIN_VALUE))
|
||||
?: Sort.ByName(true)
|
||||
inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE))
|
||||
?: Sort(Sort.Mode.ByName, true)
|
||||
set(value) {
|
||||
inner.edit {
|
||||
putInt(context.getString(R.string.set_key_lib_genre_sort), value.intCode)
|
||||
putInt(context.getString(R.string.set_key_detail_genre_sort), value.intCode)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.oxycblt.auxio.ui
|
||||
|
||||
import androidx.annotation.IdRes
|
||||
import kotlin.UnsupportedOperationException
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.Album
|
||||
|
@ -25,14 +26,14 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.util.logEOrThrow
|
||||
|
||||
/**
|
||||
* A data class representing the sort modes used in Auxio.
|
||||
*
|
||||
* Sorting can be done by Name, Artist, Album, and others. Sorting of names is always
|
||||
* case-insensitive and article-aware. Certain datatypes may only support a subset of sorts since
|
||||
* certain sorts cannot be easily applied to them (For Example, [Artist] and [ByYear] or [ByAlbum]).
|
||||
* certain sorts cannot be easily applied to them (For Example, [Mode.ByArtist] and [Mode.ByYear] or
|
||||
* [Mode.ByAlbum]).
|
||||
*
|
||||
* Internally, sorts are saved as an integer in the following format
|
||||
*
|
||||
|
@ -42,14 +43,13 @@ import org.oxycblt.auxio.util.logEOrThrow
|
|||
* representing whether this sort is ascending or descending.
|
||||
*
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Make comparators static instances
|
||||
*
|
||||
* TODO: Separate sort mode and ascending state
|
||||
*/
|
||||
sealed class Sort(open val isAscending: Boolean) {
|
||||
protected abstract val sortIntCode: Int
|
||||
abstract val itemId: Int
|
||||
data class Sort(val mode: Mode, val isAscending: Boolean) {
|
||||
fun withAscending(new: Boolean) = Sort(mode, new)
|
||||
fun withMode(new: Mode) = Sort(new, isAscending)
|
||||
|
||||
val intCode: Int
|
||||
get() = mode.intCode.shl(1) or if (isAscending) 1 else 0
|
||||
|
||||
fun songs(songs: Collection<Song>): List<Song> {
|
||||
val mutable = songs.toMutableList()
|
||||
|
@ -75,336 +75,312 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
return mutable
|
||||
}
|
||||
|
||||
open fun songsInPlace(songs: MutableList<Song>) {
|
||||
logEOrThrow("This sort is not supported for songs")
|
||||
fun songsInPlace(songs: MutableList<Song>) {
|
||||
songs.sortWith(mode.getSongComparator(isAscending))
|
||||
}
|
||||
|
||||
open fun albumsInPlace(albums: MutableList<Album>) {
|
||||
logEOrThrow("This sort is not supported for albums")
|
||||
fun albumsInPlace(albums: MutableList<Album>) {
|
||||
albums.sortWith(mode.getAlbumComparator(isAscending))
|
||||
}
|
||||
|
||||
open fun artistsInPlace(artists: MutableList<Artist>) {
|
||||
logEOrThrow("This sort is not supported for artists")
|
||||
fun artistsInPlace(artists: MutableList<Artist>) {
|
||||
artists.sortWith(mode.getArtistComparator(isAscending))
|
||||
}
|
||||
|
||||
open fun genresInPlace(genres: MutableList<Genre>) {
|
||||
logEOrThrow("This sort is not supported for genres")
|
||||
fun genresInPlace(genres: MutableList<Genre>) {
|
||||
genres.sortWith(mode.getGenreComparator(isAscending))
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply [newIsAscending] to the status of this sort.
|
||||
* @return A new [Sort] with the value of [newIsAscending] applied.
|
||||
*/
|
||||
abstract fun ascending(newIsAscending: Boolean): Sort
|
||||
sealed class Mode {
|
||||
abstract val intCode: Int
|
||||
abstract val itemId: Int
|
||||
|
||||
/** Sort by the names of an item */
|
||||
class ByName(override val isAscending: Boolean) : Sort(isAscending) {
|
||||
override val sortIntCode: Int
|
||||
get() = IntegerTable.SORT_BY_NAME
|
||||
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_name
|
||||
|
||||
override fun songsInPlace(songs: MutableList<Song>) {
|
||||
songs.sortWith(compareByDynamic(NameComparator()) { it })
|
||||
open fun getSongComparator(ascending: Boolean): Comparator<Song> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun albumsInPlace(albums: MutableList<Album>) {
|
||||
albums.sortWith(compareByDynamic(NameComparator()) { it })
|
||||
open fun getAlbumComparator(ascending: Boolean): Comparator<Album> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun artistsInPlace(artists: MutableList<Artist>) {
|
||||
artists.sortWith(compareByDynamic(NameComparator()) { it })
|
||||
open fun getArtistComparator(ascending: Boolean): Comparator<Artist> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun genresInPlace(genres: MutableList<Genre>) {
|
||||
genres.sortWith(compareByDynamic(NameComparator()) { it })
|
||||
open fun getGenreComparator(ascending: Boolean): Comparator<Genre> {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean) = ByName(newIsAscending)
|
||||
}
|
||||
/** Sort by the names of an item */
|
||||
object ByName : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_NAME
|
||||
|
||||
/** Sort by the album of an item, only supported by [Song] */
|
||||
class ByAlbum(override val isAscending: Boolean) : Sort(isAscending) {
|
||||
override val sortIntCode: Int
|
||||
get() = IntegerTable.SORT_BY_ALBUM
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_name
|
||||
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_album
|
||||
override fun getSongComparator(ascending: Boolean) =
|
||||
compareByDynamic(ascending, BasicComparator.SONG)
|
||||
|
||||
override fun songsInPlace(songs: MutableList<Song>) {
|
||||
songs.sortWith(
|
||||
override fun getAlbumComparator(ascending: Boolean) =
|
||||
compareByDynamic(ascending, BasicComparator.ALBUM)
|
||||
|
||||
override fun getArtistComparator(ascending: Boolean) =
|
||||
compareByDynamic(ascending, BasicComparator.ARTIST)
|
||||
|
||||
override fun getGenreComparator(ascending: Boolean) =
|
||||
compareByDynamic(ascending, BasicComparator.GENRE)
|
||||
}
|
||||
|
||||
/** Sort by the album of an item, only supported by [Song] */
|
||||
object ByAlbum : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_ALBUM
|
||||
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_album
|
||||
|
||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic(NameComparator()) { it.album },
|
||||
compareBy(NullableComparator()) { it.disc },
|
||||
compareBy(NullableComparator()) { it.track },
|
||||
compareBy(NameComparator()) { it }))
|
||||
compareByDynamic(ascending, BasicComparator.ALBUM) { it.album },
|
||||
compareBy(NULLABLE_INT_COMPARATOR) { it.disc },
|
||||
compareBy(NULLABLE_INT_COMPARATOR) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean) = ByAlbum(newIsAscending)
|
||||
}
|
||||
/** Sort by the artist of an item, only supported by [Album] and [Song] */
|
||||
object ByArtist : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_ARTIST
|
||||
|
||||
/** Sort by the artist of an item, only supported by [Album] and [Song] */
|
||||
class ByArtist(override val isAscending: Boolean) : Sort(isAscending) {
|
||||
override val sortIntCode: Int
|
||||
get() = IntegerTable.SORT_BY_ARTIST
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_artist
|
||||
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_artist
|
||||
|
||||
override fun songsInPlace(songs: MutableList<Song>) {
|
||||
songs.sortWith(
|
||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic(NameComparator()) { it.album.artist },
|
||||
compareByDescending(NullableComparator()) { it.album.year },
|
||||
compareByDescending(NameComparator()) { it.album },
|
||||
compareBy(NullableComparator()) { it.disc },
|
||||
compareBy(NullableComparator()) { it.track },
|
||||
compareBy(NameComparator()) { it }))
|
||||
}
|
||||
compareByDynamic(ascending, BasicComparator.ARTIST) { it.album.artist },
|
||||
compareByDescending(NULLABLE_INT_COMPARATOR) { it.album.year },
|
||||
compareByDescending(BasicComparator.ALBUM) { it.album },
|
||||
compareBy(NULLABLE_INT_COMPARATOR) { it.disc },
|
||||
compareBy(NULLABLE_INT_COMPARATOR) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
|
||||
override fun albumsInPlace(albums: MutableList<Album>) {
|
||||
albums.sortWith(
|
||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||
MultiComparator(
|
||||
compareByDynamic(NameComparator()) { it.artist },
|
||||
compareByDescending(NullableComparator()) { it.year },
|
||||
compareBy(NameComparator()) { it }))
|
||||
compareByDynamic(ascending, BasicComparator.ARTIST) { it.artist },
|
||||
compareByDescending(NULLABLE_INT_COMPARATOR) { it.year },
|
||||
compareBy(BasicComparator.ALBUM))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean) = ByArtist(newIsAscending)
|
||||
}
|
||||
/** Sort by the year of an item, only supported by [Album] and [Song] */
|
||||
object ByYear : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_YEAR
|
||||
|
||||
/** Sort by the year of an item, only supported by [Album] and [Song] */
|
||||
class ByYear(override val isAscending: Boolean) : Sort(isAscending) {
|
||||
override val sortIntCode: Int
|
||||
get() = IntegerTable.SORT_BY_YEAR
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_year
|
||||
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_year
|
||||
|
||||
override fun songsInPlace(songs: MutableList<Song>) {
|
||||
songs.sortWith(
|
||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic(NullableComparator()) { it.album.year },
|
||||
compareByDescending(NameComparator()) { it.album },
|
||||
compareBy(NullableComparator()) { it.disc },
|
||||
compareBy(NullableComparator()) { it.track },
|
||||
compareBy(NameComparator()) { it }))
|
||||
}
|
||||
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.album.year },
|
||||
compareByDescending(BasicComparator.ALBUM) { it.album },
|
||||
compareBy(NULLABLE_INT_COMPARATOR) { it.disc },
|
||||
compareBy(NULLABLE_INT_COMPARATOR) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
|
||||
override fun albumsInPlace(albums: MutableList<Album>) {
|
||||
albums.sortWith(
|
||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||
MultiComparator(
|
||||
compareByDynamic(NullableComparator()) { it.year },
|
||||
compareBy(NameComparator()) { it }))
|
||||
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.year },
|
||||
compareBy(BasicComparator.ALBUM))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean) = ByYear(newIsAscending)
|
||||
}
|
||||
/** Sort by the duration of the item. Supports all items. */
|
||||
object ByDuration : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_DURATION
|
||||
|
||||
/** Sort by the duration of the item. Supports all items. */
|
||||
class ByDuration(override val isAscending: Boolean) : Sort(isAscending) {
|
||||
override val sortIntCode: Int
|
||||
get() = IntegerTable.SORT_BY_DURATION
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_duration
|
||||
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_duration
|
||||
|
||||
override fun songsInPlace(songs: MutableList<Song>) {
|
||||
songs.sortWith(
|
||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
|
||||
}
|
||||
compareByDynamic(ascending) { it.durationSecs },
|
||||
compareBy(BasicComparator.SONG))
|
||||
|
||||
override fun albumsInPlace(albums: MutableList<Album>) {
|
||||
albums.sortWith(
|
||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||
MultiComparator(
|
||||
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
|
||||
}
|
||||
compareByDynamic(ascending) { it.durationSecs },
|
||||
compareBy(BasicComparator.ALBUM))
|
||||
|
||||
override fun artistsInPlace(artists: MutableList<Artist>) {
|
||||
artists.sortWith(
|
||||
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
||||
MultiComparator(
|
||||
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
|
||||
}
|
||||
compareByDynamic(ascending) { it.durationSecs },
|
||||
compareBy(BasicComparator.ARTIST))
|
||||
|
||||
override fun genresInPlace(genres: MutableList<Genre>) {
|
||||
genres.sortWith(
|
||||
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
|
||||
MultiComparator(
|
||||
compareByDynamic { it.durationSecs }, compareBy(NameComparator()) { it }))
|
||||
compareByDynamic(ascending) { it.durationSecs },
|
||||
compareBy(BasicComparator.GENRE))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean) = ByDuration(newIsAscending)
|
||||
}
|
||||
/** Sort by the amount of songs. Only applicable to music parents. */
|
||||
object ByCount : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_COUNT
|
||||
|
||||
/** Sort by the amount of songs. Only applicable to music parents. */
|
||||
class ByCount(override val isAscending: Boolean) : Sort(isAscending) {
|
||||
override val sortIntCode: Int
|
||||
get() = IntegerTable.SORT_BY_COUNT
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_count
|
||||
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_count
|
||||
|
||||
override fun albumsInPlace(albums: MutableList<Album>) {
|
||||
albums.sortWith(
|
||||
override fun getAlbumComparator(ascending: Boolean): Comparator<Album> =
|
||||
MultiComparator(
|
||||
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
|
||||
}
|
||||
compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.ALBUM))
|
||||
|
||||
override fun artistsInPlace(artists: MutableList<Artist>) {
|
||||
artists.sortWith(
|
||||
override fun getArtistComparator(ascending: Boolean): Comparator<Artist> =
|
||||
MultiComparator(
|
||||
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
|
||||
}
|
||||
compareByDynamic(ascending) { it.songs.size },
|
||||
compareBy(BasicComparator.ARTIST))
|
||||
|
||||
override fun genresInPlace(genres: MutableList<Genre>) {
|
||||
genres.sortWith(
|
||||
override fun getGenreComparator(ascending: Boolean): Comparator<Genre> =
|
||||
MultiComparator(
|
||||
compareByDynamic { it.songs.size }, compareBy(NameComparator()) { it }))
|
||||
compareByDynamic(ascending) { it.songs.size }, compareBy(BasicComparator.GENRE))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean) = ByCount(newIsAscending)
|
||||
}
|
||||
/** Sort by the disc, and then track number of an item. Only supported by [Song]. */
|
||||
object ByDisc : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_DISC
|
||||
|
||||
/**
|
||||
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use this
|
||||
* in a main sorting view, as it is not assigned to a particular item ID
|
||||
*/
|
||||
class ByDisc(override val isAscending: Boolean) : Sort(isAscending) {
|
||||
override val sortIntCode: Int
|
||||
get() = IntegerTable.SORT_BY_DISC
|
||||
|
||||
// Not an available option, so no ID is set
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_disc
|
||||
|
||||
override fun songsInPlace(songs: MutableList<Song>) {
|
||||
songs.sortWith(
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_disc
|
||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareByDynamic(NullableComparator()) { it.disc },
|
||||
compareBy(NullableComparator()) { it.track },
|
||||
compareBy(NameComparator()) { it }))
|
||||
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.disc },
|
||||
compareBy(NULLABLE_INT_COMPARATOR) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean) = ByDisc(newIsAscending)
|
||||
}
|
||||
/**
|
||||
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use
|
||||
* this in a main sorting view, as it is not assigned to a particular item ID
|
||||
*/
|
||||
object ByTrack : Mode() {
|
||||
override val intCode: Int
|
||||
get() = IntegerTable.SORT_BY_TRACK
|
||||
|
||||
/**
|
||||
* Sort by the disc, and then track number of an item. Only supported by [Song]. Do not use this
|
||||
* in a main sorting view, as it is not assigned to a particular item ID
|
||||
*/
|
||||
class ByTrack(override val isAscending: Boolean) : Sort(isAscending) {
|
||||
override val sortIntCode: Int
|
||||
get() = IntegerTable.SORT_BY_TRACK
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_track
|
||||
|
||||
override val itemId: Int
|
||||
get() = R.id.option_sort_track
|
||||
|
||||
override fun songsInPlace(songs: MutableList<Song>) {
|
||||
songs.sortWith(
|
||||
override fun getSongComparator(ascending: Boolean): Comparator<Song> =
|
||||
MultiComparator(
|
||||
compareBy(NullableComparator()) { it.disc },
|
||||
compareByDynamic(NullableComparator()) { it.track },
|
||||
compareBy(NameComparator()) { it }))
|
||||
compareBy(NULLABLE_INT_COMPARATOR) { it.disc },
|
||||
compareByDynamic(ascending, NULLABLE_INT_COMPARATOR) { it.track },
|
||||
compareBy(BasicComparator.SONG))
|
||||
}
|
||||
|
||||
override fun ascending(newIsAscending: Boolean) = ByTrack(newIsAscending)
|
||||
}
|
||||
protected inline fun <T : Music, K> compareByDynamic(
|
||||
ascending: Boolean,
|
||||
comparator: Comparator<in K>,
|
||||
crossinline selector: (T) -> K
|
||||
) =
|
||||
if (ascending) {
|
||||
compareBy(comparator, selector)
|
||||
} else {
|
||||
compareByDescending(comparator, selector)
|
||||
}
|
||||
|
||||
val intCode: Int
|
||||
get() = sortIntCode.shl(1) or if (isAscending) 1 else 0
|
||||
protected fun <T : Music> compareByDynamic(
|
||||
ascending: Boolean,
|
||||
comparator: Comparator<in T>
|
||||
): Comparator<T> = compareByDynamic(ascending, comparator) { it }
|
||||
|
||||
/**
|
||||
* Assign a new [id] to this sort
|
||||
* @return A new [Sort] corresponding to the [id] given, null if the ID has no analogue.
|
||||
*/
|
||||
fun assignId(@IdRes id: Int): Sort? {
|
||||
return when (id) {
|
||||
R.id.option_sort_asc -> ascending(!isAscending)
|
||||
R.id.option_sort_name -> ByName(isAscending)
|
||||
R.id.option_sort_artist -> ByArtist(isAscending)
|
||||
R.id.option_sort_album -> ByAlbum(isAscending)
|
||||
R.id.option_sort_year -> ByYear(isAscending)
|
||||
R.id.option_sort_duration -> ByDuration(isAscending)
|
||||
R.id.option_sort_count -> ByCount(isAscending)
|
||||
R.id.option_sort_disc -> ByDisc(isAscending)
|
||||
R.id.option_sort_track -> ByTrack(isAscending)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
protected inline fun <T : Music, K : Comparable<K>> compareByDynamic(
|
||||
ascending: Boolean,
|
||||
crossinline selector: (T) -> K
|
||||
) =
|
||||
if (ascending) {
|
||||
compareBy(selector)
|
||||
} else {
|
||||
compareByDescending(selector)
|
||||
}
|
||||
|
||||
protected inline fun <T : Music, K> compareByDynamic(
|
||||
comparator: Comparator<in K>,
|
||||
crossinline selector: (T) -> K
|
||||
): Comparator<T> {
|
||||
return if (isAscending) {
|
||||
compareBy(comparator, selector)
|
||||
} else {
|
||||
compareByDescending(comparator, selector)
|
||||
}
|
||||
}
|
||||
protected fun <T : Music> compareBy(comparator: Comparator<T>): Comparator<T> =
|
||||
compareBy(comparator) { it }
|
||||
|
||||
protected inline fun <T : Music, K : Comparable<K>> compareByDynamic(
|
||||
crossinline selector: (T) -> K
|
||||
): Comparator<T> {
|
||||
return if (isAscending) {
|
||||
compareBy(selector)
|
||||
} else {
|
||||
compareByDescending(selector)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Chains the given comparators together to form one comparator.
|
||||
*
|
||||
* Sorts often need to compare multiple things at once across several hierarchies, with this
|
||||
* class doing such in a more efficient manner than resorting at multiple intervals or
|
||||
* grouping items up. Comparators are checked from first to last, with the first comparator
|
||||
* that returns a non-equal result being propagated upwards.
|
||||
*/
|
||||
private class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
|
||||
private val _comparators = comparators
|
||||
|
||||
class NameComparator<T : Music> : Comparator<T> {
|
||||
override fun compare(a: T, b: T): Int {
|
||||
val aSortName = a.sortName
|
||||
val bSortName = b.sortName
|
||||
return when {
|
||||
aSortName != null && bSortName != null ->
|
||||
aSortName.compareTo(bSortName, ignoreCase = true)
|
||||
aSortName == null && bSortName != null -> -1 // a < b
|
||||
aSortName == null && bSortName == null -> 0 // a = b
|
||||
aSortName != null && bSortName == null -> 1 // a < b
|
||||
else -> error("Unreachable")
|
||||
override fun compare(a: T?, b: T?): Int {
|
||||
for (comparator in _comparators) {
|
||||
val result = comparator.compare(a, b)
|
||||
if (result != 0) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NullableComparator<T : Comparable<T>> : Comparator<T?> {
|
||||
override fun compare(a: T?, b: T?): Int {
|
||||
return when {
|
||||
a != null && b != null -> a.compareTo(b)
|
||||
a == null && b != null -> -1 // a < b
|
||||
a == null && b == null -> 0 // a = b
|
||||
a != null && b == null -> 1 // a < b
|
||||
else -> error("Unreachable")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chains the given comparators together to form one comparator.
|
||||
*
|
||||
* Sorts often need to compare multiple things at once across several hierarchies, with this
|
||||
* class doing such in a more efficient manner than resorting at multiple intervals or grouping
|
||||
* items up. Comparators are checked from first to last, with the first comparator that returns
|
||||
* a non-equal result being propagated upwards.
|
||||
*/
|
||||
class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
|
||||
private val _comparators = comparators
|
||||
|
||||
override fun compare(a: T?, b: T?): Int {
|
||||
for (comparator in _comparators) {
|
||||
val result = comparator.compare(a, b)
|
||||
if (result != 0) {
|
||||
return result
|
||||
private class BasicComparator<T : Music> private constructor() : Comparator<T> {
|
||||
override fun compare(a: T, b: T): Int {
|
||||
val aSortName = a.sortName
|
||||
val bSortName = b.sortName
|
||||
return when {
|
||||
aSortName != null && bSortName != null ->
|
||||
aSortName.compareTo(bSortName, ignoreCase = true)
|
||||
aSortName == null && bSortName != null -> -1 // a < b
|
||||
aSortName == null && bSortName == null -> 0 // a = b
|
||||
aSortName != null && bSortName == null -> 1 // a < b
|
||||
else -> error("Unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
companion object {
|
||||
val SONG: Comparator<Song> = BasicComparator()
|
||||
val ALBUM: Comparator<Album> = BasicComparator()
|
||||
val ARTIST: Comparator<Artist> = BasicComparator()
|
||||
val GENRE: Comparator<Genre> = BasicComparator()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Exposed as Indexer relies on it at points
|
||||
val NULLABLE_INT_COMPARATOR =
|
||||
Comparator<Int?> { a, b ->
|
||||
when {
|
||||
a != null && b != null -> a.compareTo(b)
|
||||
a == null && b != null -> -1 // a < b
|
||||
a == null && b == null -> 0 // a = b
|
||||
a != null && b == null -> 1 // a < b
|
||||
else -> error("Unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
fun fromItemId(@IdRes itemId: Int) =
|
||||
when (itemId) {
|
||||
ByName.itemId -> ByName
|
||||
ByAlbum.itemId -> ByAlbum
|
||||
ByArtist.itemId -> ByArtist
|
||||
ByYear.itemId -> ByYear
|
||||
ByDuration.itemId -> ByDuration
|
||||
ByCount.itemId -> ByCount
|
||||
ByDisc.itemId -> ByDisc
|
||||
ByTrack.itemId -> ByTrack
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Convert a sort's integer representation into a [Sort] instance.
|
||||
*
|
||||
|
@ -412,18 +388,20 @@ sealed class Sort(open val isAscending: Boolean) {
|
|||
*/
|
||||
fun fromIntCode(value: Int): Sort? {
|
||||
val isAscending = (value and 1) == 1
|
||||
val mode =
|
||||
when (value.shr(1)) {
|
||||
Mode.ByName.intCode -> Mode.ByName
|
||||
Mode.ByArtist.intCode -> Mode.ByArtist
|
||||
Mode.ByAlbum.intCode -> Mode.ByAlbum
|
||||
Mode.ByYear.intCode -> Mode.ByYear
|
||||
Mode.ByDuration.intCode -> Mode.ByDuration
|
||||
Mode.ByCount.intCode -> Mode.ByCount
|
||||
Mode.ByDisc.intCode -> Mode.ByDisc
|
||||
Mode.ByTrack.intCode -> Mode.ByTrack
|
||||
else -> return null
|
||||
}
|
||||
|
||||
return when (value.shr(1)) {
|
||||
IntegerTable.SORT_BY_NAME -> ByName(isAscending)
|
||||
IntegerTable.SORT_BY_ARTIST -> ByArtist(isAscending)
|
||||
IntegerTable.SORT_BY_ALBUM -> ByAlbum(isAscending)
|
||||
IntegerTable.SORT_BY_YEAR -> ByYear(isAscending)
|
||||
IntegerTable.SORT_BY_DURATION -> ByDuration(isAscending)
|
||||
IntegerTable.SORT_BY_COUNT -> ByCount(isAscending)
|
||||
IntegerTable.SORT_BY_DISC -> ByDisc(isAscending)
|
||||
IntegerTable.SORT_BY_TRACK -> ByTrack(isAscending)
|
||||
else -> null
|
||||
}
|
||||
return Sort(mode, isAscending)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,6 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import androidx.viewbinding.ViewBinding
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import org.oxycblt.auxio.R
|
||||
|
|
|
@ -37,8 +37,8 @@ import org.oxycblt.auxio.util.logD
|
|||
/**
|
||||
* A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the
|
||||
* widget state based off of that. This cannot be rolled into [WidgetProvider] directly, as it may
|
||||
* result in memory leaks if [PlaybackStateManager]/[SettingsManager] gets created and bound to
|
||||
* without being released.
|
||||
* result in memory leaks if [PlaybackStateManager]/[Settings] gets created and bound to without
|
||||
* being released.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class WidgetComponent(private val context: Context) :
|
||||
|
|
|
@ -35,9 +35,9 @@
|
|||
<string name="set_key_lib_artists_sort" translatable="false">auxio_artists_sort</string>
|
||||
<string name="set_key_lib_genres_sort" translatable="false">auxio_genres_sort</string>
|
||||
|
||||
<string name="set_key_lib_album_sort" translatable="false">auxio_album_sort</string>
|
||||
<string name="set_key_lib_artist_sort" translatable="false">auxio_artist_sort</string>
|
||||
<string name="set_key_lib_genre_sort" translatable="false">auxio_genre_sort</string>
|
||||
<string name="set_key_detail_album_sort" translatable="false">auxio_album_sort</string>
|
||||
<string name="set_key_detail_artist_sort" translatable="false">auxio_artist_sort</string>
|
||||
<string name="set_key_detail_genre_sort" translatable="false">auxio_genre_sort</string>
|
||||
|
||||
<string-array name="entries_theme">
|
||||
<item>@string/set_theme_auto</item>
|
||||
|
|
Loading…
Reference in a new issue