Replace extension NSFW flag with content warning app-wide (#3951)

This commit is contained in:
AntsyLich
2026-09-14 21:00:06 +06:00
committed by GitHub
parent cbafe9bf3f
commit 0fbaa1ecec
14 changed files with 202 additions and 68 deletions
+1 -1
View File
@@ -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
extension is Extension.Installed && extension.isObsolete ->
MR.strings.ext_obsolete to MaterialTheme.colorScheme.error
else -> null else -> null
} },
if (warning != 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,
)
}
@@ -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),
ContentWarning.NSFW to stringResource(MR.strings.ext_content_warning_nsfw),
),
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( (context as FragmentActivity).authenticate(
title = context.stringResource(MR.strings.pref_category_nsfw_content), 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>