Extension detail updates

Add readme/changelog support (readme icon is filled in for sources that have readmes)

Also added the icon when loading extensions
This commit is contained in:
Jays2Kings 2022-05-04 02:16:45 -04:00
parent c811d16c90
commit ffb7b44f76
10 changed files with 178 additions and 54 deletions

View file

@ -68,6 +68,8 @@ class ExtensionManager(
*/
private val installedExtensionsRelay = BehaviorRelay.create<List<Extension.Installed>>()
private val iconMap = mutableMapOf<String, Drawable>()
/**
* List of the currently installed extensions.
*/
@ -79,12 +81,19 @@ class ExtensionManager(
}
fun getAppIconForSource(source: Source): Drawable? {
return getAppIconForSource(source.id)
}
fun getAppIconForSource(sourceId: Long): Drawable? {
val pkgName =
installedExtensions.find { ext -> ext.sources.any { it.id == source.id } }?.pkgName
return if (pkgName != null) try {
context.packageManager.getApplicationIcon(pkgName)
installedExtensions.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
return if (pkgName != null) {
try {
return iconMap[pkgName]
?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
} catch (e: Exception) {
null
}
} else null
}
@ -105,7 +114,7 @@ class ExtensionManager(
setupAvailableSourcesMap()
}
private var availableSources = hashMapOf<String, Extension.AvailableSource>()
private var availableSources = hashMapOf<Long, Extension.AvailableSource>()
/**
* Relay used to notify the untrusted extensions.
@ -200,7 +209,7 @@ class ExtensionManager(
}
}
fun getStubSource(id: Long) = availableSources[id.toString()]
fun getStubSource(id: Long) = availableSources[id]
/**
* Finds the available extensions in the [api] and updates [availableExtensions].

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
@ -10,25 +11,29 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.Serializable
import uy.kohesive.injekt.injectLazy
internal class ExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
suspend fun findExtensions(): List<Extension.Available> {
return withIOContext {
networkService.client
val extensions = networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.await()
.parseAs<JsonArray>()
.let { parseResponse(it) }
.parseAs<List<ExtensionJsonObject>>()
.toExtensions()
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
if (extensions.size < 100) {
throw Exception()
}
extensions
}
}
@ -55,31 +60,26 @@ internal class ExtensionGithubApi {
}
}
private fun parseResponse(json: JsonArray): List<Extension.Available> {
return json
.filter { element ->
val versionName = element.jsonObject["version"]!!.jsonPrimitive.content
val libVersion = versionName.substringBeforeLast('.').toDouble()
private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
return this
.filter {
val libVersion = it.version.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.long
val lang = element.jsonObject["lang"]!!.jsonPrimitive.content
val nsfw = element.jsonObject["nsfw"]!!.jsonPrimitive.int == 1
val sources = element.jsonObject["sources"]?.jsonArray?.map f@{
val sName = it.jsonObject["name"]?.jsonPrimitive?.content ?: return@f null
val sId = it.jsonObject["id"]?.jsonPrimitive?.content ?: return@f null
val sLang = it.jsonObject["lang"]?.jsonPrimitive?.content ?: ""
val sBaseUrl = it.jsonObject["baseUrl"]?.jsonPrimitive?.content ?: ""
Extension.AvailableSource(sName, sId, sLang, sBaseUrl)
}?.filterNotNull()
val icon = "${REPO_URL_PREFIX}icon/${apkName.replace(".apk", ".png")}"
Extension.Available(name, pkgName, versionName, versionCode, lang, nsfw, apkName, icon, sources)
.map {
Extension.Available(
name = it.name.substringAfter("Tachiyomi: "),
pkgName = it.pkg,
versionName = it.version,
versionCode = it.code,
lang = it.lang,
isNsfw = it.nsfw == 1,
hasReadme = it.hasReadme == 1,
hasChangelog = it.hasChangelog == 1,
sources = it.sources,
apkName = it.apk,
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}",
)
}
}
@ -89,3 +89,17 @@ internal class ExtensionGithubApi {
}
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
@Serializable
private data class ExtensionJsonObject(
val name: String,
val pkg: String,
val apk: String,
val lang: String,
val code: Long,
val version: String,
val nsfw: Int,
val hasReadme: Int = 0,
val hasChangelog: Int = 0,
val sources: List<Extension.AvailableSource>?,
)

View file

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.extension.model
import android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.source.Source
import kotlinx.serialization.Serializable
sealed class Extension {
@ -10,6 +12,8 @@ sealed class Extension {
abstract val versionCode: Long
abstract val lang: String?
abstract val isNsfw: Boolean
abstract val hasReadme: Boolean
abstract val hasChangelog: Boolean
data class Installed(
override val name: String,
@ -18,8 +22,11 @@ sealed class Extension {
override val versionCode: Long,
override val lang: String,
override val isNsfw: Boolean,
override val hasReadme: Boolean,
override val hasChangelog: Boolean,
val pkgFactory: String?,
val sources: List<Source>,
val icon: Drawable?,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false,
@ -32,14 +39,17 @@ sealed class Extension {
override val versionCode: Long,
override val lang: String,
override val isNsfw: Boolean,
override val hasReadme: Boolean,
override val hasChangelog: Boolean,
val apkName: String,
val iconUrl: String,
val sources: List<AvailableSource>? = null,
) : Extension()
@Serializable
data class AvailableSource(
val name: String,
val id: String,
val id: Long,
val lang: String,
val baseUrl: String,
)
@ -52,5 +62,7 @@ sealed class Extension {
val signatureHash: String,
override val lang: String? = null,
override val isNsfw: Boolean = false,
override val hasReadme: Boolean = false,
override val hasChangelog: Boolean = false,
) : Extension()
}

View file

@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.util.lang.Hash
import eu.kanade.tachiyomi.util.system.getApplicationIcon
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import timber.log.Timber
@ -35,6 +36,8 @@ internal object ExtensionLoader {
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme"
private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.3
@ -151,6 +154,9 @@ internal object ExtensionLoader {
return LoadResult.Error("NSFW extension $pkgName not allowed")
}
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1
val classLoader = PathClassLoader(appInfo.sourceDir, null, context.classLoader)
val sources = appInfo.metaData.getString(METADATA_SOURCE_CLASS)!!
@ -179,7 +185,6 @@ internal object ExtensionLoader {
val langs = sources.filterIsInstance<CatalogueSource>()
.map { it.lang }
.toSet()
val lang = when (langs.size) {
0 -> ""
1 -> langs.first()
@ -193,9 +198,12 @@ internal object ExtensionLoader {
versionCode,
lang,
isNsfw,
hasReadme,
hasChangelog,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = signatureHash != officialSignature,
icon = context.getApplicationIcon(pkgName),
)
return LoadResult.Success(extension)
}

View file

@ -29,9 +29,9 @@ class AndroidCookieJar : CookieJar {
}
}
fun remove(url: HttpUrl, cookieNames: List<String>? = null, maxAge: Int = -1) {
fun remove(url: HttpUrl, cookieNames: List<String>? = null, maxAge: Int = -1): Int {
val urlString = url.toString()
val cookies = manager.getCookie(urlString) ?: return
val cookies = manager.getCookie(urlString) ?: return 0
fun List<String>.filterNames(): List<String> {
return if (cookieNames != null) {
@ -41,10 +41,11 @@ class AndroidCookieJar : CookieJar {
}
}
cookies.split(";")
return cookies.split(";")
.map { it.substringBefore("=") }
.filterNames()
.onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") }
.count()
}
fun removeAll() {

View file

@ -105,8 +105,8 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
binding.sourceImage.load(extension.iconUrl) {
target(CoverViewTarget(binding.sourceImage))
}
} else {
extension.getApplicationIcon(itemView.context)?.let { binding.sourceImage.setImageDrawable(it) }
} else if (extension is Extension.Installed) {
binding.sourceImage.load(extension.icon)
}
bindButton(item)
}

View file

@ -31,14 +31,17 @@ import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.setting.DSL
import eu.kanade.tachiyomi.ui.setting.onChange
import eu.kanade.tachiyomi.ui.setting.switchPreference
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.contextCompatDrawable
import eu.kanade.tachiyomi.util.view.openInBrowser
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.snack
@ -46,8 +49,11 @@ import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset
import eu.kanade.tachiyomi.widget.TachiyomiTextInputEditText.Companion.setIncognito
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) :
@ -60,6 +66,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
private var preferenceScreen: PreferenceScreen? = null
private val preferences: PreferencesHelper = Injekt.get()
private val network: NetworkHelper by injectLazy()
init {
setHasOptionsMenu(true)
@ -147,24 +154,74 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.extension_details, menu)
menu.findItem(R.id.action_history).isVisible = presenter.extension?.isUnofficial == false
presenter.extension?.let { extension ->
menu.findItem(R.id.action_history).isVisible = !extension.isUnofficial
menu.findItem(R.id.action_readme).isVisible = !extension.isUnofficial
if (extension.hasReadme) {
menu.findItem(R.id.action_readme).icon = view?.context?.contextCompatDrawable(R.drawable.ic_help_24dp)
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_history -> openCommitHistory()
R.id.action_history -> openChangelog()
R.id.action_readme -> openReadme()
R.id.action_clear_cookies -> clearCookies()
}
return super.onOptionsItemSelected(item)
}
private fun openCommitHistory() {
val pkgName = presenter.extension!!.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
val pkgFactory = presenter.extension!!.pkgFactory
val url = when {
!pkgFactory.isNullOrEmpty() -> "$URL_EXTENSION_COMMITS/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory"
else -> "$URL_EXTENSION_COMMITS/src/${pkgName.replace(".", "/")}"
}
private fun openChangelog() {
val extension = presenter.extension!!
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
val pkgFactory = extension.pkgFactory
if (extension.hasChangelog) {
val url = createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/CHANGELOG.md")
openInBrowser(url)
return
}
// Falling back on GitHub commit history because there is no explicit changelog in extension
val url = createUrl(URL_EXTENSION_COMMITS, pkgName, pkgFactory)
openInBrowser(url)
}
private fun createUrl(url: String, pkgName: String, pkgFactory: String?, path: String = ""): String {
return when {
!pkgFactory.isNullOrEmpty() -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory$path"
else -> "$url/src/${pkgName.replace(".", "/")}$path"
}
}
private fun openReadme() {
val extension = presenter.extension!!
if (!extension.hasReadme) {
openInBrowser("https://tachiyomi.org/help/faq/#extensions")
return
}
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
val pkgFactory = extension.pkgFactory
val url = createUrl(URL_EXTENSION_BLOB, pkgName, pkgFactory, "/README.md")
openInBrowser(url)
return
}
private fun clearCookies() {
val urls = presenter.extension?.sources
?.filterIsInstance<HttpSource>()
?.map { it.baseUrl }
?.distinct() ?: emptyList()
val cleared = urls.sumOf {
network.cookieManager.remove(it.toHttpUrl())
}
Timber.d("Cleared $cleared cookies for: ${urls.joinToString()}")
val context = view?.context ?: return
binding.coordinator.snack(context.getString(R.string.cookies_cleared))
}
private fun addPreferencesForSource(screen: PreferenceScreen, source: Source, isMultiSource: Boolean, isMultiLangSingleSource: Boolean) {
@ -307,6 +364,8 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
private companion object {
const val PKGNAME_KEY = "pkg_name"
const val LASTOPENPREFERENCE_KEY = "last_open_preference"
private const val URL_EXTENSION_BLOB =
"https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master"
private const val URL_EXTENSION_COMMITS =
"https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
}

View file

@ -455,3 +455,11 @@ fun Context.createFileInCacheDir(name: String): File {
file.createNewFile()
return file
}
fun Context.getApplicationIcon(pkgName: String): Drawable? {
return try {
packageManager.getApplicationIcon(pkgName)
} catch (e: PackageManager.NameNotFoundException) {
null
}
}

View file

@ -7,4 +7,16 @@
android:title="@string/whats_new"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_readme"
android:icon="@drawable/ic_help_outline_24dp"
android:title="@string/faq_and_guides"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_clear_cookies"
android:icon="@drawable/ic_delete_24dp"
android:title="@string/clear_cookies"
app:showAsAction="never" />
</menu>

View file

@ -873,6 +873,7 @@
<string name="send_crash_report">Send crash reports</string>
<string name="helps_fix_bugs">Helps fix any bugs. No sensitive data will be sent</string>
<string name="whats_new">What\'s new</string>
<string name="faq_and_guides">FAQ and Guides</string>
<string name="whats_new_this_release">What\'s new in this release</string>
<string name="help_translate">Help translate</string>
<string name="helpful_translation_links">Helpful translation links</string>