Adding NSFW warning to extensions

Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2021-03-23 14:12:31 -04:00
parent 17a1f49c2d
commit e8cba5c164
11 changed files with 216 additions and 85 deletions

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.annotations
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Nsfw

View File

@ -159,6 +159,10 @@ object PreferenceKeys {
const val enableDoh = "enable_doh" const val enableDoh = "enable_doh"
const val showNsfwSource = "show_nsfw_source"
const val showNsfwExtension = "show_nsfw_extension"
const val labelNsfwExtension = "label_nsfw_extension"
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"

View File

@ -301,6 +301,10 @@ class PreferencesHelper(val context: Context) {
fun hideBottomNavOnScroll() = flowPrefs.getBoolean(Keys.hideBottomNavOnScroll, true) fun hideBottomNavOnScroll() = flowPrefs.getBoolean(Keys.hideBottomNavOnScroll, true)
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
fun showNsfwExtension() = flowPrefs.getBoolean(Keys.showNsfwExtension, true)
fun labelNsfwExtension() = prefs.getBoolean(Keys.labelNsfwExtension, true)
fun createLegacyBackup() = flowPrefs.getBoolean(Keys.createLegacyBackup, true) fun createLegacyBackup() = flowPrefs.getBoolean(Keys.createLegacyBackup, true)
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false) fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
} }

View File

@ -1,90 +1,86 @@
package eu.kanade.tachiyomi.extension.api package eu.kanade.tachiyomi.extension.api
import android.content.Context import android.content.Context
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.string
import com.google.gson.Gson
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Response import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi { internal class ExtensionGithubApi {
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
private val gson: Gson by injectLazy()
suspend fun findExtensions(): List<Extension.Available> { suspend fun findExtensions(): List<Extension.Available> {
val call = GET("$REPO_URL/index.json")
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
parseResponse(network.client.newCall(call).await()) network.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.await()
.parseAs<JsonArray>()
.let { parseResponse(it) }
} }
} }
suspend fun checkForUpdates(context: Context): List<Extension.Installed> { suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val call = GET("$REPO_URL/index.json") val extensions = findExtensions()
val response = network.client.newCall(call).await()
if (response.isSuccessful) { val installedExtensions = ExtensionLoader.loadExtensions(context)
val extensions = parseResponse(response) .filterIsInstance<LoadResult.Success>()
val extensionsWithUpdate = mutableListOf<Extension.Installed>() .map { it.extension }
val installedExtensions = val extensionsWithUpdate = mutableListOf<Extension.Installed>()
ExtensionLoader.loadExtensions(context).filterIsInstance<LoadResult.Success>() val mutInstalledExtensions = installedExtensions.toMutableList()
.map { it.extension } for (installedExt in mutInstalledExtensions) {
val mutInstalledExtensions = installedExtensions.toMutableList() val pkgName = installedExt.pkgName
for (installedExt in mutInstalledExtensions) { val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
val pkgName = installedExt.pkgName
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
val hasUpdate = availableExt.versionCode > installedExt.versionCode val hasUpdate = availableExt.versionCode > installedExt.versionCode
if (hasUpdate) extensionsWithUpdate.add(installedExt) if (hasUpdate) {
extensionsWithUpdate.add(installedExt)
} }
extensionsWithUpdate
} else {
response.close()
throw Exception("Failed to get extensions")
} }
extensionsWithUpdate
} }
} }
private fun parseResponse(response: Response): List<Extension.Available> { private fun parseResponse(json: kotlinx.serialization.json.JsonArray): List<Extension.Available> {
val text = response.body?.use { it.string() } ?: return emptyList() return json
.filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val libVersion = versionName.substringBeforeLast('.').toDouble()
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
}
.map { element ->
val name = element.jsonObject["name"]!!.jsonPrimitive.content.substringAfter("Tachiyomi: ")
val pkgName = element.jsonObject["pkg"]!!.jsonPrimitive.content
val apkName = element.jsonObject["apk"]!!.jsonPrimitive.content
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val versionCode = element.jsonObject["code"]!!.jsonPrimitive.int
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val icon = "${REPO_URL_PREFIX}icon/${apkName.replace(".apk", ".png")}"
val json = gson.fromJson<JsonArray>(text) Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon)
return json.map { element ->
val name = element["name"].string.substringAfter("Tachiyomi: ")
val pkgName = element["pkg"].string
val apkName = element["apk"].string
val versionName = element["version"].string
val versionCode = element["code"].int
val lang = element["lang"].string
val icon = "$REPO_URL/icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, apkName, icon)
} }
} }
fun getApkUrl(extension: Extension.Available): String { fun getApkUrl(extension: Extension.Available): String {
return "$REPO_URL/apk/${extension.apkName}" return "${REPO_URL_PREFIX}apk/${extension.apkName}"
} }
companion object { companion object {
private const val REPO_URL = private const val BASE_URL = "https://raw.githubusercontent.com/"
"https://raw.githubusercontent.com/inorichi/tachiyomi-extensions/repo" const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
} }
} }

