Filter to show NSFW sources/extensions only in Browse tab
This commit is contained in:
parent
e028fc3fb9
commit
861e43ff06
5 changed files with 84 additions and 7 deletions
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()) },
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue