Excluded tags and content rating in filter

This commit is contained in:
Koitharu
2024-01-02 19:51:37 +02:00
parent 3f2e32dcc2
commit 2f2a5b868d
20 changed files with 338 additions and 45 deletions

View File

@@ -82,7 +82,7 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:ea095084cc') {
implementation('com.github.KotatsuApp:kotatsu-parsers:b274b51699') {
exclude group: 'org.json', module: 'json'
}

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaSource
class CaptchaNotifier(
private val context: Context,
@@ -58,6 +59,10 @@ class CaptchaNotifier(
manager.notify(TAG, exception.source.hashCode(), notification)
}
fun dismiss(source: MangaSource) {
NotificationManagerCompat.from(context).cancel(TAG, source.hashCode())
}
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
val e = result.throwable

View File

@@ -31,6 +31,16 @@ abstract class TagsDao {
)
abstract suspend fun findPopularTags(source: String, limit: Int): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source
GROUP BY tags.title
ORDER BY COUNT(manga_id) ASC
LIMIT :limit""",
)
abstract suspend fun findRareTags(source: String, limit: Int): List<TagEntity>
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id

View File

@@ -7,6 +7,7 @@ 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.ContentRating
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -56,6 +57,14 @@ val MangaState.iconResId: Int
MangaState.UPCOMING -> materialR.drawable.ic_clock_black_24dp
}
@get:StringRes
val ContentRating.titleResId: Int
get() = when (this) {
ContentRating.SAFE -> R.string.rating_safe
ContentRating.SUGGESTIVE -> R.string.rating_suggestive
ContentRating.ADULT -> R.string.rating_adult
}
fun Manga.findChapter(id: Long): MangaChapter? {
return chapters?.findById(id)
}

View File

@@ -5,6 +5,7 @@ import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
@@ -28,10 +29,16 @@ interface MangaRepository {
val states: Set<MangaState>
val contentRatings: Set<ContentRating>
var defaultSortOrder: SortOrder
val isMultipleTagsSupported: Boolean
val isTagsExclusionSupported: Boolean
val isSearchSupported: Boolean
suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga>
suspend fun getDetails(manga: Manga): Manga

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Favicons
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
@@ -49,6 +50,9 @@ class RemoteMangaRepository(
override val states: Set<MangaState>
get() = parser.availableStates
override val contentRatings: Set<ContentRating>
get() = parser.availableContentRating
override var defaultSortOrder: SortOrder
get() = getConfig().defaultSortOrder ?: sortOrders.first()
set(value) {
@@ -58,6 +62,12 @@ class RemoteMangaRepository(
override val isMultipleTagsSupported: Boolean
get() = parser.isMultipleTagsSupported
override val isSearchSupported: Boolean
get() = parser.isSearchSupported
override val isTagsExclusionSupported: Boolean
get() = parser.isTagsExclusionSupported
var domain: String
get() = parser.domain
set(value) {

View File

@@ -76,12 +76,9 @@ class ExploreRepository @Inject constructor(
}
val list = repository.getList(
offset = 0,
filter = MangaListFilter.Advanced(
sortOrder = order,
tags = setOfNotNull(tag),
locale = null,
states = emptySet(),
),
filter = MangaListFilter.Advanced.Builder(order)
.tags(setOfNotNull(tag))
.build(),
).asArrayList()
if (settings.isSuggestionsExcludeNsfw) {
list.removeAll { it.isNsfw }

View File

@@ -41,6 +41,7 @@ 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.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -67,11 +68,28 @@ class FilterCoordinator @Inject constructor(
private val coroutineScope = lifecycle.lifecycleScope
private val repository = mangaRepositoryFactory.create(savedStateHandle.require(RemoteListFragment.ARG_SOURCE))
private val currentState = MutableStateFlow(
MangaListFilter.Advanced(repository.defaultSortOrder, emptySet(), null, emptySet()),
MangaListFilter.Advanced(
sortOrder = repository.defaultSortOrder,
tags = emptySet(),
tagsExclude = emptySet(),
locale = null,
states = emptySet(),
contentRating = emptySet(),
),
)
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
@@ -96,6 +114,22 @@ class FilterCoordinator @Inject constructor(
)
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
override val filterTagsExcluded: StateFlow<FilterProperty<MangaTag>> = if (repository.isTagsExclusionSupported) {
combine(
currentState.distinctUntilChangedBy { it.tagsExclude },
getBottomTagsAsFlow(4),
) { state, tags ->
FilterProperty(
availableItems = tags.items.asArrayList(),
selectedItems = state.tagsExclude,
isLoading = tags.isLoading,
error = tags.error,
)
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
} else {
MutableStateFlow(emptyProperty())
}
override val filterSortOrder: StateFlow<FilterProperty<SortOrder>> = combine(
currentState.distinctUntilChangedBy { it.sortOrder },
flowOf(repository.sortOrders),
@@ -120,6 +154,18 @@ class FilterCoordinator @Inject constructor(
)
}.stateIn(coroutineScope + Dispatchers.Default, SharingStarted.Lazily, loadingProperty())
override val filterContentRating: StateFlow<FilterProperty<ContentRating>> = combine(
currentState.distinctUntilChangedBy { it.contentRating },
flowOf(repository.contentRatings),
) { rating, ratings ->
FilterProperty(
availableItems = ratings.sortedBy { it.ordinal },
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(),
@@ -187,7 +233,32 @@ class FilterCoordinator @Inject constructor(
emptySet()
}
}
oldValue.copy(tags = newTags)
oldValue.copy(
tags = newTags,
tagsExclude = oldValue.tagsExclude - newTags,
)
}
}
override fun setTagExcluded(value: MangaTag, addOrRemove: Boolean) {
currentState.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(
tagsExclude = newTags,
tags = oldValue.tags - newTags
)
}
}
@@ -202,6 +273,17 @@ class FilterCoordinator @Inject constructor(
}
}
override fun setContentRating(value: ContentRating, addOrRemove: Boolean) {
currentState.update { oldValue ->
val newRating = if (addOrRemove) {
oldValue.contentRating + value
} else {
oldValue.contentRating - value
}
oldValue.copy(contentRating = newRating)
}
}
override fun onListHeaderClick(item: ListHeader, view: View) {
currentState.update { oldValue ->
oldValue.copy(
@@ -224,13 +306,16 @@ class FilterCoordinator @Inject constructor(
fun setTags(tags: Set<MangaTag>) {
currentState.update { oldValue ->
oldValue.copy(tags = tags)
oldValue.copy(
tags = tags,
tagsExclude = oldValue.tagsExclude - tags
)
}
}
fun reset() {
currentState.update { oldValue ->
oldValue.copy(oldValue.sortOrder, emptySet(), null, emptySet())
MangaListFilter.Advanced.Builder(oldValue.sortOrder).build()
}
}
@@ -248,17 +333,6 @@ class FilterCoordinator @Inject constructor(
)
}
private fun getTagsAsFlow() = 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))
}
}
private fun getLocalesAsFlow(): Flow<PendingData<Locale>> = flow {
emit(PendingData(emptySet(), isLoading = true, error = null))
tryLoadLocales()
@@ -277,7 +351,18 @@ class FilterCoordinator @Inject constructor(
searchRepository.getTagsSuggestion(it).take(limit)
}
},
getTagsAsFlow(),
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(
flow { emit(searchRepository.getRareTags(repository.source, limit)) },
tagsFlow,
) { suggested, all ->
val res = suggested.toMutableList()
if (res.size < limit) {
@@ -411,6 +496,8 @@ class FilterCoordinator @Inject constructor(
private fun <T> loadingProperty() = FilterProperty<T>(emptyList(), emptySet(), true, null)
private fun <T> emptyProperty() = FilterProperty<T>(emptyList(), emptySet(), false, null)
private class TagTitleComparator(lc: String?) : Comparator<MangaTag> {
private val collator = lc?.let { Collator.getInstance(Locale(it)) }

View File

@@ -37,7 +37,7 @@ class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsV
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag
if (tag == null) {
TagsCatalogSheet.show(parentFragmentManager)
TagsCatalogSheet.show(parentFragmentManager, isExcludeTag = false)
} else {
filter.setTag(tag, chip.isChecked)
}

View File

@@ -4,6 +4,7 @@ import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
@@ -15,10 +16,14 @@ interface MangaFilter : OnFilterChangedListener {
val filterTags: StateFlow<FilterProperty<MangaTag>>
val filterTagsExcluded: StateFlow<FilterProperty<MangaTag>>
val filterSortOrder: StateFlow<FilterProperty<SortOrder>>
val filterState: StateFlow<FilterProperty<MangaState>>
val filterContentRating: StateFlow<FilterProperty<ContentRating>>
val filterLocale: StateFlow<FilterProperty<Locale?>>
val header: StateFlow<FilterHeaderModel>

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.filter.ui
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
@@ -14,5 +15,9 @@ interface OnFilterChangedListener : ListHeaderClickListener {
fun setTag(value: MangaTag, addOrRemove: Boolean)
fun setTagExcluded(value: MangaTag, addOrRemove: Boolean)
fun setState(value: MangaState, addOrRemove: Boolean)
fun setContentRating(value: ContentRating, addOrRemove: Boolean)
}

View File

@@ -18,12 +18,14 @@ import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.SheetFilterBinding
import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
@@ -31,8 +33,9 @@ import org.koitharu.kotatsu.parsers.util.toTitleCase
import java.util.Locale
import com.google.android.material.R as materialR
class FilterSheetFragment :
BaseAdaptiveSheet<SheetFilterBinding>(), AdapterView.OnItemSelectedListener, ChipsView.OnChipClickListener {
class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
AdapterView.OnItemSelectedListener,
ChipsView.OnChipClickListener {
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
return SheetFilterBinding.inflate(inflater, container, false)
@@ -50,12 +53,16 @@ class FilterSheetFragment :
filter.filterSortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
filter.filterLocale.observe(viewLifecycleOwner, this::onLocaleChanged)
filter.filterTags.observe(viewLifecycleOwner, this::onTagsChanged)
filter.filterTagsExcluded.observe(viewLifecycleOwner, this::onTagsExcludedChanged)
filter.filterState.observe(viewLifecycleOwner, this::onStateChanged)
filter.filterContentRating.observe(viewLifecycleOwner, this::onContentRatingChanged)
binding.spinnerLocale.onItemSelectedListener = this
binding.spinnerOrder.onItemSelectedListener = this
binding.chipsState.onChipClickListener = this
binding.chipsContentRating.onChipClickListener = this
binding.chipsGenres.onChipClickListener = this
binding.chipsGenresExclude.onChipClickListener = this
}
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
@@ -72,8 +79,14 @@ class FilterSheetFragment :
val filter = requireFilter()
when (data) {
is MangaState -> filter.setState(data, chip.isChecked)
is MangaTag -> filter.setTag(data, chip.isChecked)
null -> TagsCatalogSheet.show(childFragmentManager)
is MangaTag -> if (chip.parentView?.id == R.id.chips_genresExclude) {
filter.setTagExcluded(data, chip.isChecked)
} else {
filter.setTag(data, chip.isChecked)
}
is ContentRating -> filter.setContentRating(data, chip.isChecked)
null -> TagsCatalogSheet.show(childFragmentManager, chip.parentView?.id == R.id.chips_genresExclude)
}
}
@@ -166,6 +179,51 @@ class FilterSheetFragment :
b.chipsGenres.setChips(chips)
}
private fun onTagsExcludedChanged(value: FilterProperty<MangaTag>) {
val b = viewBinding ?: return
b.textViewGenresExcludeTitle.isGone = value.isEmpty()
b.chipsGenresExclude.isGone = value.isEmpty()
if (value.isEmpty()) {
return
}
val chips = ArrayList<ChipsView.ChipModel>(value.selectedItems.size + value.availableItems.size + 1)
value.selectedItems.mapTo(chips) { tag ->
ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = true,
data = tag,
)
}
value.availableItems.mapNotNullTo(chips) { tag ->
if (tag !in value.selectedItems) {
ChipsView.ChipModel(
tint = 0,
title = tag.title,
icon = 0,
isCheckable = true,
isChecked = false,
data = tag,
)
} else {
null
}
}
chips.add(
ChipsView.ChipModel(
tint = 0,
title = getString(R.string.more),
icon = materialR.drawable.abc_ic_menu_overflow_material,
isCheckable = false,
isChecked = false,
data = null,
),
)
b.chipsGenresExclude.setChips(chips)
}
private fun onStateChanged(value: FilterProperty<MangaState>) {
val b = viewBinding ?: return
b.textViewStateTitle.isGone = value.isEmpty()
@@ -186,6 +244,26 @@ class FilterSheetFragment :
b.chipsState.setChips(chips)
}
private fun onContentRatingChanged(value: FilterProperty<ContentRating>) {
val b = viewBinding ?: return
b.textViewContentRatingTitle.isGone = value.isEmpty()
b.chipsContentRating.isGone = value.isEmpty()
if (value.isEmpty()) {
return
}
val chips = value.availableItems.map { contentRating ->
ChipsView.ChipModel(
tint = 0,
title = getString(contentRating.titleResId),
icon = 0,
isCheckable = true,
isChecked = contentRating in value.selectedItems,
data = contentRating,
)
}
b.chipsContentRating.setChips(chips)
}
private fun requireFilter() = (requireActivity() as FilterOwner).filter
companion object {

View File

@@ -19,6 +19,7 @@ import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetTagsBinding
import org.koitharu.kotatsu.filter.ui.FilterOwner
import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
@@ -30,7 +31,10 @@ class TagsCatalogSheet : BaseAdaptiveSheet<SheetTagsBinding>(), OnListItemClickL
private val viewModel by viewModels<TagsCatalogViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<TagsCatalogViewModel.Factory> { factory ->
factory.create((requireActivity() as FilterOwner).filter)
factory.create(
filter = (requireActivity() as FilterOwner).filter,
isExcludeTag = requireArguments().getBoolean(ARG_EXCLUDE),
)
}
},
)
@@ -54,8 +58,7 @@ class TagsCatalogSheet : BaseAdaptiveSheet<SheetTagsBinding>(), OnListItemClickL
}
override fun onItemClick(item: TagCatalogItem, view: View) {
val filter = (requireActivity() as FilterOwner).filter
filter.setTag(item.tag, !item.isChecked)
viewModel.handleTagClick(item.tag, item.isChecked)
}
override fun onFocusChange(v: View?, hasFocus: Boolean) {
@@ -90,7 +93,10 @@ class TagsCatalogSheet : BaseAdaptiveSheet<SheetTagsBinding>(), OnListItemClickL
companion object {
private const val TAG = "TagsCatalogSheet"
private const val ARG_EXCLUDE = "exclude"
fun show(fm: FragmentManager) = TagsCatalogSheet().showDistinct(fm, TAG)
fun show(fm: FragmentManager, isExcludeTag: Boolean) = TagsCatalogSheet().withArgs(1) {
putBoolean(ARG_EXCLUDE, isExcludeTag)
}.showDistinct(fm, TAG)
}
}

View File

@@ -8,29 +8,32 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.filter.ui.MangaFilter
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
import org.koitharu.kotatsu.filter.ui.model.TagCatalogItem
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.MangaTag
@HiltViewModel(assistedFactory = TagsCatalogViewModel.Factory::class)
class TagsCatalogViewModel @AssistedInject constructor(
@Assisted filter: MangaFilter,
mangaRepositoryFactory: MangaRepository.Factory,
dataRepository: MangaDataRepository,
@Assisted private val filter: MangaFilter,
@Assisted private val isExcluded: Boolean,
) : BaseViewModel() {
val searchQuery = MutableStateFlow("")
private val filterProperty: StateFlow<FilterProperty<MangaTag>>
get() = if (isExcluded) filter.filterTagsExcluded else filter.filterTags
private val tags = combine(
filter.allTags,
filter.filterTags.map { it.selectedItems },
filterProperty.map { it.selectedItems },
) { all, selected ->
all.map { x ->
if (x is TagCatalogItem) {
@@ -52,9 +55,17 @@ class TagsCatalogViewModel @AssistedInject constructor(
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingState))
fun handleTagClick(tag: MangaTag, isChecked: Boolean) {
if (isExcluded) {
filter.setTagExcluded(tag, !isChecked)
} else {
filter.setTag(tag, !isChecked)
}
}
@AssistedFactory
interface Factory {
fun create(filter: MangaFilter): TagsCatalogViewModel
fun create(filter: MangaFilter, isExcludeTag: Boolean): TagsCatalogViewModel
}
}

View File

@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.local.data.input.LocalMangaInput
import org.koitharu.kotatsu.local.data.output.LocalMangaOutput
import org.koitharu.kotatsu.local.data.output.LocalMangaUtil
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
@@ -52,8 +53,11 @@ class LocalMangaRepository @Inject constructor(
private val locks = CompositeMutex2<Long>()
override val isMultipleTagsSupported: Boolean = true
override val isTagsExclusionSupported: Boolean = true
override val isSearchSupported: Boolean = true
override val sortOrders: Set<SortOrder> = EnumSet.of(SortOrder.ALPHABETICAL, SortOrder.RATING, SortOrder.NEWEST)
override val states = emptySet<MangaState>()
override val contentRatings = emptySet<ContentRating>()
override var defaultSortOrder: SortOrder
get() = settings.localListOrder
@@ -75,6 +79,9 @@ class LocalMangaRepository @Inject constructor(
if (filter.tags.isNotEmpty()) {
list.retainAll { x -> x.containsTags(filter.tags) }
}
if (filter.tagsExclude.isNotEmpty()) {
list.removeAll { x -> x.containsAnyTag(filter.tags) }
}
when (filter.sortOrder) {
SortOrder.ALPHABETICAL -> list.sortWith(compareBy(AlphanumComparator()) { x -> x.manga.title })
SortOrder.RATING -> list.sortByDescending { it.manga.rating }

View File

@@ -30,6 +30,12 @@ data class LocalManga(
return manga.tags.containsAll(tags)
}
fun containsAnyTag(tags: Set<MangaTag>): Boolean {
return tags.any { tag ->
manga.tags.contains(tag)
}
}
override fun toString(): String {
return "LocalManga(${file.path}: ${manga.title})"
}

View File

@@ -111,6 +111,10 @@ class MangaSearchRepository @Inject constructor(
}
}
suspend fun getRareTags(source: MangaSource, limit: Int): List<MangaTag> {
return db.getTagsDao().findRareTags(source.name, limit).toMangaTagsList()
}
fun getSourcesSuggestion(query: String, limit: Int): List<MangaSource> {
if (query.length < 3) {
return emptyList()

View File

@@ -211,12 +211,9 @@ class SuggestionsWorker @AssistedInject constructor(
}
val list = repository.getList(
offset = 0,
filter = MangaListFilter.Advanced(
sortOrder = order,
tags = setOfNotNull(tag),
locale = null,
states = setOf(),
),
filter = MangaListFilter.Advanced.Builder(order)
.tags(setOfNotNull(tag))
.build(),
).asArrayList()
if (appSettings.isSuggestionsExcludeNsfw) {
list.removeAll { it.isNsfw }

View File

@@ -126,6 +126,28 @@
tools:text="@string/error_multiple_genres_not_supported"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_genresExclude_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/genres_exclude"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_genresExclude"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_state_title"
android:layout_width="match_parent"
@@ -148,6 +170,28 @@
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_contentRating_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/margin_normal"
android:layout_marginTop="@dimen/margin_normal"
android:singleLine="true"
android:text="@string/content_rating"
android:textAppearance="?textAppearanceTitleSmall"
android:visibility="gone"
tools:visibility="visible" />
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
android:id="@+id/chips_contentRating"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_normal"
android:paddingHorizontal="@dimen/margin_normal"
android:visibility="gone"
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
tools:visibility="visible" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -550,4 +550,9 @@
<string name="backup_date_">Backup date: %s</string>
<string name="state_upcoming">Upcoming</string>
<string name="by_name_reverse">Name reversed</string>
<string name="content_rating">Content rating</string>
<string name="genres_exclude">Exclude genres</string>
<string name="rating_safe">Safe</string>
<string name="rating_suggestive">Suggestive</string>
<string name="rating_adult">Adult</string>
</resources>