Backend sorting code

This commit is contained in:
Syer10
2022-03-12 17:00:36 -05:00
parent 5eaf6a680f
commit 6669acec4f
3 changed files with 112 additions and 41 deletions

View File

@@ -9,6 +9,7 @@ package ca.gosyer.data.library
import ca.gosyer.core.prefs.Preference
import ca.gosyer.core.prefs.PreferenceStore
import ca.gosyer.data.library.model.DisplayMode
import ca.gosyer.data.library.model.Sort
class LibraryPreferences(private val preferenceStore: PreferenceStore) {
@@ -16,6 +17,14 @@ class LibraryPreferences(private val preferenceStore: PreferenceStore) {
return preferenceStore.getJsonObject("display_mode", DisplayMode.CompactGrid, DisplayMode.serializer())
}
fun sortMode(): Preference<Sort> {
return preferenceStore.getJsonObject("sort_mode", Sort.ALPHABETICAL, Sort.serializer())
}
fun sortAscending(): Preference<Boolean> {
return preferenceStore.getBoolean("sort_ascending", true)
}
fun gridColumns(): Preference<Int> {
return preferenceStore.getInt("grid_columns", 0)
}

View File

@@ -0,0 +1,21 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package ca.gosyer.data.library.model
import kotlinx.serialization.Serializable
@Serializable
enum class Sort {
ALPHABETICAL,
// LAST_READ,
// LAST_CHECKED,
UNREAD,
// TOTAL_CHAPTERS,
// LATEST_CHAPTER,
// DATE_FETCHED,
DATE_ADDED;
}

View File

@@ -9,6 +9,7 @@ package ca.gosyer.ui.library
import ca.gosyer.core.lang.withDefaultContext
import ca.gosyer.core.logging.CKLogger
import ca.gosyer.data.library.LibraryPreferences
import ca.gosyer.data.library.model.Sort
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
@@ -19,55 +20,36 @@ import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.toList
import me.tatarka.inject.annotations.Inject
import java.text.Collator
import java.util.Collections
import java.util.Locale
private typealias CategoryItems = Pair<MutableStateFlow<List<Manga>>, MutableStateFlow<List<Manga>>>
private typealias CategoryItems = Pair<StateFlow<List<Manga>>, MutableStateFlow<List<Manga>>>
private typealias LibraryMap = MutableMap<Long, CategoryItems>
private data class Library(val categories: MutableStateFlow<List<Category>>, val mangaMap: LibraryMap)
private fun LibraryMap.getManga(id: Long) =
getOrPut(id) { MutableStateFlow(emptyList<Manga>()) to MutableStateFlow(emptyList()) }
private suspend fun LibraryMap.setManga(query: String?, id: Long, manga: List<Manga>) {
getManga(id).let { (items, unfilteredItems) ->
items.value = filterManga(query, manga)
unfilteredItems.value = manga
private fun LibraryMap.getManga(id: Long, getItemsFlow: (StateFlow<List<Manga>>) -> StateFlow<List<Manga>>) =
getOrPut(id) {
val unfilteredItems = MutableStateFlow<List<Manga>>(emptyList())
getItemsFlow(unfilteredItems) to unfilteredItems
}
}
private suspend fun LibraryMap.updateMangaFilter(query: String?) {
values.forEach { (items, unfilteredItems) ->
items.value = filterManga(query, unfilteredItems.value)
}
}
private suspend fun filterManga(query: String?, mangaList: List<Manga>): List<Manga> {
if (query.isNullOrBlank()) return mangaList
val queries = query.split(" ")
return mangaList.asFlow()
.filter { manga ->
queries.all { query ->
manga.title.contains(query, true) ||
manga.author.orEmpty().contains(query, true) ||
manga.artist.orEmpty().contains(query, true) ||
manga.genre.any { it.contains(query, true) } ||
manga.description.orEmpty().contains(query, true) ||
manga.status.name.contains(query, true)
}
}
.cancellable()
.buffer()
.toList()
private fun LibraryMap.setManga(id: Long, manga: List<Manga>, getItemsFlow: (StateFlow<List<Manga>>) -> StateFlow<List<Manga>>) {
getManga(id, getItemsFlow).second.value = manga
}
class LibraryScreenViewModel @Inject constructor(
@@ -87,6 +69,9 @@ class LibraryScreenViewModel @Inject constructor(
val gridColumns = libraryPreferences.gridColumns().stateIn(scope)
val gridSize = libraryPreferences.gridSize().stateIn(scope)
private val sortMode = libraryPreferences.sortMode().stateIn(scope)
private val sortAscending = libraryPreferences.sortAscending().stateIn(scope)
private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow()
@@ -96,12 +81,12 @@ class LibraryScreenViewModel @Inject constructor(
private val _query = MutableStateFlow("")
val query = _query.asStateFlow()
private val comparator = combine(sortMode, sortAscending) { sortMode, sortAscending ->
getComparator(sortMode, sortAscending)
}.stateIn(scope, SharingStarted.Eagerly, compareBy { it.title })
init {
getLibrary()
_query.mapLatest {
library.mangaMap.updateMangaFilter(it)
}.launchIn(scope)
}
private fun getLibrary() {
@@ -127,8 +112,64 @@ class LibraryScreenViewModel @Inject constructor(
_selectedCategoryIndex.value = page
}
private fun getComparator(sortMode: Sort, ascending: Boolean): Comparator<Manga> {
val sortFn = when (sortMode) {
Sort.ALPHABETICAL -> {
val locale = Locale.getDefault()
val collator = Collator.getInstance(locale).apply {
strength = Collator.PRIMARY
};
{ a: Manga, b: Manga ->
collator.compare(a.title.lowercase(locale), b.title.lowercase(locale))
}
}
Sort.UNREAD -> {
{ a: Manga, b: Manga ->
(a.unreadCount ?: 0).compareTo(b.unreadCount ?: 0)
}
}
Sort.DATE_ADDED -> {
{ a: Manga, b: Manga ->
a.inLibraryAt.compareTo(b.inLibraryAt)
}
}
}
return if (ascending) {
Comparator(sortFn)
} else {
Collections.reverseOrder(sortFn)
}
}
private suspend fun filterManga(query: String, mangaList: List<Manga>): List<Manga> {
if (query.isBlank()) return mangaList
val queries = query.split(" ")
return mangaList.asFlow()
.filter { manga ->
queries.all { query ->
manga.title.contains(query, true) ||
manga.author.orEmpty().contains(query, true) ||
manga.artist.orEmpty().contains(query, true) ||
manga.genre.any { it.contains(query, true) } ||
manga.description.orEmpty().contains(query, true) ||
manga.status.name.contains(query, true)
}
}
.cancellable()
.buffer()
.toList()
}
private fun getMangaItemsFlow(unfilteredItemsFlow: StateFlow<List<Manga>>): StateFlow<List<Manga>> {
return combine(unfilteredItemsFlow, query, comparator) { unfilteredItems, query, comparator ->
filterManga(query, unfilteredItems)
.sortedWith(comparator)
}.stateIn(scope, SharingStarted.Eagerly, emptyList())
}
fun getLibraryForCategoryId(id: Long): StateFlow<List<Manga>> {
return library.mangaMap.getManga(id).first.asStateFlow()
return library.mangaMap.getManga(id, ::getMangaItemsFlow).first
}
private suspend fun updateCategories(categories: List<Category>) {
@@ -136,14 +177,14 @@ class LibraryScreenViewModel @Inject constructor(
categories.map { category ->
async {
library.mangaMap.setManga(
query.value,
category.id,
categoryHandler.getMangaFromCategory(category)
id = category.id,
manga = categoryHandler.getMangaFromCategory(category)
.catch {
info { "Error getting manga for category $category" }
emit(emptyList())
}
.single()
.single(),
getItemsFlow = ::getMangaItemsFlow
)
}
}.awaitAll()