From 14e669e40ccb3bf1707f2e1cc65363b28a3454dc Mon Sep 17 00:00:00 2001
From: nzoba <55888232+nzoba@users.noreply.github.com>
Date: Mon, 15 Jan 2024 21:40:12 +0100
Subject: [PATCH] Add extension repos (#1719)
* Add custom repo setting
* Few fixes custom repo
* Change controller to view
* Improve extensions
* Allow permanently trusting unofficial extensions by version code + signature
* Add advanced setting to revoke all trusted unknown extensions
* Fix crash when IO context
---
app/src/main/AndroidManifest.xml | 10 +
.../java/eu/kanade/tachiyomi/AppModule.kt | 3 +
.../java/eu/kanade/tachiyomi/Migrations.kt | 5 +
.../data/preference/PreferencesHelper.kt | 7 +-
.../tachiyomi/extension/ExtensionManager.kt | 56 ++---
.../tachiyomi/extension/ExtensionUpdateJob.kt | 4 +-
...{ExtensionGithubApi.kt => ExtensionApi.kt} | 77 +++----
.../tachiyomi/extension/model/Extension.kt | 11 +-
.../extension/util/ExtensionLoader.kt | 27 +--
.../extension/util/TrustExtension.kt | 31 +++
.../tachiyomi/ui/category/CategoryHolder.kt | 47 ++--
.../ui/extension/ExtensionBottomPresenter.kt | 4 +-
.../ui/extension/ExtensionBottomSheet.kt | 6 +-
.../tachiyomi/ui/extension/ExtensionHolder.kt | 18 +-
.../ui/extension/ExtensionTrustDialog.kt | 8 +-
.../details/ExtensionDetailsController.kt | 53 -----
.../details/ExtensionDetailsHeaderAdapter.kt | 5 +-
.../kanade/tachiyomi/ui/main/MainActivity.kt | 14 +-
.../ui/setting/SettingsAdvancedController.kt | 11 +
.../ui/setting/SettingsBrowseController.kt | 10 +
.../ui/source/browse/repos/InfoRepoMessage.kt | 65 ++++++
.../ui/source/browse/repos/RepoAdapter.kt | 38 ++++
.../ui/source/browse/repos/RepoController.kt | 203 ++++++++++++++++++
.../ui/source/browse/repos/RepoHolder.kt | 137 ++++++++++++
.../ui/source/browse/repos/RepoItem.kt | 68 ++++++
.../ui/source/browse/repos/RepoPresenter.kt | 100 +++++++++
.../eu/kanade/tachiyomi/util/CrashLogUtil.kt | 5 +-
app/src/main/res/layout/info_repo_message.xml | 48 +++++
app/src/main/res/menu/extension_details.xml | 12 --
app/src/main/res/values/strings.xml | 22 +-
30 files changed, 877 insertions(+), 228 deletions(-)
rename app/src/main/java/eu/kanade/tachiyomi/extension/api/{ExtensionGithubApi.kt => ExtensionApi.kt} (66%)
create mode 100644 app/src/main/java/eu/kanade/tachiyomi/extension/util/TrustExtension.kt
create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/repos/InfoRepoMessage.kt
create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/repos/RepoAdapter.kt
create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/repos/RepoController.kt
create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/repos/RepoHolder.kt
create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/repos/RepoItem.kt
create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/source/browse/repos/RepoPresenter.kt
create mode 100644 app/src/main/res/layout/info_repo_message.xml
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f259297e0971..5f08f9452fb9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -60,6 +60,16 @@
+
+
+
+
+
+
+
+
+
+
= try {
api.findExtensions()
} catch (e: Exception) {
- Timber.e(e)
+ Timber.e(e, context.getString(R.string.extension_api_error))
+ withUIContext { context.toast(R.string.extension_api_error) }
emptyList()
}
-
enableAdditionalSubLanguages(extensions)
_availableExtensionsFlow.value = extensions
@@ -198,14 +203,17 @@ class ExtensionManager(
val pkgName = installedExt.pkgName
val availableExt = availableExtensions.find { it.pkgName == pkgName }
- if (!installedExt.isUnofficial && availableExt == null != installedExt.isObsolete) {
+ if (availableExt == null != installedExt.isObsolete) {
mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
changed = true
}
if (availableExt != null) {
val hasUpdate = installedExt.updateExists(availableExt)
if (installedExt.hasUpdate != hasUpdate) {
- mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
+ mutInstalledExtensions[index] = installedExt.copy(
+ hasUpdate = hasUpdate,
+ repoUrl = availableExt.repoUrl,
+ )
hasUpdateCount++
changed = true
}
@@ -303,34 +311,30 @@ class ExtensionManager(
}
/**
- * Adds the given signature to the list of trusted signatures. It also loads in background the
- * extensions that match this signature.
+ * Adds the given extension to the list of trusted extensions. It also loads in background the
+ * now trusted extensions.
*
- * @param signature The signature to whitelist.
+ * @param pkgName the package name of the extension to trust
+ * @param versionCode the version code of the extension to trust
+ * @param signatureHash the signature hash of the extension to trust
*/
- fun trustSignature(signature: String) {
- val untrustedSignatures = untrustedExtensionsFlow.value.map { it.signatureHash }.toSet()
- if (signature !in untrustedSignatures) return
+ fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
+ val untrustedPkgNames = untrustedExtensionsFlow.value.map { it.pkgName }.toSet()
+ if (pkgName !in untrustedPkgNames) return
- ExtensionLoader.trustedSignatures += signature
- val preference = preferences.trustedSignatures()
- preference.set(preference.get() + signature)
+ trustExtension.trust(pkgName, versionCode, signatureHash)
- val nowTrustedExtensions = untrustedExtensionsFlow.value.filter { it.signatureHash == signature }
+ val nowTrustedExtensions = untrustedExtensionsFlow.value
+ .filter { it.pkgName == pkgName && it.versionCode == versionCode }
_untrustedExtensionsFlow.value -= nowTrustedExtensions
- val ctx = context
launchNow {
nowTrustedExtensions
.map { extension ->
- async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
- }
- .map { it.await() }
- .forEach { result ->
- if (result is LoadResult.Success) {
- registerNewExtension(result.extension)
- }
+ async { ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) }.await()
}
+ .filterIsInstance()
+ .forEach { registerNewExtension(it.extension) }
}
}
@@ -422,7 +426,7 @@ class ExtensionManager(
private fun Extension.Installed.updateExists(availableExtension: Extension.Available? = null): Boolean {
val availableExt = availableExtension ?: availableExtensionsFlow.value.find { it.pkgName == pkgName }
- if (isUnofficial || availableExt == null) return false
+ ?: return false
return (availableExt.versionCode > versionCode || availableExt.libVersion > libVersion)
}
@@ -435,6 +439,7 @@ class ExtensionManager(
val name: String,
val versionCode: Long,
val libVersion: Double,
+ val repoUrl: String,
) : Parcelable {
constructor(extension: Extension.Available) : this(
apkName = extension.apkName,
@@ -442,6 +447,7 @@ class ExtensionManager(
name = extension.name,
versionCode = extension.versionCode,
libVersion = extension.libVersion,
+ repoUrl = extension.repoUrl,
)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt
index 1a80bb6024bf..7774dad1667f 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt
@@ -24,7 +24,7 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
-import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
+import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
@@ -44,7 +44,7 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
override suspend fun doWork(): Result = coroutineScope {
val pendingUpdates = try {
- ExtensionGithubApi().checkForUpdates(context)
+ ExtensionApi().checkForUpdates(context)
} catch (e: Exception) {
return@coroutineScope Result.failure()
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt
similarity index 66%
rename from app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
rename to app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt
index d072c8b99961..8996264a4db8 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt
@@ -1,13 +1,14 @@
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
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
-import eu.kanade.tachiyomi.network.await
+import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.serialization.Serializable
@@ -17,44 +18,19 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
-internal class ExtensionGithubApi {
+internal class ExtensionApi {
private val json: Json by injectLazy()
private val networkService: NetworkHelper by injectLazy()
-
- private var requiresFallbackSource = false
+ private val preferences: PreferencesHelper by injectLazy()
suspend fun findExtensions(): List {
return withIOContext {
- val githubResponse = if (requiresFallbackSource) {
- null
- } else {
- try {
- networkService.client
- .newCall(GET("${REPO_URL_PREFIX}index.min.json"))
- .await()
- } catch (e: Throwable) {
- Timber.e(e, "Failed to get extensions from GitHub")
- requiresFallbackSource = true
- null
- }
- }
-
- val response = githubResponse ?: run {
- networkService.client
- .newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
- .await()
- }
-
- val extensions = with(json) {
- response
- .parseAs>()
- .toExtensions()
- }
+ val extensions = preferences.extensionRepos().get().flatMap { getExtensions(it) }
// Sanity check - a small number of extensions probably means something broke
// with the repo generator
- if (extensions.size < 100) {
+ if (extensions.isEmpty()) {
throw Exception()
}
@@ -62,6 +38,23 @@ internal class ExtensionGithubApi {
}
}
+ private suspend fun getExtensions(repoBaseUrl: String): List {
+ return try {
+ val response = networkService.client
+ .newCall(GET("$repoBaseUrl/index.min.json"))
+ .awaitSuccess()
+
+ with(json) {
+ response
+ .parseAs>()
+ .toExtensions(repoBaseUrl)
+ }
+ } catch (e: Throwable) {
+ Timber.e(e, "Failed to get extensions from $repoBaseUrl")
+ emptyList()
+ }
+ }
+
suspend fun checkForUpdates(context: Context, prefetchedExtensions: List? = null): List {
return withIOContext {
val extensions = prefetchedExtensions ?: findExtensions()
@@ -79,7 +72,7 @@ internal class ExtensionGithubApi {
val availableExt = extensions.find { it.pkgName == pkgName } ?: continue
val hasUpdatedVer = availableExt.versionCode > installedExt.versionCode
val hasUpdatedLib = availableExt.libVersion > installedExt.libVersion
- val hasUpdate = installedExt.isUnofficial.not() && (hasUpdatedVer || hasUpdatedLib)
+ val hasUpdate = hasUpdatedVer || hasUpdatedLib
if (hasUpdate) {
extensionsWithUpdate.add(availableExt)
}
@@ -89,7 +82,7 @@ internal class ExtensionGithubApi {
}
}
- private fun List.toExtensions(): List {
+ private fun List.toExtensions(repoUrl: String): List {
return this
.filter {
val libVersion = it.extractLibVersion()
@@ -104,25 +97,16 @@ internal class ExtensionGithubApi {
libVersion = it.extractLibVersion(),
lang = it.lang,
isNsfw = it.nsfw == 1,
- hasReadme = it.hasReadme == 1,
- hasChangelog = it.hasChangelog == 1,
sources = it.sources ?: emptyList(),
apkName = it.apk,
- iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
+ iconUrl = "$repoUrl/icon/${it.pkg}.png",
+ repoUrl = repoUrl,
)
}
}
fun getApkUrl(extension: ExtensionManager.ExtensionInfo): String {
- return "${getUrlPrefix()}apk/${extension.apkName}"
- }
-
- private fun getUrlPrefix(): String {
- return if (requiresFallbackSource) {
- FALLBACK_REPO_URL_PREFIX
- } else {
- REPO_URL_PREFIX
- }
+ return "${extension.repoUrl}/apk/${extension.apkName}"
}
private fun ExtensionJsonObject.extractLibVersion(): Double {
@@ -130,9 +114,6 @@ internal class ExtensionGithubApi {
}
}
-private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
-private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/"
-
@Serializable
private data class ExtensionJsonObject(
val name: String,
@@ -142,7 +123,5 @@ private data class ExtensionJsonObject(
val code: Long,
val version: String,
val nsfw: Int,
- val hasReadme: Int = 0,
- val hasChangelog: Int = 0,
val sources: List?,
)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt
index aaedfa516787..ba20b6a544e1 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt
@@ -13,8 +13,6 @@ sealed class Extension {
abstract val libVersion: Double
abstract val lang: String?
abstract val isNsfw: Boolean
- abstract val hasReadme: Boolean
- abstract val hasChangelog: Boolean
data class Installed(
override val name: String,
@@ -24,15 +22,13 @@ sealed class Extension {
override val libVersion: Double,
override val lang: String,
override val isNsfw: Boolean,
- override val hasReadme: Boolean,
- override val hasChangelog: Boolean,
val pkgFactory: String?,
val sources: List