View File

@ -9,16 +9,20 @@ sealed class Extension {
abstract val versionName: String abstract val versionName: String
abstract val versionCode: Int abstract val versionCode: Int
abstract val lang: String? abstract val lang: String?
abstract val isNsfw: Boolean
data class Installed( data class Installed(
override val name: String, override val name: String,
override val pkgName: String, override val pkgName: String,
override val versionName: String, override val versionName: String,
override val versionCode: Int, override val versionCode: Int,
val sources: List<Source>,
override val lang: String, override val lang: String,
override val isNsfw: Boolean,
val pkgFactory: String?,
val sources: List<Source>,
val hasUpdate: Boolean = false, val hasUpdate: Boolean = false,
val isObsolete: Boolean = false val isObsolete: Boolean = false,
val isUnofficial: Boolean = false
) : Extension() ) : Extension()
data class Available( data class Available(
@ -27,6 +31,7 @@ sealed class Extension {
override val versionName: String, override val versionName: String,
override val versionCode: Int, override val versionCode: Int,
override val lang: String, override val lang: String,
override val isNsfw: Boolean,
val apkName: String, val apkName: String,
val iconUrl: String val iconUrl: String
) : Extension() ) : Extension()
@ -37,6 +42,7 @@ sealed class Extension {
override val versionName: String, override val versionName: String,
override val versionCode: Int, override val versionCode: Int,
val signatureHash: String, val signatureHash: String,
override val lang: String? = null override val lang: String? = null,
override val isNsfw: Boolean = false
) : Extension() ) : Extension()
} }

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@ -16,8 +17,7 @@ import eu.kanade.tachiyomi.util.lang.Hash
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get
/** /**
* Class that handles the loading of the extensions installed in the system. * Class that handles the loading of the extensions installed in the system.
@ -25,20 +25,27 @@ import uy.kohesive.injekt.api.get
@SuppressLint("PackageManagerGetSignatures") @SuppressLint("PackageManagerGetSignatures")
internal object ExtensionLoader { internal object ExtensionLoader {
private val preferences: PreferencesHelper by injectLazy()
private val loadNsfwSource by lazy {
preferences.showNsfwSource().get()
}
private const val EXTENSION_FEATURE = "tachiyomi.extension" private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val LIB_VERSION_MIN = 1 private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
private const val LIB_VERSION_MAX = 1 private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key
private const val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/** /**
* List of the trusted signatures. * List of the trusted signatures.
*/ */
var trustedSignatures = mutableSetOf<String>() + var trustedSignatures = mutableSetOf<String>() + preferences.trustedSignatures().getOrDefault() + officialSignature
Injekt.get<PreferencesHelper>().trustedSignatures().getOrDefault() +
// inorichi's key
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/** /**
* Return a list of all the installed extensions initialized concurrently. * Return a list of all the installed extensions initialized concurrently.
@ -95,16 +102,21 @@ internal object ExtensionLoader {
return LoadResult.Error(error) return LoadResult.Error(error)
} }
val extName = pkgManager.getApplicationLabel(appInfo)?.toString() val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
.orEmpty().substringAfter("Tachiyomi: ")
val versionName = pkgInfo.versionName val versionName = pkgInfo.versionName
val versionCode = pkgInfo.versionCode val versionCode = pkgInfo.versionCode
if (versionName.isNullOrEmpty()) {
val exception = Exception("Missing versionName for extension $extName")
Timber.w(exception)
return LoadResult.Error(exception)
}
// Validate lib version // Validate lib version
val majorLibVersion = versionName.substringBefore('.').toInt() val libVersion = versionName.substringBeforeLast('.').toDouble()
if (majorLibVersion < LIB_VERSION_MIN || majorLibVersion > LIB_VERSION_MAX) { if (libVersion < LIB_VERSION_MIN || libVersion > LIB_VERSION_MAX) {
val exception = Exception( val exception = Exception(
"Lib version is $majorLibVersion, while only versions " + "Lib version is $libVersion, while only versions " +
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed" "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed"
) )
Timber.w(exception) Timber.w(exception)
@ -121,6 +133,11 @@ internal object ExtensionLoader {
return LoadResult.Untrusted(extension) return LoadResult.Untrusted(extension)
} }
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (!loadNsfwSource && isNsfw) {
return LoadResult.Error("NSFW extension $pkgName not allowed")
}
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader) val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!! val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
@ -134,10 +151,15 @@ internal object ExtensionLoader {
} }
.flatMap { .flatMap {
try { try {
val obj = Class.forName(it, false, classLoader).newInstance() when (val obj = Class.forName(it, false, classLoader).newInstance()) {
when (obj) {
is Source -> listOf(obj) is Source -> listOf(obj)
is SourceFactory -> obj.createSources() is SourceFactory -> {
if (isSourceNsfw(obj)) {
emptyList()
} else {
obj.createSources()
}
}
else -> throw Exception("Unknown source class type! ${obj.javaClass}") else -> throw Exception("Unknown source class type! ${obj.javaClass}")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@ -145,6 +167,7 @@ internal object ExtensionLoader {
return LoadResult.Error(e) return LoadResult.Error(e)
} }
} }
val langs = sources.filterIsInstance<CatalogueSource>() val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang } .map { it.lang }
.toSet() .toSet()
@ -155,7 +178,17 @@ internal object ExtensionLoader {
else -> "all" else -> "all"
} }
val extension = Extension.Installed(extName, pkgName, versionName, versionCode, sources, lang) val extension = Extension.Installed(
extName,
pkgName,
versionName,
versionCode,
lang,
isNsfw,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = signatureHash != officialSignature
)
return LoadResult.Success(extension) return LoadResult.Success(extension)
} }
@ -181,4 +214,22 @@ internal object ExtensionLoader {
null null
} }
} }
/**
* Checks whether a Source or SourceFactory is annotated with @Nsfw.
*/
private fun isSourceNsfw(clazz: Any): Boolean {
if (loadNsfwSource) {
return false
}
if (clazz !is Source && clazz !is SourceFactory) {
return false
}
// Annotations are proxied, hence this janky way of checking for them
return clazz.javaClass.annotations
.flatMap { it.javaClass.interfaces.map { it.simpleName } }
.firstOrNull { it == Nsfw::class.java.simpleName } != null
}
} }

