|
|
|
@ -1,540 +1,393 @@
|
|
|
|
package org.koitharu.kotatsu.filter.ui
|
|
|
|
package org.koitharu.kotatsu.filter.ui
|
|
|
|
|
|
|
|
|
|
|
|
import android.view.View
|
|
|
|
|
|
|
|
import androidx.lifecycle.SavedStateHandle
|
|
|
|
import androidx.lifecycle.SavedStateHandle
|
|
|
|
import dagger.hilt.android.ViewModelLifecycle
|
|
|
|
import dagger.hilt.android.ViewModelLifecycle
|
|
|
|
import dagger.hilt.android.scopes.ViewModelScoped
|
|
|
|
import dagger.hilt.android.scopes.ViewModelScoped
|
|
|
|
import kotlinx.coroutines.CoroutineStart
|
|
|
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
import kotlinx.coroutines.Job
|
|
|
|
|
|
|
|
import kotlinx.coroutines.async
|
|
|
|
|
|
|
|
import kotlinx.coroutines.cancelAndJoin
|
|
|
|
|
|
|
|
import kotlinx.coroutines.flow.Flow
|
|
|
|
import kotlinx.coroutines.flow.Flow
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
|
|
|
|
|
|
import kotlinx.coroutines.flow.combine
|
|
|
|
import kotlinx.coroutines.flow.combine
|
|
|
|
|
|
|
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
|
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
|
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
|
|
|
import kotlinx.coroutines.flow.flow
|
|
|
|
import kotlinx.coroutines.flow.flow
|
|
|
|
import kotlinx.coroutines.flow.flowOf
|
|
|
|
|
|
|
|
import kotlinx.coroutines.flow.map
|
|
|
|
import kotlinx.coroutines.flow.map
|
|
|
|
import kotlinx.coroutines.flow.stateIn
|
|
|
|
import kotlinx.coroutines.flow.stateIn
|
|
|
|
import kotlinx.coroutines.flow.update
|
|
|
|
import kotlinx.coroutines.flow.update
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
|
|
|
import kotlinx.coroutines.plus
|
|
|
|
import kotlinx.coroutines.plus
|
|
|
|
import org.koitharu.kotatsu.R
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.core.model.GenericSortOrder
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.core.model.MangaSource
|
|
|
|
import org.koitharu.kotatsu.core.model.MangaSource
|
|
|
|
import org.koitharu.kotatsu.core.model.SortDirection
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
|
|
|
import org.koitharu.kotatsu.core.ui.model.direction
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.core.util.LocaleComparator
|
|
|
|
import org.koitharu.kotatsu.core.util.LocaleComparator
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.asArrayList
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.asFlow
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
|
|
|
|
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
|
|
|
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
|
|
|
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
|
|
|
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
|
|
|
import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
|
|
|
|
import org.koitharu.kotatsu.filter.ui.tags.TagTitleComparator
|
|
|
|
import org.koitharu.kotatsu.list.ui.model.ErrorFooter
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.list.ui.model.toErrorFooter
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
|
|
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.Demographic
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
|
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
|
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
|
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.parsers.model.YEAR_MIN
|
|
|
|
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
|
|
|
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
|
|
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
|
|
|
|
|
|
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
|
|
|
import org.koitharu.kotatsu.remotelist.ui.RemoteListFragment
|
|
|
|
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
|
|
|
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
|
|
|
import java.text.Collator
|
|
|
|
import java.util.Calendar
|
|
|
|
import java.util.EnumSet
|
|
|
|
|
|
|
|
import java.util.LinkedList
|
|
|
|
|
|
|
|
import java.util.Locale
|
|
|
|
import java.util.Locale
|
|
|
|
import java.util.TreeSet
|
|
|
|
|
|
|
|
import javax.inject.Inject
|
|
|
|
import javax.inject.Inject
|
|
|
|
|
|
|
|
|
|
|
|
@ViewModelScoped
|
|
|
|
@ViewModelScoped
|
|
|
|
class FilterCoordinator @Inject constructor(
|
|
|
|
class FilterCoordinator @Inject constructor(
|
|
|
|
savedStateHandle: SavedStateHandle,
|
|
|
|
savedStateHandle: SavedStateHandle,
|
|
|
|
mangaRepositoryFactory: MangaRepository.Factory,
|
|
|
|
mangaRepositoryFactory: MangaRepository.Factory,
|
|
|
|
dataRepository: MangaDataRepository,
|
|
|
|
|
|
|
|
private val searchRepository: MangaSearchRepository,
|
|
|
|
private val searchRepository: MangaSearchRepository,
|
|
|
|
lifecycle: ViewModelLifecycle,
|
|
|
|
lifecycle: ViewModelLifecycle,
|
|
|
|
) : MangaFilter {
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
|
|
private val coroutineScope = lifecycle.lifecycleScope
|
|
|
|
private val coroutineScope = lifecycle.lifecycleScope + Dispatchers.Default
|
|
|
|
private val repository = mangaRepositoryFactory.create(MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE]))
|
|
|
|
private val repository = mangaRepositoryFactory.create(MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE]))
|
|
|
|
private val currentState = MutableStateFlow(
|
|
|
|
private val sourceLocale = (repository.source as? MangaParserSource)?.locale
|
|
|
|
MangaListFilter.Advanced(
|
|
|
|
|
|
|
|
sortOrder = repository.defaultSortOrder,
|
|
|
|
private val currentListFilter = MutableStateFlow(MangaListFilter.EMPTY)
|
|
|
|
tags = emptySet(),
|
|
|
|
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
|
|
|
|
tagsExclude = emptySet(),
|
|
|
|
|
|
|
|
locale = null,
|
|
|
|
private val availableSortOrders = repository.sortOrders
|
|
|
|
states = emptySet(),
|
|
|
|
private val capabilities = repository.filterCapabilities
|
|
|
|
contentRating = emptySet(),
|
|
|
|
private val filterOptions = SuspendLazy { repository.getFilterOptions() }
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
private val localTags = SuspendLazy {
|
|
|
|
|
|
|
|
dataRepository.findTags(repository.source)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private val tagsFlow = flow {
|
|
|
|
|
|
|
|
val localTags = localTags.get()
|
|
|
|
|
|
|
|
emit(PendingData(localTags, isLoading = true, error = null))
|
|
|
|
|
|
|
|
tryLoadTags()
|
|
|
|
|
|
|
|
.onSuccess { remoteTags ->
|
|
|
|
|
|
|
|
emit(PendingData(mergeTags(remoteTags, localTags), isLoading = false, error = null))
|
|
|
|
|
|
|
|
}.onFailure {
|
|
|
|
|
|
|
|
emit(PendingData(localTags, isLoading = false, error = it))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.WhileSubscribed(5000), PendingData(emptySet(), true, null))
|
|
|
|
|
|
|
|
private var availableTagsDeferred = loadTagsAsync()
|
|
|
|
|
|
|
|
private var availableLocalesDeferred = loadLocalesAsync()
|
|
|
|
|
|
|
|
private var allTagsLoadJob: Job? = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override val allTags = MutableStateFlow<List<ListModel>>(listOf(LoadingState))
|
|
|
|
|
|
|
|
get() {
|
|
|
|
|
|
|
|
if (allTagsLoadJob == null || field.value.any { it is ErrorFooter }) {
|
|
|
|
|
|
|
|
loadAllTags()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return field
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override val filterTags: StateFlow<FilterProperty<MangaTag>> = combine(
|
|
|
|
val mangaSource: MangaSource
|
|
|
|
currentState.distinctUntilChangedBy { it.tags },
|
|
|
|
get() = repository.source
|
|
|
|
getTopTagsAsFlow(currentState.map { it.tags }, 16),
|
|
|
|
|
|
|
|
) { state, tags ->
|
|
|
|
val isFilterApplied: Boolean
|
|
|
|
|
|
|
|
get() = !currentListFilter.value.isEmpty()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val sortOrder: StateFlow<FilterProperty<SortOrder>> = currentSortOrder.map { selected ->
|
|
|
|
FilterProperty(
|
|
|
|
FilterProperty(
|
|
|
|
availableItems = tags.items.asArrayList(),
|
|
|
|
availableItems = availableSortOrders.sortedByOrdinal(),
|
|
|
|
selectedItems = state.tags,
|
|
|
|
selectedItem = selected,
|
|
|
|
isLoading = tags.isLoading,
|
|
|
|
|
|
|
|
error = tags.error,
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
|
|
|
}.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)
|
|
|
|
|
|
|
|
|
|
|
|
override val filterTagsExcluded: StateFlow<FilterProperty<MangaTag>> = if (repository.isTagsExclusionSupported) {
|
|
|
|
val tagsExcluded: StateFlow<FilterProperty<MangaTag>> = if (capabilities.isTagsExclusionSupported) {
|
|
|
|
combine(
|
|
|
|
combine(
|
|
|
|
currentState.distinctUntilChangedBy { it.tagsExclude },
|
|
|
|
getBottomTags(TAGS_LIMIT),
|
|
|
|
getBottomTagsAsFlow(4),
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.tagsExclude },
|
|
|
|
) { state, tags ->
|
|
|
|
) { available, selected ->
|
|
|
|
FilterProperty(
|
|
|
|
available.fold(
|
|
|
|
availableItems = tags.items.asArrayList(),
|
|
|
|
onSuccess = {
|
|
|
|
selectedItems = state.tagsExclude,
|
|
|
|
FilterProperty(
|
|
|
|
isLoading = tags.isLoading,
|
|
|
|
availableItems = it.addFirstDistinct(selected.tagsExclude),
|
|
|
|
error = tags.error,
|
|
|
|
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 + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
MutableStateFlow(emptyProperty())
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override val filterSortOrder: StateFlow<FilterProperty<GenericSortOrder>> =
|
|
|
|
val year: StateFlow<FilterProperty<Int>> = if (capabilities.isYearSupported) {
|
|
|
|
currentState.distinctUntilChangedBy { it.sortOrder }.map { state ->
|
|
|
|
currentListFilter.distinctUntilChangedBy { it.year }.map { selected ->
|
|
|
|
val orders = repository.sortOrders
|
|
|
|
|
|
|
|
FilterProperty(
|
|
|
|
FilterProperty(
|
|
|
|
availableItems = orders.mapTo(EnumSet.noneOf(GenericSortOrder::class.java)) {
|
|
|
|
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
|
|
|
GenericSortOrder.of(it)
|
|
|
|
selectedItems = setOf(selected.year),
|
|
|
|
}.sortedByOrdinal(),
|
|
|
|
|
|
|
|
selectedItems = setOf(GenericSortOrder.of(state.sortOrder)),
|
|
|
|
|
|
|
|
isLoading = false,
|
|
|
|
|
|
|
|
error = null,
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override val filterSortDirection: StateFlow<FilterProperty<SortDirection>> =
|
|
|
|
val yearRange: StateFlow<FilterProperty<Int>> = if (capabilities.isYearRangeSupported) {
|
|
|
|
currentState.distinctUntilChangedBy { it.sortOrder }.map { state ->
|
|
|
|
currentListFilter.distinctUntilChanged { old, new ->
|
|
|
|
val orders = repository.sortOrders
|
|
|
|
old.yearTo == new.yearTo && old.yearFrom == new.yearFrom
|
|
|
|
|
|
|
|
}.map { selected ->
|
|
|
|
FilterProperty(
|
|
|
|
FilterProperty(
|
|
|
|
availableItems = state.sortOrder.let {
|
|
|
|
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
|
|
|
val genericOrder = GenericSortOrder.of(it)
|
|
|
|
selectedItems = setOf(selected.yearFrom, selected.yearTo),
|
|
|
|
val result = EnumSet.noneOf(SortDirection::class.java)
|
|
|
|
|
|
|
|
if (genericOrder.ascending in orders) result.add(SortDirection.ASC)
|
|
|
|
|
|
|
|
if (genericOrder.descending in orders) result.add(SortDirection.DESC)
|
|
|
|
|
|
|
|
result
|
|
|
|
|
|
|
|
}?.sortedByOrdinal().orEmpty(),
|
|
|
|
|
|
|
|
selectedItems = setOf(state.sortOrder.direction),
|
|
|
|
|
|
|
|
isLoading = false,
|
|
|
|
|
|
|
|
error = null,
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
|
|
|
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
MutableStateFlow(FilterProperty.EMPTY)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override val filterState: StateFlow<FilterProperty<MangaState>> = combine(
|
|
|
|
fun reset() {
|
|
|
|
currentState.distinctUntilChangedBy { it.states },
|
|
|
|
currentListFilter.value = MangaListFilter.EMPTY
|
|
|
|
flowOf(repository.states),
|
|
|
|
}
|
|
|
|
) { state, states ->
|
|
|
|
|
|
|
|
FilterProperty(
|
|
|
|
|
|
|
|
availableItems = states.sortedByOrdinal(),
|
|
|
|
|
|
|
|
selectedItems = state.states,
|
|
|
|
|
|
|
|
isLoading = false,
|
|
|
|
|
|
|
|
error = null,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override val filterContentRating: StateFlow<FilterProperty<ContentRating>> = combine(
|
|
|
|
fun snapshot() = Snapshot(
|
|
|
|
currentState.distinctUntilChangedBy { it.contentRating },
|
|
|
|
sortOrder = currentSortOrder.value,
|
|
|
|
flowOf(repository.contentRatings),
|
|
|
|
listFilter = currentListFilter.value,
|
|
|
|
) { rating, ratings ->
|
|
|
|
|
|
|
|
FilterProperty(
|
|
|
|
|
|
|
|
availableItems = ratings.sortedByOrdinal(),
|
|
|
|
|
|
|
|
selectedItems = rating.contentRating,
|
|
|
|
|
|
|
|
isLoading = false,
|
|
|
|
|
|
|
|
error = null,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override val filterLocale: StateFlow<FilterProperty<Locale?>> = combine(
|
|
|
|
|
|
|
|
currentState.distinctUntilChangedBy { it.locale },
|
|
|
|
|
|
|
|
getLocalesAsFlow(),
|
|
|
|
|
|
|
|
) { state, locales ->
|
|
|
|
|
|
|
|
val list = if (locales.items.isNotEmpty()) {
|
|
|
|
|
|
|
|
val l = ArrayList<Locale?>(locales.items.size + 1)
|
|
|
|
|
|
|
|
l.add(null)
|
|
|
|
|
|
|
|
l.addAll(locales.items)
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
l.sortWith(nullsFirst(LocaleComparator()))
|
|
|
|
|
|
|
|
} catch (e: IllegalArgumentException) {
|
|
|
|
|
|
|
|
e.printStackTraceDebug()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
l
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
emptyList()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
FilterProperty(
|
|
|
|
|
|
|
|
availableItems = list,
|
|
|
|
|
|
|
|
selectedItems = setOf(state.locale),
|
|
|
|
|
|
|
|
isLoading = locales.isLoading,
|
|
|
|
|
|
|
|
error = locales.error,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override val header: StateFlow<FilterHeaderModel> = getHeaderFlow().stateIn(
|
|
|
|
|
|
|
|
scope = coroutineScope + Dispatchers.Default,
|
|
|
|
|
|
|
|
started = SharingStarted.Lazily,
|
|
|
|
|
|
|
|
initialValue = FilterHeaderModel(
|
|
|
|
|
|
|
|
chips = emptyList(),
|
|
|
|
|
|
|
|
sortOrder = repository.defaultSortOrder,
|
|
|
|
|
|
|
|
isFilterApplied = false,
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
override fun applyFilter(tags: Set<MangaTag>) {
|
|
|
|
fun observe(): Flow<Snapshot> = combine(currentSortOrder, currentListFilter, ::Snapshot)
|
|
|
|
setTags(tags)
|
|
|
|
|
|
|
|
|
|
|
|
fun setSortOrder(newSortOrder: SortOrder) {
|
|
|
|
|
|
|
|
currentSortOrder.value = newSortOrder
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override fun setSortOrder(value: SortOrder) {
|
|
|
|
fun set(value: MangaListFilter) {
|
|
|
|
val available = repository.sortOrders
|
|
|
|
currentListFilter.value = value
|
|
|
|
val sortOrder = if (value !in available) {
|
|
|
|
|
|
|
|
val generic = GenericSortOrder.of(value)
|
|
|
|
|
|
|
|
when {
|
|
|
|
|
|
|
|
generic.ascending in available -> generic.ascending
|
|
|
|
|
|
|
|
generic.descending in available -> generic.descending
|
|
|
|
|
|
|
|
else -> return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
value
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
currentState.update { oldValue ->
|
|
|
|
|
|
|
|
oldValue.copy(sortOrder = sortOrder)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
repository.defaultSortOrder = sortOrder
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override fun setLanguage(value: Locale?) {
|
|
|
|
fun setLocale(value: Locale?) {
|
|
|
|
currentState.update { oldValue ->
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
oldValue.copy(locale = value)
|
|
|
|
oldValue.copy(locale = value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override fun setTag(value: MangaTag, addOrRemove: Boolean) {
|
|
|
|
fun setYear(value: Int) {
|
|
|
|
currentState.update { oldValue ->
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
val newTags = if (repository.isMultipleTagsSupported) {
|
|
|
|
oldValue.copy(year = value)
|
|
|
|
if (addOrRemove) {
|
|
|
|
|
|
|
|
oldValue.tags + value
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
oldValue.tags - value
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if (addOrRemove) {
|
|
|
|
|
|
|
|
setOf(value)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
emptySet()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
oldValue.copy(
|
|
|
|
|
|
|
|
tags = newTags,
|
|
|
|
|
|
|
|
tagsExclude = oldValue.tagsExclude - newTags,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override fun setTagExcluded(value: MangaTag, addOrRemove: Boolean) {
|
|
|
|
fun toggleState(value: MangaState, isSelected: Boolean) {
|
|
|
|
currentState.update { oldValue ->
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
val newTags = if (repository.isMultipleTagsSupported) {
|
|
|
|
|
|
|
|
if (addOrRemove) {
|
|
|
|
|
|
|
|
oldValue.tagsExclude + value
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
oldValue.tagsExclude - value
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if (addOrRemove) {
|
|
|
|
|
|
|
|
setOf(value)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
emptySet()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
oldValue.copy(
|
|
|
|
oldValue.copy(
|
|
|
|
tagsExclude = newTags,
|
|
|
|
states = if (isSelected) oldValue.states + value else oldValue.states - value,
|
|
|
|
tags = oldValue.tags - newTags,
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override fun setState(value: MangaState, addOrRemove: Boolean) {
|
|
|
|
fun toggleContentRating(value: ContentRating, isSelected: Boolean) {
|
|
|
|
currentState.update { oldValue ->
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
val newStates = if (addOrRemove) {
|
|
|
|
oldValue.copy(
|
|
|
|
oldValue.states + value
|
|
|
|
contentRating = if (isSelected) oldValue.contentRating + value else oldValue.contentRating - value,
|
|
|
|
} else {
|
|
|
|
)
|
|
|
|
oldValue.states - value
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
oldValue.copy(states = newStates)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
override fun setContentRating(value: ContentRating, addOrRemove: Boolean) {
|
|
|
|
fun toggleTag(value: MangaTag, isSelected: Boolean) {
|
|
|
|
currentState.update { oldValue ->
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
val newRating = if (addOrRemove) {
|
|
|
|
val newTags = if (capabilities.isMultipleTagsSupported) {
|
|
|
|
oldValue.contentRating + value
|
|
|
|
if (isSelected) oldValue.tags + value else oldValue.tags - value
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
oldValue.contentRating - value
|
|
|
|
if (isSelected) setOf(value) else emptySet()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
oldValue.copy(contentRating = newRating)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override fun onListHeaderClick(item: ListHeader, view: View) {
|
|
|
|
|
|
|
|
currentState.update { oldValue ->
|
|
|
|
|
|
|
|
oldValue.copy(
|
|
|
|
oldValue.copy(
|
|
|
|
sortOrder = oldValue.sortOrder,
|
|
|
|
tags = newTags,
|
|
|
|
tags = if (item.payload == R.string.genres) emptySet() else oldValue.tags,
|
|
|
|
tagsExclude = oldValue.tagsExclude - newTags,
|
|
|
|
locale = if (item.payload == R.string.language) null else oldValue.locale,
|
|
|
|
|
|
|
|
states = if (item.payload == R.string.state) emptySet() else oldValue.states,
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fun observeAvailableTags(): Flow<Set<MangaTag>?> = flow {
|
|
|
|
fun toggleTagExclude(value: MangaTag, isSelected: Boolean) {
|
|
|
|
if (!availableTagsDeferred.isCompleted) {
|
|
|
|
currentListFilter.update { oldValue ->
|
|
|
|
emit(emptySet())
|
|
|
|
val newTagsExclude = if (capabilities.isMultipleTagsSupported) {
|
|
|
|
}
|
|
|
|
if (isSelected) oldValue.tagsExclude + value else oldValue.tagsExclude - value
|
|
|
|
emit(availableTagsDeferred.await().getOrNull())
|
|
|
|
} else {
|
|
|
|
}
|
|
|
|
if (isSelected) setOf(value) else emptySet()
|
|
|
|
|
|
|
|
}
|
|
|
|
fun observeState() = currentState.asStateFlow()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fun setTags(tags: Set<MangaTag>) {
|
|
|
|
|
|
|
|
currentState.update { oldValue ->
|
|
|
|
|
|
|
|
oldValue.copy(
|
|
|
|
oldValue.copy(
|
|
|
|
tags = tags,
|
|
|
|
tags = oldValue.tags - newTagsExclude,
|
|
|
|
tagsExclude = oldValue.tagsExclude - tags,
|
|
|
|
tagsExclude = newTagsExclude,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fun reset() {
|
|
|
|
fun getAllTags(): Flow<Result<List<MangaTag>>> = filterOptions.asFlow().map {
|
|
|
|
currentState.update { oldValue ->
|
|
|
|
it.map { x -> x.availableTags.sortedWithSafe(TagTitleComparator(sourceLocale)) }
|
|
|
|
MangaListFilter.Advanced.Builder(oldValue.sortOrder).build()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fun snapshot() = currentState.value
|
|
|
|
private fun getTopTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
|
|
|
|
|
|
|
flow { emit(searchRepository.getTopTags(repository.source, limit)) },
|
|
|
|
private fun getHeaderFlow() = combine(
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
observeState(),
|
|
|
|
) { suggested, options ->
|
|
|
|
observeAvailableTags(),
|
|
|
|
val all = options.getOrNull()?.availableTags.orEmpty()
|
|
|
|
) { state, available ->
|
|
|
|
val result = ArrayList<MangaTag>(limit)
|
|
|
|
val chips = createChipsList(state, available.orEmpty(), 8)
|
|
|
|
result.addAll(suggested.take(limit))
|
|
|
|
FilterHeaderModel(
|
|
|
|
if (result.size < limit) {
|
|
|
|
chips = chips,
|
|
|
|
result.addAll(all.shuffled().take(limit - result.size))
|
|
|
|
sortOrder = state.sortOrder,
|
|
|
|
}
|
|
|
|
isFilterApplied = !state.isEmpty(),
|
|
|
|
if (result.isNotEmpty()) {
|
|
|
|
)
|
|
|
|
Result.success(result)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
|
|
|
|
options.map { result }
|
|
|
|
private fun getLocalesAsFlow(): Flow<PendingData<Locale>> = flow {
|
|
|
|
|
|
|
|
emit(PendingData(emptySet(), isLoading = true, error = null))
|
|
|
|
|
|
|
|
tryLoadLocales()
|
|
|
|
|
|
|
|
.onSuccess { locales ->
|
|
|
|
|
|
|
|
emit(PendingData(locales, isLoading = false, error = null))
|
|
|
|
|
|
|
|
}.onFailure {
|
|
|
|
|
|
|
|
emit(PendingData(emptySet(), isLoading = false, error = it))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun getTopTagsAsFlow(selectedTags: Flow<Set<MangaTag>>, limit: Int): Flow<PendingData<MangaTag>> = combine(
|
|
|
|
|
|
|
|
selectedTags.map {
|
|
|
|
|
|
|
|
if (it.isEmpty()) {
|
|
|
|
|
|
|
|
searchRepository.getTagsSuggestion("", limit, repository.source)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
searchRepository.getTagsSuggestion(it).take(limit)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
tagsFlow,
|
|
|
|
|
|
|
|
) { suggested, all ->
|
|
|
|
|
|
|
|
val res = suggested.toMutableList()
|
|
|
|
|
|
|
|
if (res.size < limit) {
|
|
|
|
|
|
|
|
res.addAll(all.items.shuffled().take(limit - res.size))
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
PendingData(res, all.isLoading, all.error.takeIf { res.size < limit })
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun getBottomTagsAsFlow(limit: Int): Flow<PendingData<MangaTag>> = combine(
|
|
|
|
private fun getBottomTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
|
|
|
flow { emit(searchRepository.getRareTags(repository.source, limit)) },
|
|
|
|
flow { emit(searchRepository.getRareTags(repository.source, limit)) },
|
|
|
|
tagsFlow,
|
|
|
|
filterOptions.asFlow(),
|
|
|
|
) { suggested, all ->
|
|
|
|
) { suggested, options ->
|
|
|
|
val res = suggested.toMutableList()
|
|
|
|
val all = options.getOrNull()?.availableTags.orEmpty()
|
|
|
|
if (res.size < limit) {
|
|
|
|
val result = ArrayList<MangaTag>(limit)
|
|
|
|
res.addAll(all.items.shuffled().take(limit - res.size))
|
|
|
|
result.addAll(suggested.take(limit))
|
|
|
|
|
|
|
|
if (result.size < limit) {
|
|
|
|
|
|
|
|
result.addAll(all.shuffled().take(limit - result.size))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
PendingData(res, all.isLoading, all.error.takeIf { res.size < limit })
|
|
|
|
if (result.isNotEmpty()) {
|
|
|
|
}
|
|
|
|
Result.success(result)
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun createChipsList(
|
|
|
|
|
|
|
|
filterState: MangaListFilter.Advanced,
|
|
|
|
|
|
|
|
availableTags: Set<MangaTag>,
|
|
|
|
|
|
|
|
limit: Int,
|
|
|
|
|
|
|
|
): List<ChipsView.ChipModel> {
|
|
|
|
|
|
|
|
val selectedTags = filterState.tags.toMutableSet()
|
|
|
|
|
|
|
|
var tags = if (selectedTags.isEmpty()) {
|
|
|
|
|
|
|
|
searchRepository.getTagsSuggestion("", limit, repository.source)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
searchRepository.getTagsSuggestion(selectedTags).take(limit)
|
|
|
|
options.map { result }
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tags.size < limit) {
|
|
|
|
|
|
|
|
tags = tags + availableTags.take(limit - tags.size)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tags.isEmpty() && selectedTags.isEmpty()) {
|
|
|
|
|
|
|
|
return emptyList()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
val result = LinkedList<ChipsView.ChipModel>()
|
|
|
|
|
|
|
|
for (tag in tags) {
|
|
|
|
|
|
|
|
val model = ChipsView.ChipModel(
|
|
|
|
|
|
|
|
title = tag.title,
|
|
|
|
|
|
|
|
isChecked = selectedTags.remove(tag),
|
|
|
|
|
|
|
|
data = tag,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if (model.isChecked) {
|
|
|
|
|
|
|
|
result.addFirst(model)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
result.addLast(model)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (tag in selectedTags) {
|
|
|
|
|
|
|
|
val model = ChipsView.ChipModel(
|
|
|
|
|
|
|
|
title = tag.title,
|
|
|
|
|
|
|
|
isChecked = true,
|
|
|
|
|
|
|
|
data = tag,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
result.addFirst(model)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun tryLoadTags(): Result<Set<MangaTag>> {
|
|
|
|
private fun <T> List<T>.addFirstDistinct(other: Collection<T>): List<T> {
|
|
|
|
val shouldRetryOnError = availableTagsDeferred.isCompleted
|
|
|
|
val result = ArrayDeque<T>(this.size + other.size)
|
|
|
|
val result = availableTagsDeferred.await()
|
|
|
|
result.addAll(this)
|
|
|
|
if (result.isFailure && shouldRetryOnError) {
|
|
|
|
for (item in other) {
|
|
|
|
availableTagsDeferred = loadTagsAsync()
|
|
|
|
if (item !in result) {
|
|
|
|
return availableTagsDeferred.await()
|
|
|
|
result.addFirst(item)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private suspend fun tryLoadLocales(): Result<Set<Locale>> {
|
|
|
|
private fun <T> List<T>.addFirstDistinct(item: T): List<T> {
|
|
|
|
val shouldRetryOnError = availableLocalesDeferred.isCompleted
|
|
|
|
val result = ArrayDeque<T>(this.size + 1)
|
|
|
|
val result = availableLocalesDeferred.await()
|
|
|
|
result.addAll(this)
|
|
|
|
if (result.isFailure && shouldRetryOnError) {
|
|
|
|
if (item !in result) {
|
|
|
|
availableLocalesDeferred = loadLocalesAsync()
|
|
|
|
result.addFirst(item)
|
|
|
|
return availableLocalesDeferred.await()
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
|
|
|
|
data class Snapshot(
|
|
|
|
runCatchingCancellable {
|
|
|
|
val sortOrder: SortOrder,
|
|
|
|
repository.getTags()
|
|
|
|
val listFilter: MangaListFilter,
|
|
|
|
}.onFailure { error ->
|
|
|
|
|
|
|
|
error.printStackTraceDebug()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun loadLocalesAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
|
|
|
|
|
|
|
|
runCatchingCancellable {
|
|
|
|
|
|
|
|
repository.getLocales()
|
|
|
|
|
|
|
|
}.onFailure { error ->
|
|
|
|
|
|
|
|
error.printStackTraceDebug()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun mergeTags(primary: Set<MangaTag>, secondary: Set<MangaTag>): Set<MangaTag> {
|
|
|
|
|
|
|
|
val result = TreeSet(TagTitleComparator((repository.source as? MangaParserSource)?.locale))
|
|
|
|
|
|
|
|
result.addAll(secondary)
|
|
|
|
|
|
|
|
result.addAll(primary)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun loadAllTags() {
|
|
|
|
|
|
|
|
val prevJob = allTagsLoadJob
|
|
|
|
|
|
|
|
allTagsLoadJob = coroutineScope.launch(Dispatchers.Default) {
|
|
|
|
|
|
|
|
runCatchingCancellable {
|
|
|
|
|
|
|
|
prevJob?.cancelAndJoin()
|
|
|
|
|
|
|
|
appendTagsList(localTags.get(), isLoading = true)
|
|
|
|
|
|
|
|
appendTagsList(availableTagsDeferred.await().getOrThrow(), isLoading = false)
|
|
|
|
|
|
|
|
}.onFailure { e ->
|
|
|
|
|
|
|
|
allTags.value = allTags.value.filterIsInstance<TagCatalogItem>() + e.toErrorFooter()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun appendTagsList(newTags: Collection<MangaTag>, isLoading: Boolean) = allTags.update { oldList ->
|
|
|
|
|
|
|
|
val oldTags = oldList.filterIsInstance<TagCatalogItem>()
|
|
|
|
|
|
|
|
buildList(oldTags.size + newTags.size + if (isLoading) 1 else 0) {
|
|
|
|
|
|
|
|
addAll(oldTags)
|
|
|
|
|
|
|
|
newTags.mapTo(this) { TagCatalogItem(it, isChecked = false) }
|
|
|
|
|
|
|
|
val tempSet = HashSet<MangaTag>(size)
|
|
|
|
|
|
|
|
removeAll { x -> x is TagCatalogItem && !tempSet.add(x.tag) }
|
|
|
|
|
|
|
|
sortBy { (it as TagCatalogItem).tag.title }
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
|
|
|
add(LoadingFooter())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private data class PendingData<T>(
|
|
|
|
|
|
|
|
val items: Collection<T>,
|
|
|
|
|
|
|
|
val isLoading: Boolean,
|
|
|
|
|
|
|
|
val error: Throwable?,
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
private fun <T> loadingProperty() = FilterProperty<T>(emptyList(), emptySet(), true, null)
|
|
|
|
interface Owner {
|
|
|
|
|
|
|
|
|
|
|
|
private fun <T> emptyProperty() = FilterProperty<T>(emptyList(), emptySet(), false, null)
|
|
|
|
val filterCoordinator: FilterCoordinator
|
|
|
|
|
|
|
|
}
|
|
|
|
private class TagTitleComparator(lc: String?) : Comparator<MangaTag> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private val collator = lc?.let { Collator.getInstance(Locale(it)) }
|
|
|
|
private companion object {
|
|
|
|
|
|
|
|
|
|
|
|
override fun compare(o1: MangaTag, o2: MangaTag): Int {
|
|
|
|
const val TAGS_LIMIT = 12
|
|
|
|
val t1 = o1.title.lowercase()
|
|
|
|
val MAX_YEAR = Calendar.getInstance()[Calendar.YEAR] + 1
|
|
|
|
val t2 = o2.title.lowercase()
|
|
|
|
|
|
|
|
return collator?.compare(t1, t2) ?: compareValues(t1, t2)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|