Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/src/main/kotlin/org/draken/usagi/core/prefs/AppSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ class AppSettings
get() = prefs.getBoolean(KEY_DISABLE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_DISABLE_NSFW, value) }

/** Comma-separated tag titles; manga carrying any of them is hidden from the lists. */
val excludedTags: Set<String>
get() {
val string = prefs.getString(KEY_EXCLUDED_TAGS, null)?.trimEnd(' ', ',')
if (string.isNullOrEmpty()) {
return emptySet()
}
return string.split(',').mapNotNullToSet { it.trim().takeIf { title -> title.isNotEmpty() } }
}

var appLocales: LocaleListCompat
get() {
val raw = prefs.getString(KEY_APP_LOCALE, null)
Expand Down Expand Up @@ -872,6 +882,7 @@ class AppSettings
const val KEY_SUGGESTIONS_DISABLED_SOURCES = "suggestions_disabled_sources"
const val KEY_SUGGESTIONS_NOTIFICATIONS = "suggestions_notifications"
const val KEY_FIX_EXCLUDE_SOURCES = "fix_exclude_sources"
const val KEY_EXCLUDED_TAGS = "excluded_tags"
const val KEY_SHIKIMORI = "shikimori"
const val KEY_ANILIST = "anilist"
const val KEY_MAL = "mal"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ class RelatedListViewModel

override val content =
combine(
mangaList,
combine(mangaList, tagExclusion) { list, exclusion ->
list?.let(exclusion::filter)
},
observeListModeWithTriggers(),
listError,
) { list, mode, error ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.draken.usagi.core.util.ext.asArrayList
import org.draken.usagi.core.util.ext.printStackTraceDebug
import org.draken.usagi.explore.data.MangaSourcesRepository
import org.draken.usagi.history.data.HistoryRepository
import org.draken.usagi.list.domain.ExcludedTags
import org.draken.usagi.suggestions.domain.TagsBlacklist
import tsuki.model.Manga
import tsuki.model.MangaListFilter
Expand All @@ -25,20 +26,24 @@ class ExploreRepository
) {
suspend fun findRandomManga(tagsLimit: Int): Manga {
val tagsBlacklist = TagsBlacklist(settings.suggestionsTagsBlacklist, 0.4f)
val exclusion = ExcludedTags(settings.excludedTags)
val tags =
historyRepository.getPopularTags(tagsLimit).mapNotNull {
if (it in tagsBlacklist) null else it.title
}
val sources = sourcesRepository.getEnabledSources()
check(sources.isNotEmpty()) { "No sources available" }
for (i in 0..4) {
val list = getList(sources.random(), tags, tagsBlacklist)
val list = getList(sources.random(), tags, tagsBlacklist, exclusion)
val manga = list.randomOrNull() ?: continue
val details =
runCatchingCancellable {
mangaRepositoryFactory.create(manga.source).getDetails(manga)
}.getOrNull() ?: continue
if ((settings.isSuggestionsExcludeNsfw && details.isNsfw()) || details in tagsBlacklist) {
if ((settings.isSuggestionsExcludeNsfw && details.isNsfw()) ||
details in tagsBlacklist ||
details in exclusion
) {
continue
}
return details
Expand All @@ -51,19 +56,20 @@ class ExploreRepository
tagsLimit: Int,
): Manga {
val tagsBlacklist = TagsBlacklist(settings.suggestionsTagsBlacklist, 0.4f)
val exclusion = ExcludedTags(settings.excludedTags)
val skipNsfw = settings.isSuggestionsExcludeNsfw && !source.isNsfw()
val tags =
historyRepository.getPopularTags(tagsLimit).mapNotNull {
if (it in tagsBlacklist) null else it.title
}
for (i in 0..4) {
val list = getList(source, tags, tagsBlacklist)
val list = getList(source, tags, tagsBlacklist, exclusion)
val manga = list.randomOrNull() ?: continue
val details =
runCatchingCancellable {
mangaRepositoryFactory.create(manga.source).getDetails(manga)
}.getOrNull() ?: continue
if ((skipNsfw && details.isNsfw()) || details in tagsBlacklist) {
if ((skipNsfw && details.isNsfw()) || details in tagsBlacklist || details in exclusion) {
continue
}
return details
Expand All @@ -75,6 +81,7 @@ class ExploreRepository
source: MangaSource,
tags: List<String>,
blacklist: TagsBlacklist,
exclusion: ExcludedTags,
): List<Manga> =
runCatchingCancellable {
val repository = mangaRepositoryFactory.create(source)
Expand All @@ -97,6 +104,9 @@ class ExploreRepository
if (blacklist.isNotEmpty()) {
list.removeAll { manga -> manga in blacklist }
}
if (exclusion.isNotEmpty) {
list.removeAll { manga -> manga in exclusion }
}
list.shuffle()
list
}.onFailure {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.draken.usagi.list.domain

import tsuki.model.Manga

/**
* Client-side exclusion of manga that carry one of the tags picked in the
* "Exclude tags from all manga" setting. Matching is exact and case-insensitive.
*/
class ExcludedTags(tags: Set<String>) {
private val normalized = tags.mapTo(HashSet(tags.size)) { it.lowercase() }

val isNotEmpty: Boolean
get() = normalized.isNotEmpty()

operator fun contains(manga: Manga): Boolean =
normalized.isNotEmpty() && manga.tags.any { it.title.lowercase() in normalized }

fun filter(manga: List<Manga>): List<Manga> =
if (normalized.isEmpty()) {
manga
} else {
manga.filterNot { it in this }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.draken.usagi.core.prefs.observeAsStateFlow
import org.draken.usagi.core.ui.BaseViewModel
import org.draken.usagi.core.ui.util.ReversibleAction
import org.draken.usagi.core.util.ext.MutableEventFlow
import org.draken.usagi.list.domain.ExcludedTags
import org.draken.usagi.list.domain.ListFilterOption
import org.draken.usagi.list.ui.model.ListModel
import org.draken.usagi.local.data.LocalStorageChanges
Expand All @@ -45,6 +46,15 @@ abstract class MangaListViewModel(
valueProducer = { gridSize / 100f },
)

protected val tagExclusion =
settings
.observeAsFlow(AppSettings.KEY_EXCLUDED_TAGS) { ExcludedTags(this.excludedTags) }
.stateIn(
viewModelScope + Dispatchers.Default,
SharingStarted.Eagerly,
ExcludedTags(settings.excludedTags),
)

val isIncognitoModeEnabled: Boolean
get() = settings.isIncognitoModeEnabled

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
Expand Down Expand Up @@ -80,7 +79,9 @@ open class RemoteListViewModel

override val content =
combine(
mangaList.map { it?.skipNsfwIfNeeded() },
combine(mangaList, tagExclusion) { list, exclusion ->
list?.let { exclusion.filter(it).skipNsfwIfNeeded() }
},
observeListModeWithTriggers(),
listError,
hasNextPage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.draken.usagi.core.parser.MangaRepository
import org.draken.usagi.core.prefs.AppSettings
import org.draken.usagi.core.util.ext.contains
import org.draken.usagi.core.util.ext.printStackTraceDebug
import org.draken.usagi.list.domain.ExcludedTags
import tsuki.model.Manga
import tsuki.model.MangaListFilter
import tsuki.model.MangaSource
Expand Down Expand Up @@ -96,6 +97,10 @@ class SearchV2Helper
if (settings.isNsfwContentDisabled) {
removeAll { it.isNsfw() }
}
val exclusion = ExcludedTags(settings.excludedTags)
if (exclusion.isNotEmpty) {
removeAll { it in exclusion }
}
when (kind) {
SearchKind.TITLE -> {
retainAll { m ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.draken.usagi.core.util.ext.setDefaultValueCompat
import org.draken.usagi.explore.data.MangaSourcesRepository
import org.draken.usagi.explore.data.SourcesSortOrder
import org.draken.usagi.settings.utils.MultiAutoCompleteTextViewPreference
import org.draken.usagi.settings.utils.TagsAutoCompleteProvider
import tsuki.util.names
import javax.inject.Inject

Expand All @@ -36,6 +37,9 @@ class SourcesSettingsFragment :
@Inject
lateinit var sourcesRepository: MangaSourcesRepository

@Inject
lateinit var tagsCompletionProvider: TagsAutoCompleteProvider

override fun onCreatePreferences(
savedInstanceState: Bundle?,
rootKey: String?,
Expand Down Expand Up @@ -63,6 +67,13 @@ class SourcesSettingsFragment :
getString(R.string.fix_excluded_sources_summary),
)
}
findPreference<MultiAutoCompleteTextViewPreference>(AppSettings.KEY_EXCLUDED_TAGS)?.run {
autoCompleteProvider = tagsCompletionProvider
summaryProvider =
MultiAutoCompleteTextViewPreference.SimpleSummaryProvider(
getString(R.string.excluded_tags_empty),
)
}
}

override fun onViewCreated(
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,9 @@
<string name="suggestions_disabled_sources_summary">Show suggestions from all manga sources, including disabled ones</string>
<string name="tags_warnings">Highlight dangerous genres</string>
<string name="tags_warnings_summary">Highlight genres that may be inappropriate for most users</string>
<string name="excluded_tags">Exclude tags from all manga</string>
<string name="excluded_tags_summary">Manga containing any of these tags will not be shown in source lists, search results and related manga</string>
<string name="excluded_tags_empty">No tags excluded</string>
<string name="error_non_file_uri">The selected path cannot be used because it does not denote a file or directory</string>
<string name="manga_override_hint">These changes will affect how manga is displayed in the app</string>
<string name="use_default_cover">Use default cover</string>
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/xml/pref_sources.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
android:summary="@string/disable_nsfw_summary"
android:title="@string/disable_nsfw" />

<org.draken.usagi.settings.utils.MultiAutoCompleteTextViewPreference
android:key="excluded_tags"
android:summary="@string/excluded_tags_summary"
android:title="@string/excluded_tags" />

<SwitchPreferenceCompat
android:defaultValue="false"
android:key="sources_hide_broken"
Expand Down
Loading