sort: completely rework class
Completely refactor the Sort class to be more efficient and straightforward. The original Sort class had *major* shortcomings, it was slow, poorly organized, and relied on abusing compareBy to implement special things like article sort. This rework eliminates all of that in favor of a new system relying on custom comparators and chaining to achieve something much faster and maintainable.
This commit is contained in:
parent
90f10f2a84
commit
9c07ad2d34
11 changed files with 201 additions and 129 deletions
|
@ -36,6 +36,9 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
* - Rework RecyclerView management and item dragging
|
* - Rework RecyclerView management and item dragging
|
||||||
* - Rework sealed classes to minimize whens and maximize overrides
|
* - Rework sealed classes to minimize whens and maximize overrides
|
||||||
* ```
|
* ```
|
||||||
|
*
|
||||||
|
* TODO: Dumpster int-codes for a 4-byte identifier (can still be in the form of an int) For
|
||||||
|
* example, instead of 0xA111 for ReplayGainMode.TRACK, you would instead have RTCK
|
||||||
*/
|
*/
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
class AuxioApp : Application(), ImageLoaderFactory {
|
class AuxioApp : Application(), ImageLoaderFactory {
|
||||||
|
|
|
@ -74,7 +74,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).sortAlbums(artist.albums)
|
val albums = Sort.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)
|
||||||
|
|
|
@ -162,7 +162,7 @@ class DetailViewModel : ViewModel() {
|
||||||
mShowMenu.value = MenuConfig(view, settingsManager.detailGenreSort)
|
mShowMenu.value = MenuConfig(view, settingsManager.detailGenreSort)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
data.addAll(settingsManager.detailGenreSort.sortGenre(curGenre.value!!))
|
data.addAll(settingsManager.detailGenreSort.genre(curGenre.value!!))
|
||||||
|
|
||||||
mGenreData.value = data
|
mGenreData.value = data
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,7 @@ class DetailViewModel : ViewModel() {
|
||||||
|
|
||||||
data.add(Header(id = -2, string = R.string.lbl_albums))
|
data.add(Header(id = -2, string = R.string.lbl_albums))
|
||||||
|
|
||||||
data.addAll(Sort.ByYear(false).sortAlbums(artist.albums))
|
data.addAll(Sort.ByYear(false).albums(artist.albums))
|
||||||
|
|
||||||
data.add(
|
data.add(
|
||||||
ActionHeader(
|
ActionHeader(
|
||||||
|
@ -187,7 +187,7 @@ class DetailViewModel : ViewModel() {
|
||||||
mShowMenu.value = MenuConfig(view, settingsManager.detailArtistSort)
|
mShowMenu.value = MenuConfig(view, settingsManager.detailArtistSort)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
data.addAll(settingsManager.detailArtistSort.sortArtist(artist))
|
data.addAll(settingsManager.detailArtistSort.artist(artist))
|
||||||
|
|
||||||
mArtistData.value = data.toList()
|
mArtistData.value = data.toList()
|
||||||
}
|
}
|
||||||
|
@ -208,7 +208,7 @@ class DetailViewModel : ViewModel() {
|
||||||
mShowMenu.value = MenuConfig(view, settingsManager.detailAlbumSort)
|
mShowMenu.value = MenuConfig(view, settingsManager.detailAlbumSort)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
data.addAll(settingsManager.detailAlbumSort.sortAlbum(curAlbum.value!!))
|
data.addAll(settingsManager.detailAlbumSort.album(curAlbum.value!!))
|
||||||
|
|
||||||
mAlbumData.value = data
|
mAlbumData.value = data
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,10 +81,10 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val musicStore = MusicStore.awaitInstance()
|
val musicStore = MusicStore.awaitInstance()
|
||||||
mSongs.value = settingsManager.libSongSort.sortSongs(musicStore.songs)
|
mSongs.value = settingsManager.libSongSort.songs(musicStore.songs)
|
||||||
mAlbums.value = settingsManager.libAlbumSort.sortAlbums(musicStore.albums)
|
mAlbums.value = settingsManager.libAlbumSort.albums(musicStore.albums)
|
||||||
mArtists.value = settingsManager.libArtistSort.sortParents(musicStore.artists)
|
mArtists.value = settingsManager.libArtistSort.artists(musicStore.artists)
|
||||||
mGenres.value = settingsManager.libGenreSort.sortParents(musicStore.genres)
|
mGenres.value = settingsManager.libGenreSort.genres(musicStore.genres)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,19 +113,19 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
when (mCurTab.value) {
|
when (mCurTab.value) {
|
||||||
DisplayMode.SHOW_SONGS -> {
|
DisplayMode.SHOW_SONGS -> {
|
||||||
settingsManager.libSongSort = sort
|
settingsManager.libSongSort = sort
|
||||||
mSongs.value = sort.sortSongs(mSongs.value!!)
|
mSongs.value = sort.songs(mSongs.value!!)
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ALBUMS -> {
|
DisplayMode.SHOW_ALBUMS -> {
|
||||||
settingsManager.libAlbumSort = sort
|
settingsManager.libAlbumSort = sort
|
||||||
mAlbums.value = sort.sortAlbums(mAlbums.value!!)
|
mAlbums.value = sort.albums(mAlbums.value!!)
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_ARTISTS -> {
|
DisplayMode.SHOW_ARTISTS -> {
|
||||||
settingsManager.libArtistSort = sort
|
settingsManager.libArtistSort = sort
|
||||||
mArtists.value = sort.sortParents(mArtists.value!!)
|
mArtists.value = sort.artists(mArtists.value!!)
|
||||||
}
|
}
|
||||||
DisplayMode.SHOW_GENRES -> {
|
DisplayMode.SHOW_GENRES -> {
|
||||||
settingsManager.libGenreSort = sort
|
settingsManager.libGenreSort = sort
|
||||||
mGenres.value = sort.sortParents(mGenres.value!!)
|
mGenres.value = sort.genres(mGenres.value!!)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,19 +69,11 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* - Added documentation
|
* - Added documentation
|
||||||
*
|
*
|
||||||
* @author Hai Zhang, OxygenCobalt
|
* @author Hai Zhang, OxygenCobalt
|
||||||
*
|
|
||||||
* TODO: Fix strange touch behavior when the pointer is slightly outside of the view.
|
|
||||||
*
|
|
||||||
* TODO: Really try to make this view less insane.
|
|
||||||
*/
|
*/
|
||||||
class FastScrollRecyclerView
|
class FastScrollRecyclerView
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
EdgeRecyclerView(context, attrs, defStyleAttr) {
|
EdgeRecyclerView(context, attrs, defStyleAttr) {
|
||||||
private val minTouchTargetSize =
|
|
||||||
context.getDimenSizeSafe(R.dimen.fast_scroll_thumb_touch_target_size)
|
|
||||||
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
||||||
|
|
||||||
// Thumb
|
// Thumb
|
||||||
private val thumbView =
|
private val thumbView =
|
||||||
View(context).apply {
|
View(context).apply {
|
||||||
|
@ -102,6 +94,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val scrollPositionChildRect = Rect()
|
||||||
|
|
||||||
// Popup
|
// Popup
|
||||||
private val popupView =
|
private val popupView =
|
||||||
FastScrollPopupView(context).apply {
|
FastScrollPopupView(context).apply {
|
||||||
|
@ -118,7 +112,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
private var showingPopup = false
|
private var showingPopup = false
|
||||||
|
|
||||||
// Touch events
|
// Touch
|
||||||
|
private val minTouchTargetSize =
|
||||||
|
context.getDimenSizeSafe(R.dimen.fast_scroll_thumb_touch_target_size)
|
||||||
|
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||||
|
|
||||||
private var downX = 0f
|
private var downX = 0f
|
||||||
private var downY = 0f
|
private var downY = 0f
|
||||||
private var lastY = 0f
|
private var lastY = 0f
|
||||||
|
@ -151,8 +149,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
onDragListener?.invoke(value)
|
onDragListener?.invoke(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val childRect = Rect()
|
|
||||||
|
|
||||||
/** Callback to provide a string to be shown on the popup when an item is passed */
|
/** Callback to provide a string to be shown on the popup when an item is passed */
|
||||||
var popupProvider: ((Int) -> String)? = null
|
var popupProvider: ((Int) -> String)? = null
|
||||||
|
|
||||||
|
@ -303,8 +299,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
// Combine the previous item dimensions with the current item top to find our scroll
|
// Combine the previous item dimensions with the current item top to find our scroll
|
||||||
// position
|
// position
|
||||||
getDecoratedBoundsWithMargins(getChildAt(0), childRect)
|
getDecoratedBoundsWithMargins(getChildAt(0), scrollPositionChildRect)
|
||||||
val scrollOffset = paddingTop + (firstAdapterPos * itemHeight) - childRect.top
|
val scrollOffset = paddingTop + (firstAdapterPos * itemHeight) - scrollPositionChildRect.top
|
||||||
|
|
||||||
// Then calculate the thumb position, which is just:
|
// Then calculate the thumb position, which is just:
|
||||||
// [proportion of scroll position to scroll range] * [total thumb range]
|
// [proportion of scroll position to scroll range] * [total thumb range]
|
||||||
|
@ -497,8 +493,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
val itemView = getChildAt(0)
|
val itemView = getChildAt(0)
|
||||||
getDecoratedBoundsWithMargins(itemView, childRect)
|
getDecoratedBoundsWithMargins(itemView, scrollPositionChildRect)
|
||||||
return childRect.height()
|
return scrollPositionChildRect.height()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val itemCount: Int
|
private val itemCount: Int
|
||||||
|
|
|
@ -27,6 +27,7 @@ import androidx.core.database.getStringOrNull
|
||||||
import androidx.core.text.isDigitsOnly
|
import androidx.core.text.isDigitsOnly
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.excluded.ExcludedDatabase
|
import org.oxycblt.auxio.music.excluded.ExcludedDatabase
|
||||||
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -277,9 +278,17 @@ class MusicLoader {
|
||||||
// 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.
|
||||||
// TODO: Weigh songs with null years lower than songs with zero years
|
// Note: Normally we could want to use something like maxByWith, but apparently
|
||||||
val templateSong =
|
// that does not exist in the kotlin stdlib yet.
|
||||||
requireNotNull(albumSongs.maxByOrNull { it.internalMediaStoreYear ?: 0 })
|
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 albumName = templateSong.internalMediaStoreAlbumName
|
val albumName = templateSong.internalMediaStoreAlbumName
|
||||||
val albumYear = templateSong.internalMediaStoreYear
|
val albumYear = templateSong.internalMediaStoreYear
|
||||||
val albumCoverUri =
|
val albumCoverUri =
|
||||||
|
|
|
@ -240,7 +240,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
/** Add an [Album] to the top of the queue. */
|
/** Add an [Album] to the top of the queue. */
|
||||||
fun playNext(album: Album) {
|
fun playNext(album: Album) {
|
||||||
playbackManager.playNext(settingsManager.detailAlbumSort.sortAlbum(album))
|
playbackManager.playNext(settingsManager.detailAlbumSort.album(album))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Add a [Song] to the end of the queue. */
|
/** Add a [Song] to the end of the queue. */
|
||||||
|
@ -250,7 +250,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
|
||||||
/** Add an [Album] to the end of the queue. */
|
/** Add an [Album] to the end of the queue. */
|
||||||
fun addToQueue(album: Album) {
|
fun addToQueue(album: Album) {
|
||||||
playbackManager.addToQueue(settingsManager.detailAlbumSort.sortAlbum(album))
|
playbackManager.addToQueue(settingsManager.detailAlbumSort.album(album))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- STATUS FUNCTIONS ---
|
// --- STATUS FUNCTIONS ---
|
||||||
|
|
|
@ -376,13 +376,13 @@ class PlaybackStateManager private constructor() {
|
||||||
mQueue =
|
mQueue =
|
||||||
when (mPlaybackMode) {
|
when (mPlaybackMode) {
|
||||||
PlaybackMode.ALL_SONGS ->
|
PlaybackMode.ALL_SONGS ->
|
||||||
settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList()
|
settingsManager.libSongSort.songs(musicStore.songs).toMutableList()
|
||||||
PlaybackMode.IN_ALBUM ->
|
PlaybackMode.IN_ALBUM ->
|
||||||
settingsManager.detailAlbumSort.sortAlbum(mParent as Album).toMutableList()
|
settingsManager.detailAlbumSort.album(mParent as Album).toMutableList()
|
||||||
PlaybackMode.IN_ARTIST ->
|
PlaybackMode.IN_ARTIST ->
|
||||||
settingsManager.detailArtistSort.sortArtist(mParent as Artist).toMutableList()
|
settingsManager.detailArtistSort.artist(mParent as Artist).toMutableList()
|
||||||
PlaybackMode.IN_GENRE ->
|
PlaybackMode.IN_GENRE ->
|
||||||
settingsManager.detailGenreSort.sortGenre(mParent as Genre).toMutableList()
|
settingsManager.detailGenreSort.genre(mParent as Genre).toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keepSong) {
|
if (keepSong) {
|
||||||
|
|
|
@ -88,28 +88,28 @@ class SearchViewModel : ViewModel() {
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
||||||
musicStore.artists.filterByOrNull(query)?.let { artists ->
|
musicStore.artists.filterByOrNull(query)?.let { artists ->
|
||||||
results.add(Header(-1, R.string.lbl_artists))
|
results.add(Header(-1, R.string.lbl_artists))
|
||||||
results.addAll(sort.sortParents(artists))
|
results.addAll(sort.artists(artists))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
||||||
musicStore.albums.filterByOrNull(query)?.let { albums ->
|
musicStore.albums.filterByOrNull(query)?.let { albums ->
|
||||||
results.add(Header(-2, R.string.lbl_albums))
|
results.add(Header(-2, R.string.lbl_albums))
|
||||||
results.addAll(sort.sortAlbums(albums))
|
results.addAll(sort.albums(albums))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
||||||
musicStore.genres.filterByOrNull(query)?.let { genres ->
|
musicStore.genres.filterByOrNull(query)?.let { genres ->
|
||||||
results.add(Header(-3, R.string.lbl_genres))
|
results.add(Header(-3, R.string.lbl_genres))
|
||||||
results.addAll(sort.sortParents(genres))
|
results.addAll(sort.genres(genres))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
||||||
musicStore.songs.filterByOrNull(query)?.let { songs ->
|
musicStore.songs.filterByOrNull(query)?.let { songs ->
|
||||||
results.add(Header(-4, R.string.lbl_songs))
|
results.add(Header(-4, R.string.lbl_songs))
|
||||||
results.addAll(sort.sortSongs(songs))
|
results.addAll(sort.songs(songs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@ import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
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.MusicParent
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A data class representing the sort modes used in Auxio.
|
* A data class representing the sort modes used in Auxio.
|
||||||
|
@ -43,14 +43,95 @@ import org.oxycblt.auxio.music.Song
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
sealed class Sort(open val isAscending: Boolean) {
|
sealed class Sort(open val isAscending: Boolean) {
|
||||||
|
open fun songs(songs: Collection<Song>): List<Song> {
|
||||||
|
logW("This sort is not supported for songs")
|
||||||
|
return songs.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun albums(albums: Collection<Album>): List<Album> {
|
||||||
|
logW("This sort is not supported for albums")
|
||||||
|
return albums.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun artists(artists: Collection<Artist>): List<Artist> {
|
||||||
|
logW("This sort is not supported for artists")
|
||||||
|
return artists.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun genres(genres: Collection<Genre>): List<Genre> {
|
||||||
|
logW("This sort is not supported for genres")
|
||||||
|
return genres.toList()
|
||||||
|
}
|
||||||
|
|
||||||
/** Sort by the names of an item */
|
/** Sort by the names of an item */
|
||||||
class ByName(override val isAscending: Boolean) : Sort(isAscending)
|
class ByName(override val isAscending: Boolean) : Sort(isAscending) {
|
||||||
/** Sort by the artist of an item, only supported by [Album] and [Song] */
|
override fun songs(songs: Collection<Song>): List<Song> {
|
||||||
class ByArtist(override val isAscending: Boolean) : Sort(isAscending)
|
return songs.sortedWith(compareByDynamic(NameComparator()) { it })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun albums(albums: Collection<Album>): List<Album> {
|
||||||
|
return albums.sortedWith(compareByDynamic(NameComparator()) { it })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun artists(artists: Collection<Artist>): List<Artist> {
|
||||||
|
return artists.sortedWith(compareByDynamic(NameComparator()) { it })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun genres(genres: Collection<Genre>): List<Genre> {
|
||||||
|
return genres.sortedWith(compareByDynamic(NameComparator()) { it })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Sort by the album of an item, only supported by [Song] */
|
/** Sort by the album of an item, only supported by [Song] */
|
||||||
class ByAlbum(override val isAscending: Boolean) : Sort(isAscending)
|
class ByAlbum(override val isAscending: Boolean) : Sort(isAscending) {
|
||||||
|
override fun songs(songs: Collection<Song>): List<Song> {
|
||||||
|
return songs.sortedWith(
|
||||||
|
MultiComparator(
|
||||||
|
compareByDynamic(NameComparator()) { it.album },
|
||||||
|
compareBy(NullableComparator()) { it.track },
|
||||||
|
compareBy(NameComparator()) { it }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sort by the artist of an item, only supported by [Album] and [Song] */
|
||||||
|
class ByArtist(override val isAscending: Boolean) : Sort(isAscending) {
|
||||||
|
override fun songs(songs: Collection<Song>): List<Song> {
|
||||||
|
return songs.sortedWith(
|
||||||
|
MultiComparator(
|
||||||
|
compareByDynamic(NameComparator()) { it.album.artist },
|
||||||
|
compareByDescending(NullableComparator()) { it.album.year },
|
||||||
|
compareByDescending(NameComparator()) { it.album },
|
||||||
|
compareBy(NullableComparator()) { it.track },
|
||||||
|
compareBy(NameComparator()) { it }))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun albums(albums: Collection<Album>): List<Album> {
|
||||||
|
return albums.sortedWith(
|
||||||
|
MultiComparator(
|
||||||
|
compareByDynamic(NameComparator()) { it.artist },
|
||||||
|
compareByDescending(NullableComparator()) { it.year },
|
||||||
|
compareBy(NameComparator()) { it }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Sort by the year of an item, only supported by [Album] and [Song] */
|
/** Sort by the year of an item, only supported by [Album] and [Song] */
|
||||||
class ByYear(override val isAscending: Boolean) : Sort(isAscending)
|
class ByYear(override val isAscending: Boolean) : Sort(isAscending) {
|
||||||
|
override fun songs(songs: Collection<Song>): List<Song> {
|
||||||
|
return songs.sortedWith(
|
||||||
|
MultiComparator(
|
||||||
|
compareByDynamic(NullableComparator()) { it.album.year },
|
||||||
|
compareByDescending(NameComparator()) { it.album },
|
||||||
|
compareBy(NullableComparator()) { it.track },
|
||||||
|
compareBy(NameComparator()) { it }))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun albums(albums: Collection<Album>): List<Album> {
|
||||||
|
return albums.sortedWith(
|
||||||
|
MultiComparator(
|
||||||
|
compareByDynamic(NullableComparator()) { it.year },
|
||||||
|
compareBy(NameComparator()) { it }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Get the corresponding item id for this sort. */
|
/** Get the corresponding item id for this sort. */
|
||||||
val itemId: Int
|
val itemId: Int
|
||||||
|
@ -89,72 +170,31 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort a list of [Song] instances to reflect this specific sort.
|
|
||||||
*
|
|
||||||
* Albums are sorted by ascending track, artists are sorted with [ByYear] descending.
|
|
||||||
*
|
|
||||||
* @return A sorted list of songs
|
|
||||||
*/
|
|
||||||
fun sortSongs(songs: Collection<Song>): List<Song> {
|
|
||||||
return when (this) {
|
|
||||||
is ByName -> songs.stringSort { it.resolvedName }
|
|
||||||
else ->
|
|
||||||
sortAlbums(songs.groupBy { it.album }.keys).flatMap { album ->
|
|
||||||
album.songs.intSort(true) { it.track ?: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort a list of [Album] instances to reflect this specific sort.
|
|
||||||
*
|
|
||||||
* Artists are sorted with [ByYear] descending.
|
|
||||||
*
|
|
||||||
* @return A sorted list of albums
|
|
||||||
*/
|
|
||||||
fun sortAlbums(albums: Collection<Album>): List<Album> {
|
|
||||||
return when (this) {
|
|
||||||
is ByName, is ByAlbum -> albums.stringSort { it.resolvedName }
|
|
||||||
is ByArtist ->
|
|
||||||
sortParents(albums.groupBy { it.artist }.keys).flatMap {
|
|
||||||
ByYear(false).sortAlbums(it.albums)
|
|
||||||
}
|
|
||||||
is ByYear -> albums.intSort { it.year ?: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sort a list of [MusicParent] instances to reflect this specific sort.
|
|
||||||
*
|
|
||||||
* @return A sorted list of the specific parent
|
|
||||||
*/
|
|
||||||
fun <T : MusicParent> sortParents(parents: Collection<T>): List<T> {
|
|
||||||
return parents.stringSort { it.resolvedName }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort the songs in an album.
|
* Sort the songs in an album.
|
||||||
* @see sortSongs
|
* @see songs
|
||||||
*/
|
*/
|
||||||
fun sortAlbum(album: Album): List<Song> {
|
fun album(album: Album): List<Song> {
|
||||||
return album.songs.intSort { it.track ?: 0 }
|
return album.songs.sortedWith(
|
||||||
|
MultiComparator(
|
||||||
|
compareByDynamic(NullableComparator()) { it.track },
|
||||||
|
compareBy(NameComparator()) { it }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort the songs in an artist.
|
* Sort the songs in an artist.
|
||||||
* @see sortSongs
|
* @see songs
|
||||||
*/
|
*/
|
||||||
fun sortArtist(artist: Artist): List<Song> {
|
fun artist(artist: Artist): List<Song> {
|
||||||
return sortSongs(artist.songs)
|
return songs(artist.songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort the songs in a genre.
|
* Sort the songs in a genre.
|
||||||
* @see sortSongs
|
* @see songs
|
||||||
*/
|
*/
|
||||||
fun sortGenre(genre: Genre): List<Song> {
|
fun genre(genre: Genre): List<Song> {
|
||||||
return sortSongs(genre.songs)
|
return songs(genre.songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert this sort to it's integer representation. */
|
/** Convert this sort to it's integer representation. */
|
||||||
|
@ -167,35 +207,59 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
}.shl(1) or if (isAscending) 1 else 0
|
}.shl(1) or if (isAscending) 1 else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Music> Collection<T>.stringSort(
|
protected inline fun <T : Music, K> compareByDynamic(
|
||||||
asc: Boolean = isAscending,
|
comparator: Comparator<in K>,
|
||||||
selector: (T) -> String
|
crossinline selector: (T) -> K
|
||||||
): List<T> {
|
): Comparator<T> {
|
||||||
// Chain whatever item call with sliceArticle for correctness
|
return if (isAscending) {
|
||||||
val chained: (T) -> String = { selector(it).sliceArticle() }
|
compareBy(comparator, selector)
|
||||||
|
} else {
|
||||||
val comparator =
|
compareByDescending(comparator, selector)
|
||||||
if (asc) {
|
}
|
||||||
compareBy(String.CASE_INSENSITIVE_ORDER, chained)
|
|
||||||
} else {
|
|
||||||
compareByDescending(String.CASE_INSENSITIVE_ORDER, chained)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortedWith(comparator)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Music> Collection<T>.intSort(
|
class NameComparator<T : Music> : Comparator<T> {
|
||||||
asc: Boolean = isAscending,
|
override fun compare(a: T?, b: T?): Int {
|
||||||
selector: (T) -> Int,
|
if (a == null && b != null) return -1 // -1 -> a < b
|
||||||
): List<T> {
|
if (a == null && b == null) return 0 // 0 -> 0 = b
|
||||||
val comparator =
|
if (a != null && b == null) return 1 // 1 -> a > b
|
||||||
if (asc) {
|
|
||||||
compareBy(selector)
|
return a!!.resolvedName
|
||||||
} else {
|
.sliceArticle()
|
||||||
compareByDescending(selector)
|
.compareTo(b!!.resolvedName.sliceArticle(), ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NullableComparator<T : Comparable<T>> : Comparator<T?> {
|
||||||
|
override fun compare(a: T?, b: T?): Int {
|
||||||
|
if (a == null && b != null) return -1 // -1 -> a < b
|
||||||
|
if (a == null && b == null) return 0 // 0 -> 0 = b
|
||||||
|
if (a != null && b == null) return 1 // 1 -> a > b
|
||||||
|
return a!!.compareTo(b!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 mComparators = comparators
|
||||||
|
|
||||||
|
override fun compare(a: T?, b: T?): Int {
|
||||||
|
for (comparator in mComparators) {
|
||||||
|
val result = comparator.compare(a, b)
|
||||||
|
if (result != 0) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortedWith(comparator)
|
return 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -229,15 +293,15 @@ sealed class Sort(open val isAscending: Boolean) {
|
||||||
* languages.
|
* languages.
|
||||||
*/
|
*/
|
||||||
fun String.sliceArticle(): String {
|
fun String.sliceArticle(): String {
|
||||||
if (length > 5 && startsWith("the ", true)) {
|
if (length > 5 && startsWith("the ", ignoreCase = true)) {
|
||||||
return slice(4..lastIndex)
|
return slice(4..lastIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (length > 4 && startsWith("an ", true)) {
|
if (length > 4 && startsWith("an ", ignoreCase = true)) {
|
||||||
return slice(3..lastIndex)
|
return slice(3..lastIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (length > 3 && startsWith("a ", true)) {
|
if (length > 3 && startsWith("a ", ignoreCase = true)) {
|
||||||
return slice(2..lastIndex)
|
return slice(2..lastIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<dimen name="elevation_small">2dp</dimen>
|
<dimen name="elevation_small">2dp</dimen>
|
||||||
<dimen name="elevation_normal">4dp</dimen>
|
<dimen name="elevation_normal">4dp</dimen>
|
||||||
|
|
||||||
<dimen name="fast_scroll_popup_min_width">80dp</dimen>
|
<dimen name="fast_scroll_popup_min_width">78dp</dimen>
|
||||||
<dimen name="fast_scroll_popup_min_height">64dp</dimen>
|
<dimen name="fast_scroll_popup_min_height">64dp</dimen>
|
||||||
<dimen name="fast_scroll_popup_padding_start">@dimen/spacing_medium</dimen>
|
<dimen name="fast_scroll_popup_padding_start">@dimen/spacing_medium</dimen>
|
||||||
<dimen name="fast_scroll_popup_padding_end">28dp</dimen>
|
<dimen name="fast_scroll_popup_padding_end">28dp</dimen>
|
||||||
|
|
Loading…
Reference in a new issue