Replace extension NSFW flag with content warning app-wide (#3951)
This commit is contained in:
@@ -33,7 +33,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.mihon"
|
applicationId = "app.mihon"
|
||||||
|
|
||||||
versionCode = 29
|
versionCode = 30
|
||||||
versionName = "0.20.4"
|
versionName = "0.20.4"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getLatestCommitCount()}\"")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class GetExtensionsByType(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<Extensions> {
|
fun subscribe(): Flow<Extensions> {
|
||||||
val showNsfwSources = preferences.showNsfwSource.get()
|
val enabledContentWarnings = preferences.enabledContentWarnings.get()
|
||||||
|
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages.changes(),
|
preferences.enabledLanguages.changes(),
|
||||||
@@ -24,7 +24,6 @@ class GetExtensionsByType(
|
|||||||
extensionManager.availableExtensionsFlow,
|
extensionManager.availableExtensionsFlow,
|
||||||
) { enabledLanguages, _installed, _untrusted, _available ->
|
) { enabledLanguages, _installed, _untrusted, _available ->
|
||||||
val (updates, installed) = _installed
|
val (updates, installed) = _installed
|
||||||
.filter { (showNsfwSources || !it.isNsfw) }
|
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy<Extension.Installed> { !it.isObsolete }
|
compareBy<Extension.Installed> { !it.isObsolete }
|
||||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||||
@@ -38,7 +37,7 @@ class GetExtensionsByType(
|
|||||||
.filter { extension ->
|
.filter { extension ->
|
||||||
_installed.none { it.pkgName == extension.pkgName } &&
|
_installed.none { it.pkgName == extension.pkgName } &&
|
||||||
_untrusted.none { it.pkgName == extension.pkgName } &&
|
_untrusted.none { it.pkgName == extension.pkgName } &&
|
||||||
(showNsfwSources || !extension.isNsfw)
|
extension.contentWarning in enabledContentWarnings
|
||||||
}
|
}
|
||||||
.flatMap { ext ->
|
.flatMap { ext ->
|
||||||
ext.sources.filter { it.lang in enabledLanguages }
|
ext.sources.filter { it.lang in enabledLanguages }
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import dev.zacsweers.metro.Inject
|
|||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import mihon.domain.extension.model.ContentWarning
|
||||||
import mihon.domain.migration.models.MigrationFlag
|
import mihon.domain.migration.models.MigrationFlag
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.preference.getEnum
|
import tachiyomi.core.common.preference.getEnum
|
||||||
|
import tachiyomi.core.common.preference.getEnumSet
|
||||||
import tachiyomi.core.common.preference.getLongArray
|
import tachiyomi.core.common.preference.getLongArray
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
|
|
||||||
@@ -41,7 +43,10 @@ class SourcePreferences(
|
|||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
|
|
||||||
val showNsfwSource: Preference<Boolean> = preferenceStore.getBoolean("show_nsfw_source", true)
|
val enabledContentWarnings: Preference<Set<ContentWarning>> = preferenceStore.getEnumSet(
|
||||||
|
"enabled_content_warnings",
|
||||||
|
setOf(ContentWarning.SAFE, ContentWarning.MIXED, ContentWarning.NSFW),
|
||||||
|
)
|
||||||
|
|
||||||
val migrationSortingMode: Preference<SetMigrateSorting.Mode> = preferenceStore.getEnum(
|
val migrationSortingMode: Preference<SetMigrateSorting.Mode> = preferenceStore.getEnum(
|
||||||
"pref_migration_sorting",
|
"pref_migration_sorting",
|
||||||
|
|||||||
@@ -32,16 +32,18 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
|
import eu.kanade.presentation.browse.components.label
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
@@ -152,7 +154,8 @@ private fun ExtensionDetails(
|
|||||||
onClickIncognito: (Boolean) -> Unit,
|
onClickIncognito: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
var showContentWarning by remember { mutableStateOf(false) }
|
||||||
|
val contentWarning = extension.contentWarning.label
|
||||||
|
|
||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
@@ -175,8 +178,8 @@ private fun ExtensionDetails(
|
|||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
}.takeIf { extension.isShared },
|
}.takeIf { extension.isShared },
|
||||||
onClickAgeRating = {
|
onClickContentWarning = {
|
||||||
showNsfwWarning = true
|
showContentWarning = true
|
||||||
},
|
},
|
||||||
onExtIncognitoChange = onClickIncognito,
|
onExtIncognitoChange = onClickIncognito,
|
||||||
)
|
)
|
||||||
@@ -194,10 +197,12 @@ private fun ExtensionDetails(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showNsfwWarning) {
|
if (showContentWarning && contentWarning != null) {
|
||||||
NsfwWarningDialog(
|
ContentWarningDialog(
|
||||||
|
label = contentWarning.title,
|
||||||
|
description = contentWarning.description,
|
||||||
onClickConfirm = {
|
onClickConfirm = {
|
||||||
showNsfwWarning = false
|
showContentWarning = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -207,12 +212,13 @@ private fun ExtensionDetails(
|
|||||||
private fun DetailsHeader(
|
private fun DetailsHeader(
|
||||||
extension: Extension,
|
extension: Extension,
|
||||||
extIncognitoMode: Boolean,
|
extIncognitoMode: Boolean,
|
||||||
onClickAgeRating: () -> Unit,
|
onClickContentWarning: () -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickAppInfo: (() -> Unit)?,
|
onClickAppInfo: (() -> Unit)?,
|
||||||
onExtIncognitoChange: (Boolean) -> Unit,
|
onExtIncognitoChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val contentWarning = extension.contentWarning.label
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Column(
|
Column(
|
||||||
@@ -229,7 +235,7 @@ private fun DetailsHeader(
|
|||||||
"""
|
"""
|
||||||
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
|
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
|
||||||
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
|
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
|
||||||
NSFW: ${extension.isNsfw}
|
Content warning: ${extension.contentWarning}
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -277,7 +283,7 @@ private fun DetailsHeader(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = MaterialTheme.padding.extraLarge,
|
horizontal = MaterialTheme.padding.medium,
|
||||||
vertical = MaterialTheme.padding.small,
|
vertical = MaterialTheme.padding.small,
|
||||||
),
|
),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
@@ -292,23 +298,20 @@ private fun DetailsHeader(
|
|||||||
InfoDivider()
|
InfoDivider()
|
||||||
|
|
||||||
InfoText(
|
InfoText(
|
||||||
modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
|
modifier = Modifier.weight(1f),
|
||||||
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
||||||
secondaryText = stringResource(MR.strings.ext_info_language),
|
secondaryText = stringResource(MR.strings.ext_info_language),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (extension.isNsfw) {
|
if (contentWarning != null) {
|
||||||
InfoDivider()
|
InfoDivider()
|
||||||
|
|
||||||
InfoText(
|
InfoText(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
primaryText = stringResource(MR.strings.ext_nsfw_short),
|
primaryText = stringResource(contentWarning.title),
|
||||||
primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
|
primaryTextColor = contentWarning.color,
|
||||||
color = MaterialTheme.colorScheme.error,
|
secondaryText = stringResource(MR.strings.ext_info_warning),
|
||||||
fontWeight = FontWeight.Medium,
|
onClick = onClickContentWarning,
|
||||||
),
|
|
||||||
secondaryText = stringResource(MR.strings.ext_info_age_rating),
|
|
||||||
onClick = onClickAgeRating,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,7 +369,7 @@ private fun InfoText(
|
|||||||
primaryText: String,
|
primaryText: String,
|
||||||
secondaryText: String,
|
secondaryText: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
primaryTextColor: Color = Color.Unspecified,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val clickableModifier = if (onClick != null) {
|
val clickableModifier = if (onClick != null) {
|
||||||
@@ -383,13 +386,16 @@ private fun InfoText(
|
|||||||
Text(
|
Text(
|
||||||
text = primaryText,
|
text = primaryText,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = primaryTextStyle,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = primaryTextColor,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = secondaryText + if (onClick != null) " ⓘ" else "",
|
text = secondaryText + if (onClick != null) " ⓘ" else "",
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -398,7 +404,9 @@ private fun InfoText(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun InfoDivider() {
|
private fun InfoDivider() {
|
||||||
VerticalDivider(
|
VerticalDivider(
|
||||||
modifier = Modifier.height(20.dp),
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.small)
|
||||||
|
.height(24.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,12 +452,20 @@ private fun SourceSwitchPreference(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NsfwWarningDialog(
|
private fun ContentWarningDialog(
|
||||||
|
label: StringResource,
|
||||||
|
description: StringResource,
|
||||||
onClickConfirm: () -> Unit,
|
onClickConfirm: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(label),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(text = stringResource(MR.strings.ext_nsfw_warning))
|
Text(text = stringResource(description))
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = onClickConfirm) {
|
TextButton(onClick = onClickConfirm) {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
|||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
|
import eu.kanade.presentation.browse.components.label
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
||||||
@@ -367,22 +368,27 @@ private fun ExtensionItemContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val warning = when {
|
val warnings = listOfNotNull(
|
||||||
extension is Extension.Untrusted -> MR.strings.ext_untrusted
|
when {
|
||||||
extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
extension is Extension.Untrusted ->
|
||||||
extension.isNsfw -> MR.strings.ext_nsfw_short
|
MR.strings.ext_untrusted to MaterialTheme.colorScheme.error
|
||||||
else -> null
|
extension is Extension.Installed && extension.isObsolete ->
|
||||||
}
|
MR.strings.ext_obsolete to MaterialTheme.colorScheme.error
|
||||||
if (warning != null) {
|
else -> null
|
||||||
|
},
|
||||||
|
extension.contentWarning.label?.let { it.title to it.color },
|
||||||
|
)
|
||||||
|
warnings.forEach { (label, color) ->
|
||||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||||
hasAlreadyShownAnElement = true
|
hasAlreadyShownAnElement = true
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(warning).uppercase(),
|
text = stringResource(label).uppercase(),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = color,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extension is Extension.Installed && !extension.isShared) {
|
if (extension is Extension.Installed && !extension.isShared) {
|
||||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import mihon.domain.extension.model.ContentWarning
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
|
internal data class ContentWarningLabel(
|
||||||
|
val title: StringResource,
|
||||||
|
val description: StringResource,
|
||||||
|
val color: Color,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val ContentWarning.label: ContentWarningLabel?
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = when (this) {
|
||||||
|
ContentWarning.SAFE -> null
|
||||||
|
ContentWarning.MIXED -> ContentWarningLabel(
|
||||||
|
title = MR.strings.ext_content_warning_mixed,
|
||||||
|
description = MR.strings.ext_content_warning_mixed_description,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
ContentWarning.NSFW -> ContentWarningLabel(
|
||||||
|
title = MR.strings.ext_content_warning_nsfw,
|
||||||
|
description = MR.strings.ext_content_warning_nsfw_description,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
+27
-10
@@ -13,6 +13,7 @@ import eu.kanade.presentation.more.settings.Preference
|
|||||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||||
import mihon.app.di.appGraph
|
import mihon.app.di.appGraph
|
||||||
|
import mihon.domain.extension.model.ContentWarning
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
@@ -52,19 +53,35 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_nsfw_content),
|
title = stringResource(MR.strings.pref_category_extensions),
|
||||||
preferenceItems = listOf(
|
preferenceItems = listOf(
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.MultiSelectListPreference(
|
||||||
preference = sourcePreferences.showNsfwSource,
|
preference = sourcePreferences.enabledContentWarnings,
|
||||||
title = stringResource(MR.strings.pref_show_nsfw_source),
|
entries = mapOf(
|
||||||
subtitle = stringResource(MR.strings.requires_app_restart),
|
ContentWarning.SAFE to stringResource(MR.strings.ext_content_warning_safe),
|
||||||
onValueChanged = {
|
ContentWarning.MIXED to stringResource(MR.strings.ext_content_warning_mixed),
|
||||||
(context as FragmentActivity).authenticate(
|
ContentWarning.NSFW to stringResource(MR.strings.ext_content_warning_nsfw),
|
||||||
title = context.stringResource(MR.strings.pref_category_nsfw_content),
|
),
|
||||||
)
|
title = stringResource(MR.strings.pref_allowed_content_warnings),
|
||||||
|
subtitleProvider = { value, entries ->
|
||||||
|
remember(value, entries) {
|
||||||
|
entries.filterKeys { it in value }.values.joinToString()
|
||||||
|
}
|
||||||
|
.takeUnless { it.isBlank() }
|
||||||
|
?: stringResource(MR.strings.none)
|
||||||
|
},
|
||||||
|
onValueChanged = { newValue ->
|
||||||
|
val added = newValue - sourcePreferences.enabledContentWarnings.get()
|
||||||
|
if (added.any { it != ContentWarning.SAFE }) {
|
||||||
|
(context as FragmentActivity).authenticate(
|
||||||
|
title = context.stringResource(MR.strings.pref_allowed_content_warnings),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.parental_controls_info)),
|
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.content_warnings_info)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.app.di.appGraph
|
import mihon.app.di.appGraph
|
||||||
import mihon.data.dalvik.DelegateLastClassLoaderCompat
|
import mihon.data.dalvik.DelegateLastClassLoaderCompat
|
||||||
|
import mihon.domain.extension.model.ContentWarning
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -222,7 +223,7 @@ internal object ExtensionLoader {
|
|||||||
*/
|
*/
|
||||||
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
|
private suspend fun loadExtension(context: Context, extensionInfo: ExtensionInfo): LoadResult {
|
||||||
val trustExtension: TrustExtension = context.appGraph.trustExtension
|
val trustExtension: TrustExtension = context.appGraph.trustExtension
|
||||||
val loadNsfwSource: Boolean = context.appGraph.sourcePreferences.showNsfwSource.get()
|
val enabledContentWarnings = context.appGraph.sourcePreferences.enabledContentWarnings.get()
|
||||||
|
|
||||||
val pkgManager = context.packageManager
|
val pkgManager = context.packageManager
|
||||||
val pkgInfo = extensionInfo.packageInfo
|
val pkgInfo = extensionInfo.packageInfo
|
||||||
@@ -269,10 +270,19 @@ internal object ExtensionLoader {
|
|||||||
return LoadResult.Untrusted(extension)
|
return LoadResult.Untrusted(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isNsfw = appInfo.metaData.getInt(METADATA_CONTENT_WARNING) > 0 ||
|
val contentWarning = when {
|
||||||
appInfo.metaData.getInt(METADATA_NSFW) == 1
|
appInfo.metaData.containsKey(METADATA_CONTENT_WARNING) -> {
|
||||||
if (!loadNsfwSource && isNsfw) {
|
when (appInfo.metaData.getInt(METADATA_CONTENT_WARNING)) {
|
||||||
logcat(LogPriority.WARN) { "NSFW extension $pkgName not allowed" }
|
1 -> ContentWarning.MIXED
|
||||||
|
2 -> ContentWarning.NSFW
|
||||||
|
else -> ContentWarning.SAFE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appInfo.metaData.getInt(METADATA_NSFW) == 1 -> ContentWarning.NSFW
|
||||||
|
else -> ContentWarning.SAFE
|
||||||
|
}
|
||||||
|
if (contentWarning !in enabledContentWarnings) {
|
||||||
|
logcat(LogPriority.WARN) { "Extension $pkgName with $contentWarning not allowed" }
|
||||||
return LoadResult.Error
|
return LoadResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +330,7 @@ internal object ExtensionLoader {
|
|||||||
versionCode = versionCode,
|
versionCode = versionCode,
|
||||||
libVersion = libVersion,
|
libVersion = libVersion,
|
||||||
lang = lang,
|
lang = lang,
|
||||||
isNsfw = isNsfw,
|
contentWarning = contentWarning,
|
||||||
sources = sources,
|
sources = sources,
|
||||||
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
|
||||||
icon = appInfo.loadIcon(pkgManager),
|
icon = appInfo.loadIcon(pkgManager),
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package mihon.core.migration.migrations
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesIntoSet
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import mihon.core.migration.Migration
|
||||||
|
import mihon.core.migration.MigrationContext
|
||||||
|
import mihon.domain.extension.model.ContentWarning
|
||||||
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@ContributesIntoSet(AppScope::class)
|
||||||
|
class ContentWarningMigration(
|
||||||
|
private val preferenceStore: PreferenceStore,
|
||||||
|
private val sourcePreferences: SourcePreferences,
|
||||||
|
) : Migration {
|
||||||
|
override val version: Float = 30f
|
||||||
|
|
||||||
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
|
||||||
|
val showNsfwSource = preferenceStore.getBoolean("show_nsfw_source", true)
|
||||||
|
if (!showNsfwSource.isSet()) return true
|
||||||
|
|
||||||
|
if (!showNsfwSource.get()) {
|
||||||
|
sourcePreferences.enabledContentWarnings.set(setOf(ContentWarning.SAFE))
|
||||||
|
}
|
||||||
|
showNsfwSource.delete()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import mihon.data.extension.model.NetworkExtensionStore.ContentWarning
|
|||||||
import mihon.data.extension.model.NetworkExtensionStore.ExtensionList
|
import mihon.data.extension.model.NetworkExtensionStore.ExtensionList
|
||||||
import mihon.domain.extension.model.ExtensionStore
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension as TachiyomiExtension
|
import eu.kanade.tachiyomi.extension.model.Extension as TachiyomiExtension
|
||||||
|
import mihon.domain.extension.model.ContentWarning as DomainContentWarning
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -104,7 +105,12 @@ fun ExtensionList.toAvailableExtensions(store: ExtensionStore): List<TachiyomiEx
|
|||||||
versionCode = extension.versionCode,
|
versionCode = extension.versionCode,
|
||||||
versionName = extension.versionName,
|
versionName = extension.versionName,
|
||||||
lang = if (lang.size == 1) lang.first() else "all",
|
lang = if (lang.size == 1) lang.first() else "all",
|
||||||
isNsfw = extension.contentWarning >= ContentWarning.MIXED,
|
contentWarning = when (extension.contentWarning) {
|
||||||
|
ContentWarning.SAFE -> DomainContentWarning.SAFE
|
||||||
|
ContentWarning.MIXED -> DomainContentWarning.MIXED
|
||||||
|
ContentWarning.NSFW -> DomainContentWarning.NSFW
|
||||||
|
else -> DomainContentWarning.SAFE
|
||||||
|
},
|
||||||
sources = extension.sources.map { source ->
|
sources = extension.sources.map { source ->
|
||||||
TachiyomiExtension.Available.Source(
|
TachiyomiExtension.Available.Source(
|
||||||
id = source.id,
|
id = source.id,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package mihon.data.extension.model
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import mihon.domain.extension.model.ContentWarning
|
||||||
import mihon.domain.extension.model.ExtensionStore
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
@@ -35,7 +36,7 @@ data class NetworkLegacyExtension(
|
|||||||
versionCode = code,
|
versionCode = code,
|
||||||
versionName = version,
|
versionName = version,
|
||||||
lang = lang,
|
lang = lang,
|
||||||
isNsfw = nsfw == 1,
|
contentWarning = if (nsfw == 1) ContentWarning.NSFW else ContentWarning.SAFE,
|
||||||
sources = if (sources.isNullOrEmpty()) {
|
sources = if (sources.isNullOrEmpty()) {
|
||||||
listOf(
|
listOf(
|
||||||
Extension.Available.Source(
|
Extension.Available.Source(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.model
|
|||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import mihon.domain.extension.model.ContentWarning
|
||||||
import mihon.domain.extension.model.ExtensionStore
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ sealed class Extension {
|
|||||||
abstract val versionCode: Long
|
abstract val versionCode: Long
|
||||||
abstract val libVersion: Double
|
abstract val libVersion: Double
|
||||||
abstract val lang: String?
|
abstract val lang: String?
|
||||||
abstract val isNsfw: Boolean
|
abstract val contentWarning: ContentWarning
|
||||||
|
|
||||||
data class Installed(
|
data class Installed(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
@@ -22,7 +23,7 @@ sealed class Extension {
|
|||||||
override val versionCode: Long,
|
override val versionCode: Long,
|
||||||
override val libVersion: Double,
|
override val libVersion: Double,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
override val isNsfw: Boolean,
|
override val contentWarning: ContentWarning,
|
||||||
val pkgFactory: String?,
|
val pkgFactory: String?,
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
val icon: Drawable?,
|
val icon: Drawable?,
|
||||||
@@ -39,7 +40,7 @@ sealed class Extension {
|
|||||||
override val versionCode: Long,
|
override val versionCode: Long,
|
||||||
override val libVersion: Double,
|
override val libVersion: Double,
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
override val isNsfw: Boolean,
|
override val contentWarning: ContentWarning,
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
val apkUrl: String,
|
val apkUrl: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
@@ -70,6 +71,6 @@ sealed class Extension {
|
|||||||
override val libVersion: Double,
|
override val libVersion: Double,
|
||||||
val signatureHash: String,
|
val signatureHash: String,
|
||||||
override val lang: String? = null,
|
override val lang: String? = null,
|
||||||
override val isNsfw: Boolean = false,
|
override val contentWarning: ContentWarning = ContentWarning.SAFE,
|
||||||
) : Extension()
|
) : Extension()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package mihon.domain.extension.model
|
||||||
|
|
||||||
|
enum class ContentWarning {
|
||||||
|
SAFE,
|
||||||
|
MIXED,
|
||||||
|
NSFW,
|
||||||
|
}
|
||||||
@@ -266,9 +266,9 @@
|
|||||||
<string name="secure_screen_summary">Secure screen hides app contents when switching apps and block screenshots</string>
|
<string name="secure_screen_summary">Secure screen hides app contents when switching apps and block screenshots</string>
|
||||||
<string name="firebase_summary">Sending crash logs and analytics will allow us to identify and fix issues, improve performance, and make future updates more relevant to your needs</string>
|
<string name="firebase_summary">Sending crash logs and analytics will allow us to identify and fix issues, improve performance, and make future updates more relevant to your needs</string>
|
||||||
|
|
||||||
<string name="pref_category_nsfw_content">NSFW (18+) sources</string>
|
<string name="pref_category_extensions">Extensions</string>
|
||||||
<string name="pref_show_nsfw_source">Show in sources and extensions lists</string>
|
<string name="pref_allowed_content_warnings">Allowed content warnings</string>
|
||||||
<string name="parental_controls_info">This does not prevent unofficial or potentially incorrectly flagged extensions from surfacing NSFW (18+) content within the app.</string>
|
<string name="content_warnings_info">Requires app restart to take effect. Extensions that aren\'t loaded won\'t provide their sources anywhere in the app. This does not prevent unofficial or potentially incorrectly flagged extensions from surfacing 18+ content within the app.</string>
|
||||||
|
|
||||||
<string name="relative_time_today">Today</string>
|
<string name="relative_time_today">Today</string>
|
||||||
|
|
||||||
@@ -349,9 +349,12 @@
|
|||||||
<string name="extension_api_error">Failed to fetch available extensions</string>
|
<string name="extension_api_error">Failed to fetch available extensions</string>
|
||||||
<string name="ext_info_version">Version</string>
|
<string name="ext_info_version">Version</string>
|
||||||
<string name="ext_info_language">Language</string>
|
<string name="ext_info_language">Language</string>
|
||||||
<string name="ext_info_age_rating">Age rating</string>
|
<string name="ext_info_warning">Warning</string>
|
||||||
<string name="ext_nsfw_short">18+</string>
|
<string name="ext_content_warning_safe">Safe</string>
|
||||||
<string name="ext_nsfw_warning">Sources from this extension may contain NSFW (18+) content</string>
|
<string name="ext_content_warning_mixed">Mixed</string>
|
||||||
|
<string name="ext_content_warning_nsfw">18+</string>
|
||||||
|
<string name="ext_content_warning_mixed_description">Sources from this extension might contain both safe and 18+ content.</string>
|
||||||
|
<string name="ext_content_warning_nsfw_description">Sources from this extension are primarily focused on 18+ content.</string>
|
||||||
<string name="ext_permission_install_apps_warning">Permissions are needed to install extensions. Tap here to grant.</string>
|
<string name="ext_permission_install_apps_warning">Permissions are needed to install extensions. Tap here to grant.</string>
|
||||||
<string name="ext_install_service_notif">Installing extension…</string>
|
<string name="ext_install_service_notif">Installing extension…</string>
|
||||||
<string name="ext_installer_pref">Installer</string>
|
<string name="ext_installer_pref">Installer</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user