View File

@ -14,8 +14,12 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.resetStrokeColor import eu.kanade.tachiyomi.util.view.resetStrokeColor
import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget import eu.kanade.tachiyomi.data.image.coil.CoverViewTarget
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import kotlinx.android.synthetic.main.extension_card_item.* import kotlinx.android.synthetic.main.extension_card_item.*
import kotlinx.android.synthetic.main.source_global_search_controller_card_item.* import kotlinx.android.synthetic.main.source_global_search_controller_card_item.*
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
class ExtensionHolder(view: View, val adapter: ExtensionAdapter) : class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
BaseFlexibleViewHolder(view, adapter) { BaseFlexibleViewHolder(view, adapter) {
@ -26,17 +30,24 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
} }
} }
private val shouldLabelNsfw by lazy {
Injekt.get<PreferencesHelper>().labelNsfwExtension()
}
fun bind(item: ExtensionItem) { fun bind(item: ExtensionItem) {
val extension = item.extension val extension = item.extension
// Set source name // Set source name
ext_title.text = extension.name ext_title.text = extension.name
version.text = extension.versionName version.text = extension.versionName
lang.text = if (extension !is Extension.Untrusted) { lang.text = LocaleHelper.getDisplayName(extension.lang, itemView.context)
LocaleHelper.getDisplayName(extension.lang, itemView.context) warning.text = when {
} else { extension is Extension.Untrusted -> itemView.context.getString(R.string.untrusted)
itemView.context.getString(R.string.untrusted).toUpperCase() extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.obsolete)
} extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.unofficial)
extension.isNsfw && shouldLabelNsfw -> itemView.context.getString(R.string.nsfw_short)
else -> ""
}.toUpperCase(Locale.ROOT)
edit_button.clear() edit_button.clear()
@ -86,14 +97,8 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
strokeColor = ColorStateList.valueOf(Color.TRANSPARENT) strokeColor = ColorStateList.valueOf(Color.TRANSPARENT)
setText(R.string.update) setText(R.string.update)
} }
extension.isObsolete -> {
// Red outline
setTextColor(ContextCompat.getColorStateList(context, R.drawable.button_bg_error))
setText(R.string.obsolete)
}
else -> { else -> {
setText(R.string.details) setText(R.string.settings)
} }
} }
} else if (extension is Extension.Untrusted) { } else if (extension is Extension.Untrusted) {

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import android.app.Activity import android.app.Activity
import androidx.annotation.StringRes
import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.drawable.DrawableCompat
import androidx.preference.CheckBoxPreference import androidx.preference.CheckBoxPreference
import androidx.preference.DialogPreference import androidx.preference.DialogPreference
@ -13,6 +14,8 @@ import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.widget.preference.IntListMatPreference import eu.kanade.tachiyomi.widget.preference.IntListMatPreference
import eu.kanade.tachiyomi.widget.preference.ListMatPreference import eu.kanade.tachiyomi.widget.preference.ListMatPreference
import eu.kanade.tachiyomi.widget.preference.MultiListMatPreference import eu.kanade.tachiyomi.widget.preference.MultiListMatPreference
@ -86,6 +89,18 @@ inline fun PreferenceScreen.preferenceCategory(block: (@DSL PreferenceCategory).
) )
} }
inline fun PreferenceGroup.infoPreference(@StringRes infoRes: Int): Preference {
return initThenAdd(
Preference(context),
{
iconRes = R.drawable.ic_info_outline_24dp
iconTint = context.getResourceColor(android.R.attr.textColorSecondary)
summaryRes = infoRes
isSelectable = false
}
)
}
inline fun PreferenceScreen.preferenceScreen(block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen { inline fun PreferenceScreen.preferenceScreen(block: (@DSL PreferenceScreen).() -> Unit): PreferenceScreen {
return addThenInit(preferenceManager.createPreferenceScreen(context), block) return addThenInit(preferenceManager.createPreferenceScreen(context), block)
} }

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
@ -11,6 +12,7 @@ import eu.kanade.tachiyomi.ui.migration.MigrationController
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.withFadeTransaction import eu.kanade.tachiyomi.util.view.withFadeTransaction
import kotlinx.coroutines.flow.launchIn
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class SettingsBrowseController : SettingsController() { class SettingsBrowseController : SettingsController() {
@ -105,12 +107,33 @@ class SettingsBrowseController : SettingsController() {
) )
} }
} }
infoPreference(R.string.you_can_migrate_in_library)
} }
preference {
iconRes = R.drawable.ic_info_outline_24dp preferenceCategory {
iconTint = activity?.getResourceColor(android.R.attr.textColorSecondary) ?: 0 titleRes = R.string.nsfw_sources
summaryRes = R.string.you_can_migrate_in_library
isEnabled = false switchPreference {
key = PreferenceKeys.showNsfwSource
titleRes = R.string.show_in_sources
summaryRes = R.string.requires_app_restart
defaultValue = true
}
switchPreference {
key = PreferenceKeys.showNsfwExtension
titleRes = R.string.show_in_extensions
defaultValue = true
}
switchPreference {
key = PreferenceKeys.labelNsfwExtension
titleRes = R.string.label_in_extensions
defaultValue = true
preferences.showNsfwExtension().asImmediateFlow { isVisible = it }.launchIn(viewScope)
}
infoPreference(R.string.does_not_prevent_unofficial_nsfw)
} }
} }
} }

