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:
OxygenCobalt 2022-03-19 15:41:03 -06:00
parent 90f10f2a84
commit 9c07ad2d34
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 201 additions and 129 deletions

View file

@ -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 {

View file

@ -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)

View file

@ -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
} }

View file

@ -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 -> {}
} }

View file

@ -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

View file

@ -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 =

View file

@ -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 ---

View file

@ -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) {

View file

@ -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))
} }
} }

View file

@ -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)
} }

View file

@ -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>