Add manga state to filter

pull/571/head
Koitharu 2 years ago
parent acba312e8d
commit d3f23ea3a3
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 599 versionCode = 600
versionName = '6.4-a1' versionName = '6.4'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner" testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp { ksp {
@ -82,7 +82,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:c3613f3ba4') { implementation('com.github.KotatsuApp:kotatsu-parsers:46e863ef79') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }

@ -1,12 +1,15 @@
package org.koitharu.kotatsu.core.model package org.koitharu.kotatsu.core.model
import android.net.Uri import android.net.Uri
import androidx.annotation.StringRes
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.iterator import org.koitharu.kotatsu.core.util.ext.iterator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
@JvmName("mangaIds") @JvmName("mangaIds")
@ -31,6 +34,15 @@ fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
return acc.values.max() 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? { fun Manga.findChapter(id: Long): MangaChapter? {
return chapters?.findById(id) return chapters?.findById(id)
} }

@ -63,6 +63,9 @@ class MirrorSwitchInterceptor @Inject constructor(
} }
synchronized(obtainLock(repository.source)) { synchronized(obtainLock(repository.source)) {
val currentMirror = repository.domain val currentMirror = repository.domain
if (currentMirror !in mirrors) {
return@synchronized false
}
addToBlacklist(repository.source, currentMirror) addToBlacklist(repository.source, currentMirror)
val newMirror = mirrors.firstOrNull { x -> val newMirror = mirrors.firstOrNull { x ->
x != currentMirror && !isBlacklisted(repository.source, x) x != currentMirror && !isBlacklisted(repository.source, x)

@ -8,6 +8,7 @@ import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga 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.MangaSource
import org.koitharu.kotatsu.parsers.util.almostEquals import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.levenshteinDistance 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? { private suspend fun MangaRepository.findExact(url: String?, title: String?): Manga? {
if (!title.isNullOrEmpty()) { if (!title.isNullOrEmpty()) {
val list = getList(0, title) val list = getList(0, MangaListFilter.Search(title))
if (url != null) { if (url != null) {
list.find { it.url == url }?.let { list.find { it.url == url }?.let {
return it return it
@ -77,7 +78,7 @@ class MangaLinkResolver @Inject constructor(
}.ifNullOrEmpty { }.ifNullOrEmpty {
seed.author seed.author
} ?: return@runCatchingCancellable null } ?: return@runCatchingCancellable null
val seedList = getList(0, seedTitle) val seedList = getList(0, MangaListFilter.Search(seedTitle))
seedList.first { x -> x.url == url } seedList.first { x -> x.url == url }
}.getOrThrow() }.getOrThrow()
} }

@ -7,8 +7,10 @@ import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter 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.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource 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.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
@ -23,11 +25,13 @@ interface MangaRepository {
val sortOrders: Set<SortOrder> val sortOrders: Set<SortOrder>
val states: Set<MangaState>
var defaultSortOrder: SortOrder var defaultSortOrder: SortOrder
suspend fun getList(offset: Int, query: String): List<Manga> val isMultipleTagsSupported: Boolean
suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga>
suspend fun getDetails(manga: Manga): Manga suspend fun getDetails(manga: Manga): Manga

@ -23,8 +23,10 @@ import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.Favicons import org.koitharu.kotatsu.parsers.model.Favicons
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter 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.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource 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.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.domain import org.koitharu.kotatsu.parsers.util.domain
@ -40,7 +42,10 @@ class RemoteMangaRepository(
get() = parser.source get() = parser.source
override val sortOrders: Set<SortOrder> override val sortOrders: Set<SortOrder>
get() = parser.sortOrders get() = parser.availableSortOrders
override val states: Set<MangaState>
get() = parser.availableStates
override var defaultSortOrder: SortOrder override var defaultSortOrder: SortOrder
get() = getConfig().defaultSortOrder ?: sortOrders.first() get() = getConfig().defaultSortOrder ?: sortOrders.first()
@ -48,6 +53,9 @@ class RemoteMangaRepository(
getConfig().defaultSortOrder = value getConfig().defaultSortOrder = value
} }
override val isMultipleTagsSupported: Boolean
get() = parser.isMultipleTagsSupported
var domain: String var domain: String
get() = parser.domain get() = parser.domain
set(value) { set(value) {
@ -68,15 +76,9 @@ class RemoteMangaRepository(
} }
} }
override suspend fun getList(offset: Int, query: String): List<Manga> { override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
return mirrorSwitchInterceptor.withMirrorSwitching {
parser.getList(offset, query)
}
}
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> {
return mirrorSwitchInterceptor.withMirrorSwitching { return mirrorSwitchInterceptor.withMirrorSwitching {
parser.getList(offset, tags, sortOrder) parser.getList(offset, filter)
} }
} }
@ -98,7 +100,7 @@ class RemoteMangaRepository(
} }
override suspend fun getTags(): Set<MangaTag> = mirrorSwitchInterceptor.withMirrorSwitching { override suspend fun getTags(): Set<MangaTag> = mirrorSwitchInterceptor.withMirrorSwitching {
parser.getTags() parser.getAvailableTags()
} }
suspend fun getFavicons(): Favicons = mirrorSwitchInterceptor.withMirrorSwitching { suspend fun getFavicons(): Favicons = mirrorSwitchInterceptor.withMirrorSwitching {
@ -133,7 +135,7 @@ class RemoteMangaRepository(
} }
suspend fun find(manga: Manga): Manga? { 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 } return list.find { x -> x.id == manga.id }
} }

@ -9,6 +9,7 @@ import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.ContentType import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga 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.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist import org.koitharu.kotatsu.suggestions.domain.TagsBlacklist
@ -73,7 +74,15 @@ class ExploreRepository @Inject constructor(
val tag = tags.firstNotNullOfOrNull { title -> val tag = tags.firstNotNullOfOrNull { title ->
availableTags.find { x -> x.title.almostEquals(title, 0.4f) } 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) { if (settings.isSuggestionsExcludeNsfw) {
list.removeAll { it.isNsfw } list.removeAll { it.isNsfw }
} }

@ -5,6 +5,7 @@ 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.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject import javax.inject.Inject
@ -18,7 +19,7 @@ class RecoverMangaUseCase @Inject constructor(
return@runCatchingCancellable null return@runCatchingCancellable null
} }
val repository = repositoryFactory.create(manga.source) 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 { val newManga = list.find { x -> x.title == manga.title }?.let {
repository.getDetails(it) repository.getDetails(it)
} ?: return@runCatchingCancellable null } ?: return@runCatchingCancellable null

@ -19,6 +19,8 @@ class FilterAdapter(
init { init {
addDelegate(ListItemType.FILTER_SORT, filterSortDelegate(listener)) addDelegate(ListItemType.FILTER_SORT, filterSortDelegate(listener))
addDelegate(ListItemType.FILTER_TAG, filterTagDelegate(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.HEADER, listHeaderAD(listener))
addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD()) addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())

@ -4,6 +4,7 @@ import android.widget.TextView
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R 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.ui.model.titleRes
import org.koitharu.kotatsu.core.util.ext.setChecked import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
@ -27,10 +28,44 @@ fun filterSortDelegate(
} }
} }
fun filterStateDelegate(
listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.State, ListModel, ItemCheckableMultipleBinding>(
{ 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( fun filterTagDelegate(
listener: OnFilterChangedListener, listener: OnFilterChangedListener,
) = adapterDelegateViewBinding<FilterItem.Tag, ListModel, ItemCheckableSingleBinding>(
{ 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<FilterItem.Tag, ListModel, ItemCheckableMultipleBinding>( ) = adapterDelegateViewBinding<FilterItem.Tag, ListModel, ItemCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) }, { layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is FilterItem.Tag && item.isMultiple },
) { ) {
itemView.setOnClickListener { itemView.setOnClickListener {

@ -28,11 +28,11 @@ import org.koitharu.kotatsu.core.util.ext.require
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.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterItem 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.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState 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.model.MangaTag
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.parsers.util.runCatchingCancellable
@ -55,7 +55,8 @@ class FilterCoordinator @Inject constructor(
private val coroutineScope = lifecycle.lifecycleScope private val coroutineScope = lifecycle.lifecycleScope
private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE)) 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 var searchQuery = MutableStateFlow("")
private val localTags = SuspendLazy { private val localTags = SuspendLazy {
dataRepository.findTags(repository.source) dataRepository.findTags(repository.source)
@ -68,7 +69,12 @@ class FilterCoordinator @Inject constructor(
override val header: StateFlow<FilterHeaderModel> = getHeaderFlow().stateIn( override val header: StateFlow<FilterHeaderModel> = getHeaderFlow().stateIn(
scope = coroutineScope + Dispatchers.Default, scope = coroutineScope + Dispatchers.Default,
started = SharingStarted.Lazily, started = SharingStarted.Lazily,
initialValue = FilterHeaderModel(emptyList(), repository.defaultSortOrder, false), initialValue = FilterHeaderModel(
chips = emptyList(),
sortOrder = repository.defaultSortOrder,
hasSelectedTags = false,
allowMultipleTags = repository.isMultipleTagsSupported,
),
) )
init { init {
@ -81,24 +87,44 @@ class FilterCoordinator @Inject constructor(
override fun onSortItemClick(item: FilterItem.Sort) { override fun onSortItemClick(item: FilterItem.Sort) {
currentState.update { oldValue -> currentState.update { oldValue ->
FilterState(item.order, oldValue.tags) oldValue.copy(sortOrder = item.order)
} }
repository.defaultSortOrder = item.order repository.defaultSortOrder = item.order
} }
override fun onTagItemClick(item: FilterItem.Tag) { override fun onTagItemClick(item: FilterItem.Tag) {
currentState.update { oldValue -> currentState.update { oldValue ->
val newTags = if (item.isChecked) { val newTags = if (!item.isMultiple) {
setOf(item.tag)
} else if (item.isChecked) {
oldValue.tags - item.tag oldValue.tags - item.tag
} else { } else {
oldValue.tags + item.tag 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) { 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<Set<MangaTag>?> = flow { fun observeAvailableTags(): Flow<Set<MangaTag>?> = flow {
@ -112,13 +138,13 @@ class FilterCoordinator @Inject constructor(
fun setTags(tags: Set<MangaTag>) { fun setTags(tags: Set<MangaTag>) {
currentState.update { oldValue -> currentState.update { oldValue ->
FilterState(oldValue.sortOrder, tags) oldValue.copy(tags = tags)
} }
} }
fun reset() { fun reset() {
currentState.update { oldValue -> currentState.update { oldValue ->
FilterState(oldValue.sortOrder, emptySet()) oldValue.copy(oldValue.sortOrder, emptySet(), null, emptySet())
} }
} }
@ -133,7 +159,12 @@ class FilterCoordinator @Inject constructor(
observeAvailableTags(), observeAvailableTags(),
) { state, available -> ) { state, available ->
val chips = createChipsList(state, available.orEmpty(), 8) 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( private fun getItemsFlow() = combine(
@ -156,7 +187,7 @@ class FilterCoordinator @Inject constructor(
} }
private suspend fun createChipsList( private suspend fun createChipsList(
filterState: FilterState, filterState: MangaListFilter.Advanced,
availableTags: Set<MangaTag>, availableTags: Set<MangaTag>,
limit: Int, limit: Int,
): List<ChipsView.ChipModel> { ): List<ChipsView.ChipModel> {
@ -205,12 +236,14 @@ class FilterCoordinator @Inject constructor(
@WorkerThread @WorkerThread
private fun buildFilterList( private fun buildFilterList(
allTags: TagsWrapper, allTags: TagsWrapper,
state: FilterState, state: MangaListFilter.Advanced,
query: String, query: String,
): List<ListModel> { ): List<ListModel> {
val sortOrders = repository.sortOrders.sortedByOrdinal() val sortOrders = repository.sortOrders.sortedByOrdinal()
val states = repository.states
val tags = mergeTags(state.tags, allTags.tags).toList() val tags = mergeTags(state.tags, allTags.tags).toList()
val list = ArrayList<ListModel>(tags.size + sortOrders.size + 3) val list = ArrayList<ListModel>(tags.size + states.size + sortOrders.size + 4)
val isMultiTag = repository.isMultipleTagsSupported
if (query.isEmpty()) { if (query.isEmpty()) {
if (sortOrders.isNotEmpty()) { if (sortOrders.isNotEmpty()) {
list.add(ListHeader(R.string.sort_order)) list.add(ListHeader(R.string.sort_order))
@ -218,10 +251,28 @@ class FilterCoordinator @Inject constructor(
FilterItem.Sort(it, isSelected = it == state.sortOrder) 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()) { 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) { tags.mapTo(list) {
FilterItem.Tag(it, isChecked = it in state.tags) FilterItem.Tag(it, isMultiple = isMultiTag, isChecked = it in state.tags)
} }
} }
if (allTags.isError) { if (allTags.isError) {
@ -232,7 +283,7 @@ class FilterCoordinator @Inject constructor(
} else { } else {
tags.mapNotNullTo(list) { tags.mapNotNullTo(list) {
if (it.title.contains(query, ignoreCase = true)) { 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 { } else {
null null
} }

@ -39,7 +39,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
if (tag == null) { if (tag == null) {
FilterSheetFragment.show(parentFragmentManager) FilterSheetFragment.show(parentFragmentManager)
} else { } else {
filter.onTagItemClick(FilterItem.Tag(tag, !chip.isChecked)) filter.onTagItemClick(FilterItem.Tag(tag, filter.header.value.allowMultipleTags, !chip.isChecked))
} }
} }

@ -8,4 +8,6 @@ interface OnFilterChangedListener : ListHeaderClickListener {
fun onSortItemClick(item: FilterItem.Sort) fun onSortItemClick(item: FilterItem.Sort)
fun onTagItemClick(item: FilterItem.Tag) fun onTagItemClick(item: FilterItem.Tag)
fun onStateItemClick(item: FilterItem.State)
} }

@ -7,6 +7,7 @@ class FilterHeaderModel(
val chips: Collection<ChipsView.ChipModel>, val chips: Collection<ChipsView.ChipModel>,
val sortOrder: SortOrder?, val sortOrder: SortOrder?,
val hasSelectedTags: Boolean, val hasSelectedTags: Boolean,
val allowMultipleTags: Boolean,
) { ) {
val textSummary: String val textSummary: String
@ -19,6 +20,7 @@ class FilterHeaderModel(
other as FilterHeaderModel other as FilterHeaderModel
if (chips != other.chips) return false if (chips != other.chips) return false
if (allowMultipleTags != other.allowMultipleTags) return false
return sortOrder == other.sortOrder return sortOrder == other.sortOrder
// Not need to check hasSelectedTags // Not need to check hasSelectedTags
@ -26,6 +28,7 @@ class FilterHeaderModel(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = chips.hashCode() var result = chips.hashCode()
result = 31 * result + allowMultipleTags.hashCode()
result = 31 * result + (sortOrder?.hashCode() ?: 0) result = 31 * result + (sortOrder?.hashCode() ?: 0)
return result return result
} }

@ -3,6 +3,7 @@ package org.koitharu.kotatsu.filter.ui.model
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel 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.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
@ -28,11 +29,12 @@ sealed interface FilterItem : ListModel {
data class Tag( data class Tag(
val tag: MangaTag, val tag: MangaTag,
val isMultiple: Boolean,
val isChecked: Boolean, val isChecked: Boolean,
) : FilterItem { ) : FilterItem {
override fun areItemsTheSame(other: ListModel): Boolean { 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? { 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( data class Error(
@StringRes val textResId: Int, @StringRes val textResId: Int,
) : FilterItem { ) : FilterItem {

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

@ -4,6 +4,8 @@ enum class ListItemType {
FILTER_SORT, FILTER_SORT,
FILTER_TAG, FILTER_TAG,
FILTER_TAG_MULTI,
FILTER_STATE,
HEADER, HEADER,
MANGA_LIST, MANGA_LIST,
MANGA_LIST_DETAILED, MANGA_LIST_DETAILED,

@ -29,6 +29,8 @@ class TypedListSpacingDecoration(
when (itemType) { when (itemType) {
ListItemType.FILTER_SORT, ListItemType.FILTER_SORT,
ListItemType.FILTER_TAG, ListItemType.FILTER_TAG,
ListItemType.FILTER_TAG_MULTI,
ListItemType.FILTER_STATE,
-> outRect.set(0) -> outRect.set(0)
ListItemType.HEADER, ListItemType.HEADER,

@ -98,7 +98,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), View.OnClickList
if (filter == null) { if (filter == null) {
startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag))) startActivity(MangaListActivity.newIntent(requireContext(), setOf(tag)))
} else { } else {
filter.onTagItemClick(FilterItem.Tag(tag, false)) filter.onTagItemClick(FilterItem.Tag(tag, filter.header.value.allowMultipleTags, false))
closeSelf() closeSelf()
} }
} }

@ -14,6 +14,7 @@ import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings 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.CompositeMutex2
import org.koitharu.kotatsu.core.util.ext.children import org.koitharu.kotatsu.core.util.ext.children
import org.koitharu.kotatsu.core.util.ext.deleteAwait 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.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter 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.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource 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.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@ -47,7 +50,9 @@ class LocalMangaRepository @Inject constructor(
override val source = MangaSource.LOCAL override val source = MangaSource.LOCAL
private val locks = CompositeMutex2<Long>() private val locks = CompositeMutex2<Long>()
override val isMultipleTagsSupported: Boolean = true
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST) override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST)
override val states = emptySet<MangaState>()
override var defaultSortOrder: SortOrder override var defaultSortOrder: SortOrder
get() = settings.localListOrder get() = settings.localListOrder
@ -55,33 +60,32 @@ class LocalMangaRepository @Inject constructor(
settings.localListOrder = value settings.localListOrder = value
} }
override suspend fun getList(offset: Int, query: String): List<Manga> { override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
if (offset > 0) { if (offset > 0) {
return emptyList() return emptyList()
} }
val list = getRawList() val list = getRawList()
if (query.isNotEmpty()) { when (filter) {
list.retainAll { x -> x.isMatchesQuery(query) } is MangaListFilter.Search -> {
} list.retainAll { x -> x.isMatchesQuery(filter.query) }
return list.unwrap() }
}
override suspend fun getList(offset: Int, tags: Set<MangaTag>?, sortOrder: SortOrder?): List<Manga> { is MangaListFilter.Advanced -> {
if (offset > 0) { if (filter.tags.isNotEmpty()) {
return emptyList() list.retainAll { x -> x.containsTags(filter.tags) }
} }
val list = getRawList() when (filter.sortOrder) {
if (!tags.isNullOrEmpty()) { SortOrder.ALPHABETICAL -> list.sortWith(compareBy(AlphanumComparator()) { x -> x.manga.title })
list.retainAll { x -> x.containsTags(tags) } SortOrder.RATING -> list.sortByDescending { it.manga.rating }
} SortOrder.NEWEST,
when (sortOrder) { SortOrder.UPDATED,
SortOrder.ALPHABETICAL -> list.sortWith(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.manga.title }) -> list.sortByDescending { it.createdAt }
SortOrder.RATING -> list.sortByDescending { it.manga.rating }
SortOrder.NEWEST, else -> Unit
SortOrder.UPDATED, }
-> list.sortByDescending { it.createdAt } }
else -> Unit null -> Unit
} }
return list.unwrap() return list.unwrap()
} }

@ -1,11 +1,11 @@
package org.koitharu.kotatsu.local.domain package org.koitharu.kotatsu.local.domain
import org.koitharu.kotatsu.core.model.isLocal 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.history.data.HistoryRepository
import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@ -27,7 +27,7 @@ class DeleteLocalMangaUseCase @Inject constructor(
} }
suspend operator fun invoke(ids: Set<Long>) { suspend operator fun invoke(ids: Set<Long>) {
val list = localMangaRepository.getList(0, null, null) val list = localMangaRepository.getList(0, null)
var removed = 0 var removed = 0
for (manga in list) { for (manga in list) {
if (manga.id in ids) { if (manga.id in ids) {

@ -30,7 +30,6 @@ import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.explore.domain.ExploreRepository import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.MangaFilter 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.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState 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.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga 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.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import javax.inject.Inject import javax.inject.Inject
@ -122,7 +122,7 @@ open class RemoteListViewModel @Inject constructor(
applyFilter(tags) applyFilter(tags)
} }
protected fun loadList(filterState: FilterState, append: Boolean): Job { protected fun loadList(filterState: MangaListFilter.Advanced, append: Boolean): Job {
loadingJob?.let { loadingJob?.let {
if (it.isActive) return it if (it.isActive) return it
} }
@ -131,8 +131,7 @@ open class RemoteListViewModel @Inject constructor(
listError.value = null listError.value = null
val list = repository.getList( val list = repository.getList(
offset = if (append) mangaList.value?.size ?: 0 else 0, offset = if (append) mangaList.value?.size ?: 0 else 0,
sortOrder = filterState.sortOrder, filter = filterState,
tags = filterState.tags,
) )
val oldList = mangaList.getAndUpdate { oldList -> val oldList = mangaList.getAndUpdate { oldList ->
if (!append || oldList.isNullOrEmpty()) { if (!append || oldList.isNullOrEmpty()) {

@ -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.toErrorState
import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -101,7 +102,7 @@ class SearchViewModel @Inject constructor(
listError.value = null listError.value = null
val list = repository.getList( val list = repository.getList(
offset = if (append) mangaList.value?.size ?: 0 else 0, offset = if (append) mangaList.value?.size ?: 0 else 0,
query = query, filter = MangaListFilter.Search(query)
) )
if (!append) { if (!append) {
mangaList.value = list mangaList.value = list

@ -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.LoadingState
import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject import javax.inject.Inject
@ -115,7 +116,7 @@ class MultiSearchViewModel @Inject constructor(
launch { launch {
val item = runCatchingCancellable { val item = runCatchingCancellable {
semaphore.withPermit { semaphore.withPermit {
mangaRepositoryFactory.create(source).getList(offset = 0, query = q) mangaRepositoryFactory.create(source).getList(offset = 0, filter = MangaListFilter.Search(q))
.toUi(ListMode.GRID, extraProvider) .toUi(ListMode.GRID, extraProvider)
} }
}.fold( }.fold(

@ -62,6 +62,7 @@ import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga 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.MangaSource
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
@ -208,7 +209,15 @@ class SuggestionsWorker @AssistedInject constructor(
val tag = tags.firstNotNullOfOrNull { title -> val tag = tags.firstNotNullOfOrNull { title ->
availableTags.find { x -> x.title.almostEquals(title, TAG_EQ_THRESHOLD) } 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) { if (appSettings.isSuggestionsExcludeNsfw) {
list.removeAll { it.isNsfw } list.removeAll { it.isNsfw }
} }

@ -529,4 +529,5 @@
<string name="state_paused">Paused</string> <string name="state_paused">Paused</string>
<string name="reader_optimize">Reduce memory consumption (beta)</string> <string name="reader_optimize">Reduce memory consumption (beta)</string>
<string name="reader_optimize_summary">Reduce offscreen pages quality to use less memory</string> <string name="reader_optimize_summary">Reduce offscreen pages quality to use less memory</string>
<string name="state">State</string>
</resources> </resources>

Loading…
Cancel
Save