|
|
|
|
@ -15,16 +15,20 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|
|
|
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
|
|
|
import kotlinx.coroutines.flow.flow
|
|
|
|
|
import kotlinx.coroutines.flow.map
|
|
|
|
|
import kotlinx.coroutines.flow.first
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import kotlinx.coroutines.flow.stateIn
|
|
|
|
|
import kotlinx.coroutines.flow.update
|
|
|
|
|
import kotlinx.coroutines.plus
|
|
|
|
|
import org.koitharu.kotatsu.core.model.MangaSource
|
|
|
|
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
|
|
|
import org.koitharu.kotatsu.core.model.unwrap
|
|
|
|
|
import org.koitharu.kotatsu.core.util.LocaleComparator
|
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.asFlow
|
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
|
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
|
|
|
|
import org.koitharu.kotatsu.filter.data.SavedFiltersRepository
|
|
|
|
|
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
|
|
|
|
import org.koitharu.kotatsu.filter.ui.tags.TagTitleComparator
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
|
|
|
|
@ -42,423 +46,476 @@ import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
|
|
|
|
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
|
|
|
|
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
|
|
|
|
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
|
|
|
|
import org.json.JSONObject
|
|
|
|
|
import java.util.Calendar
|
|
|
|
|
import java.util.Locale
|
|
|
|
|
import javax.inject.Inject
|
|
|
|
|
|
|
|
|
|
@ViewModelScoped
|
|
|
|
|
class FilterCoordinator @Inject constructor(
|
|
|
|
|
savedStateHandle: SavedStateHandle,
|
|
|
|
|
mangaRepositoryFactory: MangaRepository.Factory,
|
|
|
|
|
private val searchRepository: MangaSearchRepository,
|
|
|
|
|
lifecycle: ViewModelLifecycle,
|
|
|
|
|
savedStateHandle: SavedStateHandle,
|
|
|
|
|
mangaRepositoryFactory: MangaRepository.Factory,
|
|
|
|
|
private val searchRepository: MangaSearchRepository,
|
|
|
|
|
private val savedFiltersRepository: SavedFiltersRepository,
|
|
|
|
|
lifecycle: ViewModelLifecycle,
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
private val coroutineScope = lifecycle.lifecycleScope + Dispatchers.Default
|
|
|
|
|
private val repository = mangaRepositoryFactory.create(MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE]))
|
|
|
|
|
private val sourceLocale = (repository.source as? MangaParserSource)?.locale
|
|
|
|
|
|
|
|
|
|
private val currentListFilter = MutableStateFlow(MangaListFilter.EMPTY)
|
|
|
|
|
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
|
|
|
|
|
|
|
|
|
|
private val availableSortOrders = repository.sortOrders
|
|
|
|
|
private val filterOptions = suspendLazy { repository.getFilterOptions() }
|
|
|
|
|
val capabilities = repository.filterCapabilities
|
|
|
|
|
|
|
|
|
|
val mangaSource: MangaSource
|
|
|
|
|
get() = repository.source
|
|
|
|
|
|
|
|
|
|
val isFilterApplied: Boolean
|
|
|
|
|
get() = currentListFilter.value.isNotEmpty()
|
|
|
|
|
|
|
|
|
|
val query: StateFlow<String?> = currentListFilter.map { it.query }
|
|
|
|
|
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
|
|
|
|
|
|
|
|
|
|
val sortOrder: StateFlow<FilterProperty<SortOrder>> = currentSortOrder.map { selected ->
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = availableSortOrders.sortedByOrdinal(),
|
|
|
|
|
selectedItem = selected,
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val tags: StateFlow<FilterProperty<MangaTag>> = combine(
|
|
|
|
|
getTopTags(TAGS_LIMIT),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.tags },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.addFirstDistinct(selected.tags),
|
|
|
|
|
selectedItems = selected.tags,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val tagsExcluded: StateFlow<FilterProperty<MangaTag>> = if (capabilities.isTagsExclusionSupported) {
|
|
|
|
|
combine(
|
|
|
|
|
getBottomTags(TAGS_LIMIT),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.tagsExclude },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.addFirstDistinct(selected.tagsExclude),
|
|
|
|
|
selectedItems = selected.tagsExclude,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
} else {
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val states: StateFlow<FilterProperty<MangaState>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.states },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableStates.sortedByOrdinal(),
|
|
|
|
|
selectedItems = selected.states,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val contentRating: StateFlow<FilterProperty<ContentRating>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.contentRating },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableContentRating.sortedByOrdinal(),
|
|
|
|
|
selectedItems = selected.contentRating,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val contentTypes: StateFlow<FilterProperty<ContentType>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.types },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableContentTypes.sortedByOrdinal(),
|
|
|
|
|
selectedItems = selected.types,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val demographics: StateFlow<FilterProperty<Demographic>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.demographics },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableDemographics.sortedByOrdinal(),
|
|
|
|
|
selectedItems = selected.demographics,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val locale: StateFlow<FilterProperty<Locale?>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.locale },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableLocales.sortedWithSafe(LocaleComparator()).addFirstDistinct(null),
|
|
|
|
|
selectedItems = setOfNotNull(selected.locale),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val originalLocale: StateFlow<FilterProperty<Locale?>> = if (capabilities.isOriginalLocaleSupported) {
|
|
|
|
|
combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.originalLocale },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableLocales.sortedWithSafe(LocaleComparator()).addFirstDistinct(null),
|
|
|
|
|
selectedItems = setOfNotNull(selected.originalLocale),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
} else {
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val year: StateFlow<FilterProperty<Int>> = if (capabilities.isYearSupported) {
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.year }.map { selected ->
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
|
|
|
|
selectedItems = setOf(selected.year),
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
} else {
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val yearRange: StateFlow<FilterProperty<Int>> = if (capabilities.isYearRangeSupported) {
|
|
|
|
|
currentListFilter.distinctUntilChanged { old, new ->
|
|
|
|
|
old.yearTo == new.yearTo && old.yearFrom == new.yearFrom
|
|
|
|
|
}.map { selected ->
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
|
|
|
|
selectedItems = setOf(selected.yearFrom.ifZero { YEAR_MIN }, selected.yearTo.ifZero { MAX_YEAR }),
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
} else {
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun reset() {
|
|
|
|
|
currentListFilter.value = MangaListFilter.EMPTY
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun snapshot() = Snapshot(
|
|
|
|
|
sortOrder = currentSortOrder.value,
|
|
|
|
|
listFilter = currentListFilter.value,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fun observe(): Flow<Snapshot> = combine(currentSortOrder, currentListFilter, ::Snapshot)
|
|
|
|
|
|
|
|
|
|
fun setSortOrder(newSortOrder: SortOrder) {
|
|
|
|
|
currentSortOrder.value = newSortOrder
|
|
|
|
|
repository.defaultSortOrder = newSortOrder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun set(value: MangaListFilter) {
|
|
|
|
|
currentListFilter.value = value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setAdjusted(value: MangaListFilter) {
|
|
|
|
|
var newFilter = value
|
|
|
|
|
if (!newFilter.author.isNullOrEmpty() && !capabilities.isAuthorSearchSupported) {
|
|
|
|
|
newFilter = newFilter.copy(
|
|
|
|
|
query = newFilter.author,
|
|
|
|
|
author = null,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (!capabilities.isSearchSupported && !newFilter.query.isNullOrEmpty()) {
|
|
|
|
|
newFilter = newFilter.copy(
|
|
|
|
|
query = null,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (!newFilter.query.isNullOrEmpty() && !newFilter.hasNonSearchOptions() && !capabilities.isSearchWithFiltersSupported) {
|
|
|
|
|
newFilter = MangaListFilter(query = newFilter.query)
|
|
|
|
|
}
|
|
|
|
|
set(newFilter)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setQuery(value: String?) {
|
|
|
|
|
val newQuery = value?.trim()?.nullIfEmpty()
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
if (capabilities.isSearchWithFiltersSupported || newQuery == null) {
|
|
|
|
|
oldValue.copy(query = newQuery)
|
|
|
|
|
} else {
|
|
|
|
|
MangaListFilter(query = newQuery)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setLocale(value: Locale?) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
locale = value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setAuthor(value: String?) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
author = value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setOriginalLocale(value: Locale?) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
originalLocale = value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setYear(value: Int) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
year = value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setYearRange(valueFrom: Int, valueTo: Int) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
yearFrom = valueFrom,
|
|
|
|
|
yearTo = valueTo,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleState(value: MangaState, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
states = if (isSelected) oldValue.states + value else oldValue.states - value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleContentRating(value: ContentRating, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
contentRating = if (isSelected) oldValue.contentRating + value else oldValue.contentRating - value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleDemographic(value: Demographic, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
demographics = if (isSelected) oldValue.demographics + value else oldValue.demographics - value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleContentType(value: ContentType, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
types = if (isSelected) oldValue.types + value else oldValue.types - value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleTag(value: MangaTag, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
val newTags = if (capabilities.isMultipleTagsSupported) {
|
|
|
|
|
if (isSelected) oldValue.tags + value else oldValue.tags - value
|
|
|
|
|
} else {
|
|
|
|
|
if (isSelected) setOf(value) else emptySet()
|
|
|
|
|
}
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
tags = newTags,
|
|
|
|
|
tagsExclude = oldValue.tagsExclude - newTags,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleTagExclude(value: MangaTag, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
val newTagsExclude = if (capabilities.isMultipleTagsSupported) {
|
|
|
|
|
if (isSelected) oldValue.tagsExclude + value else oldValue.tagsExclude - value
|
|
|
|
|
} else {
|
|
|
|
|
if (isSelected) setOf(value) else emptySet()
|
|
|
|
|
}
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
tags = oldValue.tags - newTagsExclude,
|
|
|
|
|
tagsExclude = newTagsExclude,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getAllTags(): Flow<Result<List<MangaTag>>> = filterOptions.asFlow().map {
|
|
|
|
|
it.map { x -> x.availableTags.sortedWithSafe(TagTitleComparator(sourceLocale)) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun MangaListFilter.takeQueryIfSupported() = when {
|
|
|
|
|
capabilities.isSearchWithFiltersSupported -> query
|
|
|
|
|
query.isNullOrEmpty() -> query
|
|
|
|
|
hasNonSearchOptions() -> null
|
|
|
|
|
else -> query
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun getTopTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
|
|
|
|
flow { emit(searchRepository.getTopTags(repository.source, limit)) },
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
) { suggested, options ->
|
|
|
|
|
val all = options.getOrNull()?.availableTags.orEmpty()
|
|
|
|
|
val result = ArrayList<MangaTag>(limit)
|
|
|
|
|
result.addAll(suggested.take(limit))
|
|
|
|
|
if (result.size < limit) {
|
|
|
|
|
result.addAll(all.shuffled().take(limit - result.size))
|
|
|
|
|
}
|
|
|
|
|
if (result.isNotEmpty()) {
|
|
|
|
|
Result.success(result)
|
|
|
|
|
} else {
|
|
|
|
|
options.map { result }
|
|
|
|
|
}
|
|
|
|
|
}.catch {
|
|
|
|
|
emit(Result.failure(it))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun getBottomTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
|
|
|
|
flow { emit(searchRepository.getRareTags(repository.source, limit)) },
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
) { suggested, options ->
|
|
|
|
|
val all = options.getOrNull()?.availableTags.orEmpty()
|
|
|
|
|
val result = ArrayList<MangaTag>(limit)
|
|
|
|
|
result.addAll(suggested.take(limit))
|
|
|
|
|
if (result.size < limit) {
|
|
|
|
|
result.addAll(all.shuffled().take(limit - result.size))
|
|
|
|
|
}
|
|
|
|
|
if (result.isNotEmpty()) {
|
|
|
|
|
Result.success(result)
|
|
|
|
|
} else {
|
|
|
|
|
options.map { result }
|
|
|
|
|
}
|
|
|
|
|
}.catch {
|
|
|
|
|
emit(Result.failure(it))
|
|
|
|
|
}
|
|
|
|
|
private val coroutineScope = lifecycle.lifecycleScope + Dispatchers.Default
|
|
|
|
|
private val repository = mangaRepositoryFactory.create(MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE]))
|
|
|
|
|
private val sourceLocale = (repository.source as? MangaParserSource)?.locale
|
|
|
|
|
|
|
|
|
|
private val currentListFilter = MutableStateFlow(MangaListFilter.EMPTY)
|
|
|
|
|
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
|
|
|
|
|
private val currentPresetId = MutableStateFlow<Long?>(null)
|
|
|
|
|
private var lastAppliedPayload: JSONObject? = null
|
|
|
|
|
|
|
|
|
|
private val availableSortOrders = repository.sortOrders
|
|
|
|
|
private val filterOptions = suspendLazy { repository.getFilterOptions() }
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
coroutineScope.launch {
|
|
|
|
|
currentListFilter.collect { lf ->
|
|
|
|
|
val applied = lastAppliedPayload
|
|
|
|
|
if (applied != null) {
|
|
|
|
|
val cur = savedFiltersRepository.serializeFilter(lf)
|
|
|
|
|
if (cur.toString() != applied.toString()) {
|
|
|
|
|
currentPresetId.value = null
|
|
|
|
|
lastAppliedPayload = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val capabilities = repository.filterCapabilities
|
|
|
|
|
|
|
|
|
|
val mangaSource: MangaSource
|
|
|
|
|
get() = repository.source
|
|
|
|
|
|
|
|
|
|
val isFilterApplied: Boolean
|
|
|
|
|
get() = currentListFilter.value.isNotEmpty()
|
|
|
|
|
|
|
|
|
|
val query: StateFlow<String?> = currentListFilter.map { it.query }
|
|
|
|
|
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
|
|
|
|
|
|
|
|
|
|
val sortOrder: StateFlow<FilterProperty<SortOrder>> = currentSortOrder.map { selected ->
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = availableSortOrders.sortedByOrdinal(),
|
|
|
|
|
selectedItem = selected,
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val tags: StateFlow<FilterProperty<MangaTag>> = combine(
|
|
|
|
|
getTopTags(TAGS_LIMIT),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.tags },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.addFirstDistinct(selected.tags),
|
|
|
|
|
selectedItems = selected.tags,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val tagsExcluded: StateFlow<FilterProperty<MangaTag>> = if (capabilities.isTagsExclusionSupported) {
|
|
|
|
|
combine(
|
|
|
|
|
getBottomTags(TAGS_LIMIT),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.tagsExclude },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.addFirstDistinct(selected.tagsExclude),
|
|
|
|
|
selectedItems = selected.tagsExclude,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
} else {
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val states: StateFlow<FilterProperty<MangaState>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.states },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableStates.sortedByOrdinal(),
|
|
|
|
|
selectedItems = selected.states,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val contentRating: StateFlow<FilterProperty<ContentRating>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.contentRating },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableContentRating.sortedByOrdinal(),
|
|
|
|
|
selectedItems = selected.contentRating,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val contentTypes: StateFlow<FilterProperty<ContentType>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.types },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableContentTypes.sortedByOrdinal(),
|
|
|
|
|
selectedItems = selected.types,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val demographics: StateFlow<FilterProperty<Demographic>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.demographics },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableDemographics.sortedByOrdinal(),
|
|
|
|
|
selectedItems = selected.demographics,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val locale: StateFlow<FilterProperty<Locale?>> = combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.locale },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableLocales.sortedWithSafe(LocaleComparator()).addFirstDistinct(null),
|
|
|
|
|
selectedItems = setOfNotNull(selected.locale),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
|
|
val originalLocale: StateFlow<FilterProperty<Locale?>> = if (capabilities.isOriginalLocaleSupported) {
|
|
|
|
|
combine(
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.originalLocale },
|
|
|
|
|
) { available, selected ->
|
|
|
|
|
available.fold(
|
|
|
|
|
onSuccess = {
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = it.availableLocales.sortedWithSafe(LocaleComparator()).addFirstDistinct(null),
|
|
|
|
|
selectedItems = setOfNotNull(selected.originalLocale),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
onFailure = {
|
|
|
|
|
FilterProperty.error(it)
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
} else {
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val year: StateFlow<FilterProperty<Int>> = if (capabilities.isYearSupported) {
|
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.year }.map { selected ->
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
|
|
|
|
selectedItems = setOf(selected.year),
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
} else {
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val yearRange: StateFlow<FilterProperty<Int>> = if (capabilities.isYearRangeSupported) {
|
|
|
|
|
currentListFilter.distinctUntilChanged { old, new ->
|
|
|
|
|
old.yearTo == new.yearTo && old.yearFrom == new.yearFrom
|
|
|
|
|
}.map { selected ->
|
|
|
|
|
FilterProperty(
|
|
|
|
|
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
|
|
|
|
selectedItems = setOf(selected.yearFrom.ifZero { YEAR_MIN }, selected.yearTo.ifZero { MAX_YEAR }),
|
|
|
|
|
)
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
} else {
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val savedPresets: StateFlow<List<SavedFiltersRepository.Preset>> =
|
|
|
|
|
savedFiltersRepository.observe(repository.source.unwrap().name)
|
|
|
|
|
.stateIn(coroutineScope, SharingStarted.Eagerly, emptyList())
|
|
|
|
|
|
|
|
|
|
val selectedPresetId: StateFlow<Long?> = currentPresetId
|
|
|
|
|
|
|
|
|
|
fun reset() {
|
|
|
|
|
currentListFilter.value = MangaListFilter.EMPTY
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun snapshot() = Snapshot(
|
|
|
|
|
sortOrder = currentSortOrder.value,
|
|
|
|
|
listFilter = currentListFilter.value,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
fun observe(): Flow<Snapshot> = combine(currentSortOrder, currentListFilter, ::Snapshot)
|
|
|
|
|
|
|
|
|
|
fun setSortOrder(newSortOrder: SortOrder) {
|
|
|
|
|
currentSortOrder.value = newSortOrder
|
|
|
|
|
repository.defaultSortOrder = newSortOrder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun set(value: MangaListFilter) {
|
|
|
|
|
currentListFilter.value = value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setAdjusted(value: MangaListFilter) {
|
|
|
|
|
var newFilter = value
|
|
|
|
|
if (!newFilter.author.isNullOrEmpty() && !capabilities.isAuthorSearchSupported) {
|
|
|
|
|
newFilter = newFilter.copy(
|
|
|
|
|
query = newFilter.author,
|
|
|
|
|
author = null,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (!newFilter.query.isNullOrEmpty() && !newFilter.hasNonSearchOptions() && !capabilities.isSearchWithFiltersSupported) {
|
|
|
|
|
newFilter = MangaListFilter(query = newFilter.query)
|
|
|
|
|
}
|
|
|
|
|
set(newFilter)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun saveCurrentPreset(name: String) {
|
|
|
|
|
val preset = savedFiltersRepository.save(repository.source.unwrap().name, name, currentListFilter.value)
|
|
|
|
|
currentPresetId.value = preset.id
|
|
|
|
|
lastAppliedPayload = preset.payload
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun applyPreset(preset: SavedFiltersRepository.Preset) {
|
|
|
|
|
coroutineScope.launch {
|
|
|
|
|
val available = filterOptions.asFlow().map { it.getOrNull()?.availableTags.orEmpty() }.first()
|
|
|
|
|
val byKey: (Set<String>) -> Set<MangaTag> = { keys ->
|
|
|
|
|
val all = available.associateBy { it.key }
|
|
|
|
|
keys.mapNotNull { all[it] }.toSet()
|
|
|
|
|
}
|
|
|
|
|
val filter = savedFiltersRepository.deserializeFilter(preset.payload, byKey)
|
|
|
|
|
setAdjusted(filter)
|
|
|
|
|
currentPresetId.value = preset.id
|
|
|
|
|
lastAppliedPayload = preset.payload
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun renamePreset(id: Long, newName: String) {
|
|
|
|
|
savedFiltersRepository.rename(repository.source.unwrap().name, id, newName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun deletePreset(id: Long) {
|
|
|
|
|
savedFiltersRepository.delete(repository.source.unwrap().name, id)
|
|
|
|
|
if (currentPresetId.value == id) {
|
|
|
|
|
currentPresetId.value = null
|
|
|
|
|
lastAppliedPayload = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setQuery(value: String?) {
|
|
|
|
|
val newQuery = value?.trim()?.nullIfEmpty()
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
if (capabilities.isSearchWithFiltersSupported || newQuery == null) {
|
|
|
|
|
oldValue.copy(query = newQuery)
|
|
|
|
|
} else {
|
|
|
|
|
MangaListFilter(query = newQuery)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setLocale(value: Locale?) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
locale = value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setAuthor(value: String?) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
author = value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setOriginalLocale(value: Locale?) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
originalLocale = value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setYear(value: Int) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
year = value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setYearRange(valueFrom: Int, valueTo: Int) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
yearFrom = valueFrom,
|
|
|
|
|
yearTo = valueTo,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleState(value: MangaState, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
states = if (isSelected) oldValue.states + value else oldValue.states - value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleContentRating(value: ContentRating, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
contentRating = if (isSelected) oldValue.contentRating + value else oldValue.contentRating - value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleDemographic(value: Demographic, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
demographics = if (isSelected) oldValue.demographics + value else oldValue.demographics - value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleContentType(value: ContentType, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
types = if (isSelected) oldValue.types + value else oldValue.types - value,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleTag(value: MangaTag, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
val newTags = if (capabilities.isMultipleTagsSupported) {
|
|
|
|
|
if (isSelected) oldValue.tags + value else oldValue.tags - value
|
|
|
|
|
} else {
|
|
|
|
|
if (isSelected) setOf(value) else emptySet()
|
|
|
|
|
}
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
tags = newTags,
|
|
|
|
|
tagsExclude = oldValue.tagsExclude - newTags,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toggleTagExclude(value: MangaTag, isSelected: Boolean) {
|
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
|
val newTagsExclude = if (capabilities.isMultipleTagsSupported) {
|
|
|
|
|
if (isSelected) oldValue.tagsExclude + value else oldValue.tagsExclude - value
|
|
|
|
|
} else {
|
|
|
|
|
if (isSelected) setOf(value) else emptySet()
|
|
|
|
|
}
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
tags = oldValue.tags - newTagsExclude,
|
|
|
|
|
tagsExclude = newTagsExclude,
|
|
|
|
|
query = oldValue.takeQueryIfSupported(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getAllTags(): Flow<Result<List<MangaTag>>> = filterOptions.asFlow().map {
|
|
|
|
|
it.map { x -> x.availableTags.sortedWithSafe(TagTitleComparator(sourceLocale)) }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun MangaListFilter.takeQueryIfSupported() = when {
|
|
|
|
|
capabilities.isSearchWithFiltersSupported -> query
|
|
|
|
|
query.isNullOrEmpty() -> query
|
|
|
|
|
hasNonSearchOptions() -> null
|
|
|
|
|
else -> query
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun getTopTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
|
|
|
|
flow { emit(searchRepository.getTopTags(repository.source, limit)) },
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
) { suggested, options ->
|
|
|
|
|
val all = options.getOrNull()?.availableTags.orEmpty()
|
|
|
|
|
val result = ArrayList<MangaTag>(limit)
|
|
|
|
|
result.addAll(suggested.take(limit))
|
|
|
|
|
if (result.size < limit) {
|
|
|
|
|
result.addAll(all.shuffled().take(limit - result.size))
|
|
|
|
|
}
|
|
|
|
|
if (result.isNotEmpty()) {
|
|
|
|
|
Result.success(result)
|
|
|
|
|
} else {
|
|
|
|
|
options.map { result }
|
|
|
|
|
}
|
|
|
|
|
}.catch {
|
|
|
|
|
emit(Result.failure(it))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun getBottomTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
|
|
|
|
flow { emit(searchRepository.getRareTags(repository.source, limit)) },
|
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
|
) { suggested, options ->
|
|
|
|
|
val all = options.getOrNull()?.availableTags.orEmpty()
|
|
|
|
|
val result = ArrayList<MangaTag>(limit)
|
|
|
|
|
result.addAll(suggested.take(limit))
|
|
|
|
|
if (result.size < limit) {
|
|
|
|
|
result.addAll(all.shuffled().take(limit - result.size))
|
|
|
|
|
}
|
|
|
|
|
if (result.isNotEmpty()) {
|
|
|
|
|
Result.success(result)
|
|
|
|
|
} else {
|
|
|
|
|
options.map { result }
|
|
|
|
|
}
|
|
|
|
|
}.catch {
|
|
|
|
|
emit(Result.failure(it))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun <T> List<T>.addFirstDistinct(other: Collection<T>): List<T> {
|
|
|
|
|
val result = ArrayDeque<T>(this.size + other.size)
|
|
|
|
|
|