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
|
- 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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) :
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue