Skip to content
Open
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
2 changes: 1 addition & 1 deletion app/src/main/baselineProfiles/baseline-prof.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21730,7 +21730,7 @@ HSPLeu/kanade/domain/source/service/SourcePreferences;->getLastUsedSource()Ltach
HSPLeu/kanade/domain/source/service/SourcePreferences;->getMigrationSortingDirection()Ltachiyomi/core/common/preference/Preference;
HSPLeu/kanade/domain/source/service/SourcePreferences;->getMigrationSortingMode()Ltachiyomi/core/common/preference/Preference;
HSPLeu/kanade/domain/source/service/SourcePreferences;->getPinnedSources()Ltachiyomi/core/common/preference/Preference;
HSPLeu/kanade/domain/source/service/SourcePreferences;->getShowNsfwSource()Ltachiyomi/core/common/preference/Preference;
HSPLeu/kanade/domain/source/service/SourcePreferences;->getContentWarningLevel()Ltachiyomi/core/common/preference/Preference;
Leu/kanade/domain/source/service/SourcePreferences$$ExternalSyntheticLambda0;
HSPLeu/kanade/domain/source/service/SourcePreferences$$ExternalSyntheticLambda0;-><init>()V
Leu/kanade/domain/source/service/SourcePreferences$$ExternalSyntheticLambda1;
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/eu/kanade/domain/DomainModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class DomainModule : InjektModule {

addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
addSingletonFactory<StubSourceRepository> { StubSourceRepositoryImpl(get()) }
addFactory { GetEnabledSources(get(), get()) }
addFactory { GetEnabledSources(get(), get(), get()) }
addFactory { GetLanguagesWithSources(get(), get()) }
addFactory { GetRemoteManga(get()) }
addFactory { GetSourcesWithFavoriteCount(get(), get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ class GetExtensionsByType(
) {

fun subscribe(): Flow<Extensions> {
val showNsfwSources = preferences.showNsfwSource.get()
val contentWarningLevel = preferences.contentWarningLevel.get()

return combine(
preferences.enabledLanguages.changes(),
extensionManager.installedExtensionsFlow,
extensionManager.untrustedExtensionsFlow,
extensionManager.availableExtensionsFlow,
) { enabledLanguages, _installed, _untrusted, _available ->
// Installed extensions stay listed regardless of the content warning level, except
// under the strictest (Safe only) level — see ContentWarningLevel.allowsInstalled.
val (updates, installed) = _installed
.filter { (showNsfwSources || !it.isNsfw) }
.filter { contentWarningLevel.allowsInstalled(it.contentWarning) }
.sortedWith(
compareBy<Extension.Installed> { !it.isObsolete }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
Expand All @@ -36,7 +38,7 @@ class GetExtensionsByType(
.filter { extension ->
_installed.none { it.pkgName == extension.pkgName } &&
_untrusted.none { it.pkgName == extension.pkgName } &&
(showNsfwSources || !extension.isNsfw)
contentWarningLevel.allowsDiscovery(extension.contentWarning)
}
.flatMap { ext ->
ext.sources.filter { it.lang in enabledLanguages }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eu.kanade.domain.source.interactor

import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
Expand All @@ -13,19 +14,36 @@ import tachiyomi.source.local.isLocal
class GetEnabledSources(
private val repository: SourceRepository,
private val preferences: SourcePreferences,
private val extensionManager: ExtensionManager,
) {

fun subscribe(): Flow<List<Source>> {
val sourcesWithContentWarnings = combine(
repository.getSources(),
extensionManager.installedExtensionsFlow,
) { sources, installedExtensions ->
val contentWarningBySourceId = installedExtensions
.flatMap { extension -> extension.sources.map { it.id to extension.contentWarning } }
.toMap()
sources to contentWarningBySourceId
}

return combine(
preferences.pinnedSources.changes(),
preferences.enabledLanguages.changes(),
preferences.disabledSources.changes(),
preferences.lastUsedSource.changes(),
repository.getSources(),
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
sourcesWithContentWarnings,
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, (sources, contentWarningBySourceId) ->
val contentWarningLevel = preferences.contentWarningLevel.get()

sources
.filter { it.lang in enabledLanguages || it.isLocal() }
.filterNot { it.id.toString() in disabledSources }
.filter { source ->
val contentWarning = contentWarningBySourceId[source.id] ?: return@filter true
contentWarningLevel.allowsInstalled(contentWarning)
}
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
.flatMap {
val flag = if ("${it.id}" in pinnedSourceIds) Pins.pinned else Pins.unpinned
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package eu.kanade.domain.source.model

import dev.icerock.moko.resources.StringResource
import eu.kanade.tachiyomi.extension.model.ContentWarning
import tachiyomi.i18n.MR

/**
* User-selectable cutoff for which [ContentWarning] tiers are surfaced when discovering new
* extensions/sources in Browse, and for which already-installed ones stay listed — see
* `GetExtensionsByType`.
*
* Entries are declared least to most permissive; [allowsDiscovery] treats that ordering as
* cumulative.
*/
enum class ContentWarningLevel(val titleRes: StringResource) {
SAFE(MR.strings.content_warning_level_safe),
SAFE_AND_MIXED(MR.strings.content_warning_level_safe_and_mixed),
ALL(MR.strings.content_warning_level_all),
;

/** Whether a not-yet-installed extension/source of this [contentWarning] shows up in Browse. */
fun allowsDiscovery(contentWarning: ContentWarning): Boolean {
return when (this) {
SAFE -> !contentWarning.hasAdultContent
SAFE_AND_MIXED -> contentWarning != ContentWarning.NSFW
ALL -> true
}
}

/**
* Whether an already-installed extension of this [contentWarning] stays listed.
*
* Only the strictest level, [SAFE], also hides installed extensions — [SAFE_AND_MIXED] and
* [ALL] never hide something the user explicitly installed, so they keep receiving updates
* and don't regress the fix for https://github.com/mihonapp/mihon/issues/1673.
*/
fun allowsInstalled(contentWarning: ContentWarning): Boolean {
return this != SAFE || !contentWarning.hasAdultContent
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package eu.kanade.domain.source.service

import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.ContentWarningLevel
import eu.kanade.tachiyomi.util.system.LocaleHelper
import mihon.domain.migration.models.MigrationFlag
import tachiyomi.core.common.preference.Preference
Expand Down Expand Up @@ -36,7 +37,10 @@ class SourcePreferences(
-1,
)

val showNsfwSource: Preference<Boolean> = preferenceStore.getBoolean("show_nsfw_source", true)
val contentWarningLevel: Preference<ContentWarningLevel> = preferenceStore.getEnum(
"content_warning_level",
ContentWarningLevel.SAFE_AND_MIXED,
)

val migrationSortingMode: Preference<SetMigrateSorting.Mode> = preferenceStore.getEnum(
"pref_migration_sorting",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.ContentWarning
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsViewModel
Expand Down Expand Up @@ -152,7 +153,7 @@ private fun ExtensionDetails(
onClickIncognito: (Boolean) -> Unit,
) {
val context = LocalContext.current
var showNsfwWarning by remember { mutableStateOf(false) }
var showContentWarning by remember { mutableStateOf(false) }

ScrollbarLazyColumn(
contentPadding = contentPadding,
Expand All @@ -176,7 +177,7 @@ private fun ExtensionDetails(
Unit
}.takeIf { extension.isShared },
onClickAgeRating = {
showNsfwWarning = true
showContentWarning = true
},
onExtIncognitoChange = onClickIncognito,
)
Expand All @@ -194,10 +195,11 @@ private fun ExtensionDetails(
)
}
}
if (showNsfwWarning) {
NsfwWarningDialog(
if (showContentWarning) {
ContentWarningDialog(
contentWarning = extension.contentWarning,
onClickConfirm = {
showNsfwWarning = false
showContentWarning = false
},
)
}
Expand Down Expand Up @@ -229,7 +231,7 @@ private fun DetailsHeader(
"""
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
NSFW: ${extension.isNsfw}
Content warning: ${extension.contentWarning}
""".trimIndent(),
)

Expand Down Expand Up @@ -292,17 +294,23 @@ private fun DetailsHeader(
InfoDivider()

InfoText(
modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
modifier = Modifier.weight(if (extension.contentWarning.hasAdultContent) 1.5f else 1f),
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
secondaryText = stringResource(MR.strings.ext_info_language),
)

if (extension.isNsfw) {
if (extension.contentWarning.hasAdultContent) {
InfoDivider()

InfoText(
modifier = Modifier.weight(1f),
primaryText = stringResource(MR.strings.ext_nsfw_short),
primaryText = stringResource(
if (extension.contentWarning == ContentWarning.MIXED) {
MR.strings.ext_mixed_short
} else {
MR.strings.ext_nsfw_short
},
),
primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Medium,
Expand Down Expand Up @@ -444,12 +452,21 @@ private fun SourceSwitchPreference(
}

@Composable
private fun NsfwWarningDialog(
private fun ContentWarningDialog(
contentWarning: ContentWarning,
onClickConfirm: () -> Unit,
) {
AlertDialog(
text = {
Text(text = stringResource(MR.strings.ext_nsfw_warning))
Text(
text = stringResource(
if (contentWarning == ContentWarning.MIXED) {
MR.strings.ext_mixed_warning
} else {
MR.strings.ext_nsfw_warning
},
),
)
},
confirmButton = {
TextButton(onClick = onClickConfirm) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import eu.kanade.presentation.components.WarningBanner
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.extension.model.ContentWarning
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
Expand Down Expand Up @@ -370,7 +371,8 @@ private fun ExtensionItemContent(
val warning = when {
extension is Extension.Untrusted -> MR.strings.ext_untrusted
extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
extension.isNsfw -> MR.strings.ext_nsfw_short
extension.contentWarning == ContentWarning.NSFW -> MR.strings.ext_nsfw_short
extension.contentWarning == ContentWarning.MIXED -> MR.strings.ext_mixed_short
else -> null
}
if (warning != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.model.ContentWarningLevel
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import eu.kanade.tachiyomi.util.system.toast
import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
Expand Down Expand Up @@ -57,14 +59,17 @@ object SettingsBrowseScreen : SearchableSettings {
Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_category_nsfw_content),
preferenceItems = listOf(
Preference.PreferenceItem.SwitchPreference(
preference = sourcePreferences.showNsfwSource,
Preference.PreferenceItem.ListPreference(
preference = sourcePreferences.contentWarningLevel,
entries = ContentWarningLevel.entries
.associateWith { stringResource(it.titleRes) },
title = stringResource(MR.strings.pref_show_nsfw_source),
subtitle = stringResource(MR.strings.requires_app_restart),
onValueChanged = {
(context as FragmentActivity).authenticate(
val authenticated = (context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.pref_category_nsfw_content),
)
if (authenticated) context.toast(MR.strings.requires_app_restart)
authenticated
},
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.parental_controls_info)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import eu.kanade.domain.extension.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.model.ContentWarning
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.source.Source
Expand Down Expand Up @@ -39,11 +39,7 @@ import java.io.File
*/
internal object ExtensionLoader {

private val preferences: SourcePreferences by injectLazy()
private val trustExtension: TrustExtension by injectLazy()
private val loadNsfwSource by lazy {
preferences.showNsfwSource.get()
}

private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
Expand Down Expand Up @@ -272,11 +268,22 @@ internal object ExtensionLoader {
return LoadResult.Untrusted(extension)
}

val isNsfw = appInfo.metaData.getInt(METADATA_CONTENT_WARNING) > 0 ||
appInfo.metaData.getInt(METADATA_NSFW) == 1
if (!loadNsfwSource && isNsfw) {
logcat(LogPriority.WARN) { "NSFW extension $pkgName not allowed" }
return LoadResult.Error
// Adult extensions are always loaded here because this only runs for packages already
// installed on the device. The NSFW preference only gates discovery of new extensions
// (see GetExtensionsByType), so installed ones stay usable and keep receiving updates.
//
// `tachiyomix.contentWarning` keeps the numbering it was introduced with; the
// `CONTENT_WARNING_UNSPECIFIED` value later added to the store index format did not shift
// it. Absent metadata reads as -1 so the legacy flag still decides in that case.
val contentWarning = if (appInfo.metaData.getInt(METADATA_NSFW) == 1) {
ContentWarning.NSFW
} else {
when (appInfo.metaData.getInt(METADATA_CONTENT_WARNING, -1)) {
0 -> ContentWarning.SAFE
1 -> ContentWarning.MIXED
2 -> ContentWarning.NSFW
else -> ContentWarning.UNSPECIFIED
}
}
Comment on lines +278 to 287

val classLoader = try {
Expand Down Expand Up @@ -323,7 +330,7 @@ internal object ExtensionLoader {
versionCode = versionCode,
libVersion = libVersion,
lang = lang,
isNsfw = isNsfw,
contentWarning = contentWarning,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
icon = appInfo.loadIcon(pkgManager),
Expand Down
Loading