mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2025-01-23 21:51:20 +01:00
Adding NSFW warning to extensions
Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
parent
17a1f49c2d
commit
e8cba5c164
@ -0,0 +1,5 @@
|
|||||||
|
package eu.kanade.tachiyomi.annotations
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class Nsfw
|
@ -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"
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user