From d3f23ea3a3e9c9b96ede1a6c62ad6062a5f66d4c Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 25 Nov 2023 17:25:48 +0200 Subject: [PATCH] Add manga state to filter --- app/build.gradle | 6 +- .../org/koitharu/kotatsu/core/model/Manga.kt | 12 +++ .../core/network/MirrorSwitchInterceptor.kt | 3 + .../kotatsu/core/parser/MangaLinkResolver.kt | 5 +- .../kotatsu/core/parser/MangaRepository.kt | 8 +- .../core/parser/RemoteMangaRepository.kt | 24 +++--- .../explore/domain/ExploreRepository.kt | 11 ++- .../explore/domain/RecoverMangaUseCase.kt | 3 +- .../kotatsu/filter/ui/FilterAdapter.kt | 2 + .../filter/ui/FilterAdapterDelegates.kt | 35 ++++++++ .../kotatsu/filter/ui/FilterCoordinator.kt | 83 +++++++++++++++---- .../kotatsu/filter/ui/FilterHeaderFragment.kt | 2 +- .../filter/ui/OnFilterChangedListener.kt | 2 + .../filter/ui/model/FilterHeaderModel.kt | 3 + .../kotatsu/filter/ui/model/FilterItem.kt | 22 ++++- .../kotatsu/filter/ui/model/FilterState.kt | 9 -- .../kotatsu/list/ui/adapter/ListItemType.kt | 2 + .../ui/adapter/TypedListSpacingDecoration.kt | 2 + .../list/ui/preview/PreviewFragment.kt | 2 +- .../local/data/LocalMangaRepository.kt | 46 +++++----- .../local/domain/DeleteLocalMangaUseCase.kt | 4 +- .../remotelist/ui/RemoteListViewModel.kt | 7 +- .../kotatsu/search/ui/SearchViewModel.kt | 3 +- .../search/ui/multi/MultiSearchViewModel.kt | 3 +- .../suggestions/ui/SuggestionsWorker.kt | 11 ++- app/src/main/res/values/strings.xml | 1 + 26 files changed, 233 insertions(+), 78 deletions(-) delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterState.kt diff --git a/app/build.gradle b/app/build.gradle index c9c8e1761..bd2ddbd98 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId 'org.koitharu.kotatsu' minSdk = 21 targetSdk = 34 - versionCode = 599 - versionName = '6.4-a1' + versionCode = 600 + versionName = '6.4' generatedDensities = [] testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner" ksp { @@ -82,7 +82,7 @@ afterEvaluate { } dependencies { //noinspection GradleDependency - implementation('com.github.KotatsuApp:kotatsu-parsers:c3613f3ba4') { + implementation('com.github.KotatsuApp:kotatsu-parsers:46e863ef79') { exclude group: 'org.json', module: 'json' } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt index 2d5d6d2aa..eea9ae360 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/model/Manga.kt @@ -1,12 +1,15 @@ package org.koitharu.kotatsu.core.model import android.net.Uri +import androidx.annotation.StringRes import androidx.core.os.LocaleListCompat +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.util.ext.iterator import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.util.mapToSet @JvmName("mangaIds") @@ -31,6 +34,15 @@ fun Collection.countChaptersByBranch(): Int { return acc.values.max() } +@get:StringRes +val MangaState.titleResId: Int + get() = when (this) { + MangaState.ONGOING -> R.string.state_ongoing + MangaState.FINISHED -> R.string.state_finished + MangaState.ABANDONED -> R.string.state_abandoned + MangaState.PAUSED -> R.string.state_paused + } + fun Manga.findChapter(id: Long): MangaChapter? { return chapters?.findById(id) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt index 63f70cf61..efdc9564c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/MirrorSwitchInterceptor.kt @@ -63,6 +63,9 @@ class MirrorSwitchInterceptor @Inject constructor( } synchronized(obtainLock(repository.source)) { val currentMirror = repository.domain + if (currentMirror !in mirrors) { + return@synchronized false + } addToBlacklist(repository.source, currentMirror) val newMirror = mirrors.firstOrNull { x -> x != currentMirror && !isBlacklisted(repository.source, x) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt index 08a14b6cf..d0b1efe1a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaLinkResolver.kt @@ -8,6 +8,7 @@ import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.almostEquals import org.koitharu.kotatsu.parsers.util.levenshteinDistance @@ -58,7 +59,7 @@ class MangaLinkResolver @Inject constructor( private suspend fun MangaRepository.findExact(url: String?, title: String?): Manga? { if (!title.isNullOrEmpty()) { - val list = getList(0, title) + val list = getList(0, MangaListFilter.Search(title)) if (url != null) { list.find { it.url == url }?.let { return it @@ -77,7 +78,7 @@ class MangaLinkResolver @Inject constructor( }.ifNullOrEmpty { seed.author } ?: return@runCatchingCancellable null - val seedList = getList(0, seedTitle) + val seedList = getList(0, MangaListFilter.Search(seedTitle)) seedList.first { x -> x.url == url } }.getOrThrow() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt index 7335ff5c1..8f66ff6be 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/MangaRepository.kt @@ -7,8 +7,10 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import java.lang.ref.WeakReference @@ -23,11 +25,13 @@ interface MangaRepository { val sortOrders: Set + val states: Set + var defaultSortOrder: SortOrder - suspend fun getList(offset: Int, query: String): List + val isMultipleTagsSupported: Boolean - suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List + suspend fun getList(offset: Int, filter: MangaListFilter?): List suspend fun getDetails(manga: Manga): Manga diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index 1a66dc4e6..18ab51fb7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -23,8 +23,10 @@ import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.model.Favicons import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.domain @@ -40,7 +42,10 @@ class RemoteMangaRepository( get() = parser.source override val sortOrders: Set - get() = parser.sortOrders + get() = parser.availableSortOrders + + override val states: Set + get() = parser.availableStates override var defaultSortOrder: SortOrder get() = getConfig().defaultSortOrder ?: sortOrders.first() @@ -48,6 +53,9 @@ class RemoteMangaRepository( getConfig().defaultSortOrder = value } + override val isMultipleTagsSupported: Boolean + get() = parser.isMultipleTagsSupported + var domain: String get() = parser.domain set(value) { @@ -68,15 +76,9 @@ class RemoteMangaRepository( } } - override suspend fun getList(offset: Int, query: String): List { + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { return mirrorSwitchInterceptor.withMirrorSwitching { - parser.getList(offset, query) - } - } - - override suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List { - return mirrorSwitchInterceptor.withMirrorSwitching { - parser.getList(offset, tags, sortOrder) + parser.getList(offset, filter) } } @@ -98,7 +100,7 @@ class RemoteMangaRepository( } override suspend fun getTags(): Set = mirrorSwitchInterceptor.withMirrorSwitching { - parser.getTags() + parser.getAvailableTags() } suspend fun getFavicons(): Favicons = mirrorSwitchInterceptor.withMirrorSwitching { @@ -133,7 +135,7 @@ class RemoteMangaRepository( } suspend fun find(manga: Manga): Manga? { - val list = getList(0, manga.title) + val list = getList(0, MangaListFilter.Search(manga.title)) return list.find { x -> x.id == manga.id } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt index c351e80cf..ce30bcd85 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/ExploreRepository.kt @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist @@ -73,7 +74,15 @@ class ExploreRepository @Inject constructor( val tag = tags.firstNotNullOfOrNull { title -> availableTags.find { x -> x.title.almostEquals(title, 0.4f) } } - val list = repository.getList(0, setOfNotNull(tag), order).asArrayList() + val list = repository.getList( + offset = 0, + filter = MangaListFilter.Advanced( + sortOrder = order, + tags = setOfNotNull(tag), + locale = null, + states = emptySet(), + ), + ).asArrayList() if (settings.isSuggestionsExcludeNsfw) { list.removeAll { it.isNsfw } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/RecoverMangaUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/RecoverMangaUseCase.kt index 50aecfc4e..fbdb0f045 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/RecoverMangaUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/explore/domain/RecoverMangaUseCase.kt @@ -5,6 +5,7 @@ import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import javax.inject.Inject @@ -18,7 +19,7 @@ class RecoverMangaUseCase @Inject constructor( return@runCatchingCancellable null } val repository = repositoryFactory.create(manga.source) - val list = repository.getList(offset = 0, query = manga.title) + val list = repository.getList(offset = 0, filter = MangaListFilter.Search(manga.title)) val newManga = list.find { x -> x.title == manga.title }?.let { repository.getDetails(it) } ?: return@runCatchingCancellable null diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt index 6c6b2387f..67280aaa2 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapter.kt @@ -19,6 +19,8 @@ class FilterAdapter( init { addDelegate(ListItemType.FILTER_SORT, filterSortDelegate(listener)) addDelegate(ListItemType.FILTER_TAG, filterTagDelegate(listener)) + addDelegate(ListItemType.FILTER_TAG_MULTI, filterTagMultipleDelegate(listener)) + addDelegate(ListItemType.FILTER_STATE, filterStateDelegate(listener)) addDelegate(ListItemType.HEADER, listHeaderAD(listener)) addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt index c2125070c..5edcf13e8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterAdapterDelegates.kt @@ -4,6 +4,7 @@ import android.widget.TextView import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.titleResId import org.koitharu.kotatsu.core.ui.model.titleRes import org.koitharu.kotatsu.core.util.ext.setChecked import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding @@ -27,10 +28,44 @@ fun filterSortDelegate( } } +fun filterStateDelegate( + listener: OnFilterChangedListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, +) { + + itemView.setOnClickListener { + listener.onStateItemClick(item) + } + + bind { payloads -> + binding.root.setText(item.state.titleResId) + binding.root.setChecked(item.isChecked, payloads.isNotEmpty()) + } +} + fun filterTagDelegate( listener: OnFilterChangedListener, +) = adapterDelegateViewBinding( + { layoutInflater, parent -> ItemCheckableSingleBinding.inflate(layoutInflater, parent, false) }, + on = { item, _, _ -> item is FilterItem.Tag && !item.isMultiple }, +) { + + itemView.setOnClickListener { + listener.onTagItemClick(item) + } + + bind { payloads -> + binding.root.text = item.tag.title + binding.root.setChecked(item.isChecked, payloads.isNotEmpty()) + } +} + +fun filterTagMultipleDelegate( + listener: OnFilterChangedListener, ) = adapterDelegateViewBinding( { layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, + on = { item, _, _ -> item is FilterItem.Tag && item.isMultiple }, ) { itemView.setOnClickListener { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index 7c04099ab..976466180 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -28,11 +28,11 @@ import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel import org.koitharu.kotatsu.filter.ui.model.FilterItem -import org.koitharu.kotatsu.filter.ui.model.FilterState 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.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.util.SuspendLazy import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -55,7 +55,8 @@ class FilterCoordinator @Inject constructor( private val coroutineScope = lifecycle.lifecycleScope private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE)) - private val currentState = MutableStateFlow(FilterState(repository.defaultSortOrder, emptySet())) + private val currentState = + MutableStateFlow(MangaListFilter.Advanced(repository.defaultSortOrder, emptySet(), null, emptySet())) private var searchQuery = MutableStateFlow("") private val localTags = SuspendLazy { dataRepository.findTags(repository.source) @@ -68,7 +69,12 @@ class FilterCoordinator @Inject constructor( override val header: StateFlow = getHeaderFlow().stateIn( scope = coroutineScope + Dispatchers.Default, started = SharingStarted.Lazily, - initialValue = FilterHeaderModel(emptyList(), repository.defaultSortOrder, false), + initialValue = FilterHeaderModel( + chips = emptyList(), + sortOrder = repository.defaultSortOrder, + hasSelectedTags = false, + allowMultipleTags = repository.isMultipleTagsSupported, + ), ) init { @@ -81,24 +87,44 @@ class FilterCoordinator @Inject constructor( override fun onSortItemClick(item: FilterItem.Sort) { currentState.update { oldValue -> - FilterState(item.order, oldValue.tags) + oldValue.copy(sortOrder = item.order) } repository.defaultSortOrder = item.order } override fun onTagItemClick(item: FilterItem.Tag) { currentState.update { oldValue -> - val newTags = if (item.isChecked) { + val newTags = if (!item.isMultiple) { + setOf(item.tag) + } else if (item.isChecked) { oldValue.tags - item.tag } else { oldValue.tags + item.tag } - FilterState(oldValue.sortOrder, newTags) + oldValue.copy(tags = newTags) + } + } + + override fun onStateItemClick(item: FilterItem.State) { + currentState.update { oldValue -> + val newStates = if (item.isChecked) { + oldValue.states - item.state + } else { + oldValue.states + item.state + } + oldValue.copy(states = newStates) } } override fun onListHeaderClick(item: ListHeader, view: View) { - reset() + currentState.update { oldValue -> + oldValue.copy( + sortOrder = oldValue.sortOrder, + tags = if (item.payload == R.string.genres) emptySet() else oldValue.tags, + locale = null, + states = if (item.payload == R.string.state) emptySet() else oldValue.states, + ) + } } fun observeAvailableTags(): Flow?> = flow { @@ -112,13 +138,13 @@ class FilterCoordinator @Inject constructor( fun setTags(tags: Set) { currentState.update { oldValue -> - FilterState(oldValue.sortOrder, tags) + oldValue.copy(tags = tags) } } fun reset() { currentState.update { oldValue -> - FilterState(oldValue.sortOrder, emptySet()) + oldValue.copy(oldValue.sortOrder, emptySet(), null, emptySet()) } } @@ -133,7 +159,12 @@ class FilterCoordinator @Inject constructor( observeAvailableTags(), ) { state, available -> val chips = createChipsList(state, available.orEmpty(), 8) - FilterHeaderModel(chips, state.sortOrder, state.tags.isNotEmpty()) + FilterHeaderModel( + chips = chips, + sortOrder = state.sortOrder, + hasSelectedTags = state.tags.isNotEmpty(), + allowMultipleTags = repository.isMultipleTagsSupported, + ) } private fun getItemsFlow() = combine( @@ -156,7 +187,7 @@ class FilterCoordinator @Inject constructor( } private suspend fun createChipsList( - filterState: FilterState, + filterState: MangaListFilter.Advanced, availableTags: Set, limit: Int, ): List { @@ -205,12 +236,14 @@ class FilterCoordinator @Inject constructor( @WorkerThread private fun buildFilterList( allTags: TagsWrapper, - state: FilterState, + state: MangaListFilter.Advanced, query: String, ): List { val sortOrders = repository.sortOrders.sortedByOrdinal() + val states = repository.states val tags = mergeTags(state.tags, allTags.tags).toList() - val list = ArrayList(tags.size + sortOrders.size + 3) + val list = ArrayList(tags.size + states.size + sortOrders.size + 4) + val isMultiTag = repository.isMultipleTagsSupported if (query.isEmpty()) { if (sortOrders.isNotEmpty()) { list.add(ListHeader(R.string.sort_order)) @@ -218,10 +251,28 @@ class FilterCoordinator @Inject constructor( FilterItem.Sort(it, isSelected = it == state.sortOrder) } } + if (states.isNotEmpty()) { + list.add( + ListHeader( + textRes = R.string.state, + buttonTextRes = if (state.states.isEmpty()) 0 else R.string.reset, + payload = R.string.state, + ), + ) + states.mapTo(list) { + FilterItem.State(it, isChecked = it in state.states) + } + } if (allTags.isLoading || allTags.isError || tags.isNotEmpty()) { - list.add(ListHeader(R.string.genres, if (state.tags.isEmpty()) 0 else R.string.reset)) + list.add( + ListHeader( + textRes = R.string.genres, + buttonTextRes = if (state.tags.isEmpty()) 0 else R.string.reset, + payload = R.string.genres, + ), + ) tags.mapTo(list) { - FilterItem.Tag(it, isChecked = it in state.tags) + FilterItem.Tag(it, isMultiple = isMultiTag, isChecked = it in state.tags) } } if (allTags.isError) { @@ -232,7 +283,7 @@ class FilterCoordinator @Inject constructor( } else { tags.mapNotNullTo(list) { if (it.title.contains(query, ignoreCase = true)) { - FilterItem.Tag(it, isChecked = it in state.tags) + FilterItem.Tag(it, isMultiple = isMultiTag, isChecked = it in state.tags) } else { null } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt index a16c55c15..cd56216c7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterHeaderFragment.kt @@ -39,7 +39,7 @@ class FilterHeaderFragment : BaseFragment(), ChipsV if (tag == null) { FilterSheetFragment.show(parentFragmentManager) } else { - filter.onTagItemClick(FilterItem.Tag(tag, !chip.isChecked)) + filter.onTagItemClick(FilterItem.Tag(tag, filter.header.value.allowMultipleTags, !chip.isChecked)) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/OnFilterChangedListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/OnFilterChangedListener.kt index b82e08a43..0c67d4209 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/OnFilterChangedListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/OnFilterChangedListener.kt @@ -8,4 +8,6 @@ interface OnFilterChangedListener : ListHeaderClickListener { fun onSortItemClick(item: FilterItem.Sort) fun onTagItemClick(item: FilterItem.Tag) + + fun onStateItemClick(item: FilterItem.State) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt index 7e376dd55..40e9bba4b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterHeaderModel.kt @@ -7,6 +7,7 @@ class FilterHeaderModel( val chips: Collection, val sortOrder: SortOrder?, val hasSelectedTags: Boolean, + val allowMultipleTags: Boolean, ) { val textSummary: String @@ -19,6 +20,7 @@ class FilterHeaderModel( other as FilterHeaderModel if (chips != other.chips) return false + if (allowMultipleTags != other.allowMultipleTags) return false return sortOrder == other.sortOrder // Not need to check hasSelectedTags @@ -26,6 +28,7 @@ class FilterHeaderModel( override fun hashCode(): Int { var result = chips.hashCode() + result = 31 * result + allowMultipleTags.hashCode() result = 31 * result + (sortOrder?.hashCode() ?: 0) return result } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt index 063d3521c..536f6f6dc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterItem.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.filter.ui.model import androidx.annotation.StringRes import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder @@ -28,11 +29,12 @@ sealed interface FilterItem : ListModel { data class Tag( val tag: MangaTag, + val isMultiple: Boolean, val isChecked: Boolean, ) : FilterItem { override fun areItemsTheSame(other: ListModel): Boolean { - return other is Tag && other.tag == tag + return other is Tag && other.isMultiple == isMultiple && other.tag == tag } override fun getChangePayload(previousState: ListModel): Any? { @@ -44,6 +46,24 @@ sealed interface FilterItem : ListModel { } } + data class State( + val state: MangaState, + val isChecked: Boolean + ) : FilterItem { + + override fun areItemsTheSame(other: ListModel): Boolean { + return other is State && other.state == state + } + + override fun getChangePayload(previousState: ListModel): Any? { + return if (previousState is State && previousState.isChecked != isChecked) { + ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED + } else { + super.getChangePayload(previousState) + } + } + } + data class Error( @StringRes val textResId: Int, ) : FilterItem { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterState.kt deleted file mode 100644 index efb1d4168..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/model/FilterState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.koitharu.kotatsu.filter.ui.model - -import org.koitharu.kotatsu.parsers.model.MangaTag -import org.koitharu.kotatsu.parsers.model.SortOrder - -data class FilterState( - val sortOrder: SortOrder?, - val tags: Set, -) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt index 63cbd2c45..af92052b0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/ListItemType.kt @@ -4,6 +4,8 @@ enum class ListItemType { FILTER_SORT, FILTER_TAG, + FILTER_TAG_MULTI, + FILTER_STATE, HEADER, MANGA_LIST, MANGA_LIST_DETAILED, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt index a1b3d8ca2..3251cd0f8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/TypedListSpacingDecoration.kt @@ -29,6 +29,8 @@ class TypedListSpacingDecoration( when (itemType) { ListItemType.FILTER_SORT, ListItemType.FILTER_TAG, + ListItemType.FILTER_TAG_MULTI, + ListItemType.FILTER_STATE, -> outRect.set(0) ListItemType.HEADER, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt index 712d717f7..3f1df2cff 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt @@ -98,7 +98,7 @@ class PreviewFragment : BaseFragment(), View.OnClickList if (filter == null) { startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag))) } else { - filter.onTagItemClick(FilterItem.Tag(tag, false)) + filter.onTagItemClick(FilterItem.Tag(tag, filter.header.value.allowMultipleTags, false)) closeSelf() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt index 822d0a52d..ad427fb09 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.runInterruptible import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.AlphanumComparator import org.koitharu.kotatsu.core.util.CompositeMutex2 import org.koitharu.kotatsu.core.util.ext.children import org.koitharu.kotatsu.core.util.ext.deleteAwait @@ -25,8 +26,10 @@ import org.koitharu.kotatsu.local.data.output.LocalMangaUtil import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.runCatchingCancellable @@ -47,7 +50,9 @@ class LocalMangaRepository @Inject constructor( override val source = MangaSource.LOCAL private val locks = CompositeMutex2() + override val isMultipleTagsSupported: Boolean = true override val sortOrders: Set = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST) + override val states = emptySet() override var defaultSortOrder: SortOrder get() = settings.localListOrder @@ -55,33 +60,32 @@ class LocalMangaRepository @Inject constructor( settings.localListOrder = value } - override suspend fun getList(offset: Int, query: String): List { + override suspend fun getList(offset: Int, filter: MangaListFilter?): List { if (offset > 0) { return emptyList() } val list = getRawList() - if (query.isNotEmpty()) { - list.retainAll { x -> x.isMatchesQuery(query) } - } - return list.unwrap() - } + when (filter) { + is MangaListFilter.Search -> { + list.retainAll { x -> x.isMatchesQuery(filter.query) } + } - override suspend fun getList(offset: Int, tags: Set?, sortOrder: SortOrder?): List { - if (offset > 0) { - return emptyList() - } - val list = getRawList() - if (!tags.isNullOrEmpty()) { - list.retainAll { x -> x.containsTags(tags) } - } - when (sortOrder) { - SortOrder.ALPHABETICAL -> list.sortWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.manga.title }) - SortOrder.RATING -> list.sortByDescending { it.manga.rating } - SortOrder.NEWEST, - SortOrder.UPDATED, - -> list.sortByDescending { it.createdAt } + is MangaListFilter.Advanced -> { + if (filter.tags.isNotEmpty()) { + list.retainAll { x -> x.containsTags(filter.tags) } + } + when (filter.sortOrder) { + SortOrder.ALPHABETICAL -> list.sortWith(compareBy(AlphanumComparator()) { x -> x.manga.title }) + SortOrder.RATING -> list.sortByDescending { it.manga.rating } + SortOrder.NEWEST, + SortOrder.UPDATED, + -> list.sortByDescending { it.createdAt } - else -> Unit + else -> Unit + } + } + + null -> Unit } return list.unwrap() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteLocalMangaUseCase.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteLocalMangaUseCase.kt index 07a22f924..506b3a657 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteLocalMangaUseCase.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/domain/DeleteLocalMangaUseCase.kt @@ -1,11 +1,11 @@ package org.koitharu.kotatsu.local.domain import org.koitharu.kotatsu.core.model.isLocal +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.util.runCatchingCancellable -import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import java.io.IOException import javax.inject.Inject @@ -27,7 +27,7 @@ class DeleteLocalMangaUseCase @Inject constructor( } suspend operator fun invoke(ids: Set) { - val list = localMangaRepository.getList(0, null, null) + val list = localMangaRepository.getList(0, null) var removed = 0 for (manga in list) { if (manga.id in ids) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 3b834d3e4..35b01a548 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -30,7 +30,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.explore.domain.ExploreRepository import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.MangaFilter -import org.koitharu.kotatsu.filter.ui.model.FilterState import org.koitharu.kotatsu.list.domain.ListExtraProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState @@ -40,6 +39,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import javax.inject.Inject @@ -122,7 +122,7 @@ open class RemoteListViewModel @Inject constructor( applyFilter(tags) } - protected fun loadList(filterState: FilterState, append: Boolean): Job { + protected fun loadList(filterState: MangaListFilter.Advanced, append: Boolean): Job { loadingJob?.let { if (it.isActive) return it } @@ -131,8 +131,7 @@ open class RemoteListViewModel @Inject constructor( listError.value = null val list = repository.getList( offset = if (append) mangaList.value?.size ?: 0 else 0, - sortOrder = filterState.sortOrder, - tags = filterState.tags, + filter = filterState, ) val oldList = mangaList.getAndUpdate { oldList -> if (!append || oldList.isNullOrEmpty()) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index a1ad1f52a..2b6f4a2a7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorFooter import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import javax.inject.Inject @HiltViewModel @@ -101,7 +102,7 @@ class SearchViewModel @Inject constructor( listError.value = null val list = repository.getList( offset = if (append) mangaList.value?.size ?: 0 else 0, - query = query, + filter = MangaListFilter.Search(query) ) if (!append) { mangaList.value = list diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt index d6dfe6c26..8ae7a5052 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt @@ -35,6 +35,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import javax.inject.Inject @@ -115,7 +116,7 @@ class MultiSearchViewModel @Inject constructor( launch { val item = runCatchingCancellable { semaphore.withPermit { - mangaRepositoryFactory.create(source).getList(offset = 0, query = q) + mangaRepositoryFactory.create(source).getList(offset = 0, filter = MangaListFilter.Search(q)) .toUi(ListMode.GRID, extraProvider) } }.fold( diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index 679a89a0d..166050e20 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -62,6 +62,7 @@ import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaListFilter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder @@ -208,7 +209,15 @@ class SuggestionsWorker @AssistedInject constructor( val tag = tags.firstNotNullOfOrNull { title -> availableTags.find { x -> x.title.almostEquals(title, TAG_EQ_THRESHOLD) } } - val list = repository.getList(0, setOfNotNull(tag), order).asArrayList() + val list = repository.getList( + offset = 0, + filter = MangaListFilter.Advanced( + sortOrder = order, + tags = setOfNotNull(tag), + locale = null, + states = setOf(), + ), + ).asArrayList() if (appSettings.isSuggestionsExcludeNsfw) { list.removeAll { it.isNsfw } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0897be3d7..aba27624c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -529,4 +529,5 @@ Paused Reduce memory consumption (beta) Reduce offscreen pages quality to use less memory + State