View File

@ -64,6 +64,20 @@
app:layout_constraintStart_toEndOf="@id/lang" app:layout_constraintStart_toEndOf="@id/lang"
tools:text="Version" /> tools:text="Version" />
<TextView
android:id="@+id/warning"
style="@style/TextAppearance.Regular.Body1.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:maxLines="1"
android:textColor="?attr/colorError"
android:textSize="12sp"
app:layout_constraintStart_toEndOf="@id/version"
app:layout_constraintTop_toBottomOf="@+id/ext_title"
tools:text="Warning" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/ext_button" android:id="@+id/ext_button"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"

View File

@ -263,6 +263,9 @@
<string name="version_">Version: %1$s</string> <string name="version_">Version: %1$s</string>
<string name="language_">Language: %1$s</string> <string name="language_">Language: %1$s</string>
<string name="empty_preferences_for_extension">No preferences to edit for this extension</string> <string name="empty_preferences_for_extension">No preferences to edit for this extension</string>
<string name="nsfw_short">18+</string>
<string name="unofficial">Unofficial</string>
<string name="may_contain_nsfw">May contain NSFW (18+) content</string>
<plurals name="extension_updates_available"> <plurals name="extension_updates_available">
<item quantity="one">Extension update available</item> <item quantity="one">Extension update available</item>
<item quantity="other">%d extension updates available</item> <item quantity="other">%d extension updates available</item>
@ -622,6 +625,11 @@
<string name="you_can_migrate_in_library">You can also migrate by selecting manga in your <string name="you_can_migrate_in_library">You can also migrate by selecting manga in your
library</string> library</string>
<string name="source_migration_guide">Source migration guide</string> <string name="source_migration_guide">Source migration guide</string>
<string name="nsfw_sources">NSFW (18+) sources</string>
<string name="show_in_sources">Show in sources list</string>
<string name="show_in_extensions">Show in extensions list</string>
<string name="label_in_extensions">Label in extensions list</string>
<string name="does_not_prevent_unofficial_nsfw">This does not prevent unofficial or potentially incorrectly flagged extensions from surfacing NSFW (18+) content within the app.</string>
<!-- About section --> <!-- About section -->
<string name="version">Version</string> <string name="version">Version</string>