Filter to show NSFW sources/extensions only in Browse tab

This commit is contained in:
Cuong M. Tran 2024-02-04 17:06:20 +07:00
parent e028fc3fb9
commit 861e43ff06
5 changed files with 84 additions and 7 deletions

View file

@ -49,8 +49,11 @@ class ExtensionsScreenModel(
ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle) ExtensionUiModel.Item(it, map[it.pkgName] ?: InstallStep.Idle)
} }
} }
val queryFilter: (String) -> ((Extension) -> Boolean) = { query -> val queryFilter: (String/* KMK --> */, Boolean/* KMK <-- */) -> ((Extension) -> Boolean) = { query/* KMK --> */, nsfwOnly/* KMK <-- */ ->
filter@{ extension -> filter@{ extension ->
// KMK -->
if (nsfwOnly && !extension.isNsfw) return@filter false
// KMK <--
if (query.isEmpty()) return@filter true if (query.isEmpty()) return@filter true
query.split(",").any { _input -> query.split(",").any { _input ->
val input = _input.trim() val input = _input.trim()
@ -79,26 +82,29 @@ class ExtensionsScreenModel(
screenModelScope.launchIO { screenModelScope.launchIO {
combine( combine(
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
// KMK -->
state.map { it.nsfwOnly }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
// KMK <--
_currentDownloads, _currentDownloads,
getExtensions.subscribe(), getExtensions.subscribe(),
) { query, downloads, (_updates, _installed, _available, _untrusted) -> ) { query/* KMK --> */, nsfwOnly/* KMK <-- */, downloads, (_updates, _installed, _available, _untrusted) ->
val searchQuery = query ?: "" val searchQuery = query ?: ""
val itemsGroups: ItemGroups = mutableMapOf() val itemsGroups: ItemGroups = mutableMapOf()
val updates = _updates.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) val updates = _updates.filter(queryFilter(searchQuery/* KMK --> */, nsfwOnly/* KMK <-- */)).map(extensionMapper(downloads))
if (updates.isNotEmpty()) { if (updates.isNotEmpty()) {
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending)] = updates itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_updates_pending)] = updates
} }
val installed = _installed.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) val installed = _installed.filter(queryFilter(searchQuery/* KMK --> */, nsfwOnly/* KMK <-- */)).map(extensionMapper(downloads))
val untrusted = _untrusted.filter(queryFilter(searchQuery)).map(extensionMapper(downloads)) val untrusted = _untrusted.filter(queryFilter(searchQuery/* KMK --> */, nsfwOnly/* KMK <-- */)).map(extensionMapper(downloads))
if (installed.isNotEmpty() || untrusted.isNotEmpty()) { if (installed.isNotEmpty() || untrusted.isNotEmpty()) {
itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_installed)] = untrusted + installed itemsGroups[ExtensionUiModel.Header.Resource(MR.strings.ext_installed)] = untrusted + installed
} }
val languagesWithExtensions = _available val languagesWithExtensions = _available
.filter(queryFilter(searchQuery)) .filter(queryFilter(searchQuery/* KMK --> */, nsfwOnly/* KMK <-- */))
.groupBy { it.lang } .groupBy { it.lang }
.toSortedMap(LocaleHelper.comparator) .toSortedMap(LocaleHelper.comparator)
.map { (lang, exts) -> .map { (lang, exts) ->
@ -198,6 +204,14 @@ class ExtensionsScreenModel(
extensionManager.trust(extension) extensionManager.trust(extension)
} }
// KMK -->
fun toggleNsfwOnly() {
mutableState.update {
it.copy(nsfwOnly = !it.nsfwOnly)
}
}
// KMK <--
@Immutable @Immutable
data class State( data class State(
val isLoading: Boolean = true, val isLoading: Boolean = true,
@ -206,6 +220,9 @@ class ExtensionsScreenModel(
val updates: Int = 0, val updates: Int = 0,
val installer: BasePreferences.ExtensionInstaller? = null, val installer: BasePreferences.ExtensionInstaller? = null,
val searchQuery: String? = null, val searchQuery: String? = null,
// KMK -->
val nsfwOnly: Boolean = false,
// KMK <--
) { ) {
val isEmpty = items.isEmpty() val isEmpty = items.isEmpty()
} }

View file

@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.ui.browse.extension package eu.kanade.tachiyomi.ui.browse.extension
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined._18UpRating
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -14,6 +18,7 @@ import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen
import eu.kanade.tachiyomi.ui.webview.WebViewScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
@Composable @Composable
@ -28,6 +33,14 @@ fun extensionsTab(
badgeNumber = state.updates.takeIf { it > 0 }, badgeNumber = state.updates.takeIf { it > 0 },
searchEnabled = true, searchEnabled = true,
actions = persistentListOf( actions = persistentListOf(
// KMK -->
AppBar.Action(
title = stringResource(SYMR.strings.label_extension_nsfw),
icon = Icons.Outlined._18UpRating,
iconTint = if (state.nsfwOnly) MaterialTheme.colorScheme.error else LocalContentColor.current,
onClick = { extensionsScreenModel.toggleNsfwOnly() },
),
// KMK <--
AppBar.OverflowAction( AppBar.OverflowAction(
title = stringResource(MR.strings.action_filter), title = stringResource(MR.strings.action_filter),
onClick = { navigator.push(ExtensionFilterScreen()) }, onClick = { navigator.push(ExtensionFilterScreen()) },

View file

@ -12,6 +12,7 @@ import eu.kanade.domain.source.interactor.SetSourceCategories
import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver import eu.kanade.domain.source.interactor.ToggleExcludeFromDataSaver
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.source.model.installedExtension
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.source.service.SourcePreferences.DataSaver import eu.kanade.domain.source.service.SourcePreferences.DataSaver
import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.UiPreferences
@ -22,6 +23,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@ -66,6 +68,7 @@ class SourcesScreenModel(
combine( combine(
// KMK --> // KMK -->
state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS), state.map { it.searchQuery }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
state.map { it.nsfwOnly }.distinctUntilChanged().debounce(SEARCH_DEBOUNCE_MILLIS),
// KMK <-- // KMK <--
getEnabledSources.subscribe(), getEnabledSources.subscribe(),
getSourceCategories.subscribe(), getSourceCategories.subscribe(),
@ -95,13 +98,17 @@ class SourcesScreenModel(
private fun collectLatestSources( private fun collectLatestSources(
// KMK --> // KMK -->
searchQuery: String?, searchQuery: String?,
nsfwOnly: Boolean,
@Suppress("LocalVariableName") _sources: List<Source>, @Suppress("LocalVariableName") _sources: List<Source>,
// KMK <-- // KMK <--
categories: List<String>, showLatest: Boolean, showPin: Boolean categories: List<String>,
showLatest: Boolean,
showPin: Boolean
) { ) {
// KMK --> // KMK -->
val queryFilter: (String?) -> ((Source) -> Boolean) = { query -> val queryFilter: (String?) -> ((Source) -> Boolean) = { query ->
filter@{ source -> filter@{ source ->
if (nsfwOnly && source.installedExtension != null && !source.installedExtension!!.isNsfw) return@filter false
if (query.isNullOrBlank()) return@filter true if (query.isNullOrBlank()) return@filter true
query.split(",").any { query.split(",").any {
val input = it.trim() val input = it.trim()
@ -192,6 +199,32 @@ class SourcesScreenModel(
it.copy(searchQuery = query) it.copy(searchQuery = query)
} }
} }
fun toggleNsfwOnly() {
mutableState.update {
it.copy(nsfwOnly = !it.nsfwOnly)
}
}
private fun <T1, T2, T3, T4, T5, T6, R> combine(
flow: Flow<T1>,
flow2: Flow<T2>,
flow3: Flow<T3>,
flow4: Flow<T4>,
flow5: Flow<T5>,
flow6: Flow<T6>,
transform: suspend (T1, T2, T3, T4, T5, T6) -> R
): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> ->
@Suppress("UNCHECKED_CAST")
transform(
args[0] as T1,
args[1] as T2,
args[2] as T3,
args[3] as T4,
args[4] as T5,
args[5] as T6,
)
}
// KMK <-- // KMK <--
fun showSourceDialog(source: Source) { fun showSourceDialog(source: Source) {
@ -224,6 +257,7 @@ class SourcesScreenModel(
// SY <-- // SY <--
// KMK --> // KMK -->
val searchQuery: String? = null, val searchQuery: String? = null,
val nsfwOnly: Boolean = false,
// KMK <-- // KMK <--
) { ) {
val isEmpty = items.isEmpty() val isEmpty = items.isEmpty()

View file

@ -3,6 +3,9 @@ package eu.kanade.tachiyomi.ui.browse.source
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FilterList import androidx.compose.material.icons.outlined.FilterList
import androidx.compose.material.icons.outlined.TravelExplore import androidx.compose.material.icons.outlined.TravelExplore
import androidx.compose.material.icons.outlined._18UpRating
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -50,6 +53,14 @@ fun Screen.sourcesTab(
icon = Icons.Outlined.TravelExplore, icon = Icons.Outlined.TravelExplore,
onClick = { navigator.push(GlobalSearchScreen()) }, onClick = { navigator.push(GlobalSearchScreen()) },
), ),
// KMK -->
AppBar.Action(
title = stringResource(SYMR.strings.label_source_nsfw),
icon = Icons.Outlined._18UpRating,
iconTint = if (state.nsfwOnly) MaterialTheme.colorScheme.error else LocalContentColor.current,
onClick = { screenModel.toggleNsfwOnly() },
),
// KMK <--
AppBar.Action( AppBar.Action(
title = stringResource(MR.strings.action_filter), title = stringResource(MR.strings.action_filter),
icon = Icons.Outlined.FilterList, icon = Icons.Outlined.FilterList,

View file

@ -350,6 +350,7 @@
<string name="find_in_another_source">Find in another source</string> <string name="find_in_another_source">Find in another source</string>
<string name="data_saver_exclude">Exclude from data saver</string> <string name="data_saver_exclude">Exclude from data saver</string>
<string name="data_saver_stop_exclude">Stop excluding from data saver</string> <string name="data_saver_stop_exclude">Stop excluding from data saver</string>
<string name="label_source_nsfw">Toggle NSFW only</string>
<!-- Smart Search --> <!-- Smart Search -->
<string name="searching_source">Searching source…</string> <string name="searching_source">Searching source…</string>
@ -391,6 +392,7 @@
<!-- Extension section --> <!-- Extension section -->
<string name="ext_redundant">Redundant</string> <string name="ext_redundant">Redundant</string>
<string name="redundant_extension_message">This extension is redundant and will not be used inside this version of Tachiyomi.</string> <string name="redundant_extension_message">This extension is redundant and will not be used inside this version of Tachiyomi.</string>
<string name="label_extension_nsfw">Toggle NSFW only</string>
<!-- Migration --> <!-- Migration -->
<string name="select_sources">Select sources</string> <string name="select_sources">Select sources</string>