From 534642ea57117a807ecc574f7e81b78655aaec17 Mon Sep 17 00:00:00 2001 From: ziro Date: Fri, 12 Jan 2024 21:08:09 +0700 Subject: [PATCH] feat: Extension repo backend --- app/build.gradle.kts | 5 +- .../repo/ExtensionRepoController.kt} | 6 +- .../repo/ExtensionRepoScreen.kt} | 4 +- .../extension/repo/ExtensionRepoViewModel.kt | 72 ++++++++++++++++++ .../tachiyomi/extension/ExtensionManager.kt | 10 ++- .../tachiyomi/extension/ExtensionUpdateJob.kt | 4 +- ...{ExtensionGithubApi.kt => ExtensionApi.kt} | 76 ++++++++----------- .../tachiyomi/extension/model/Extension.kt | 2 + .../details/ExtensionDetailsController.kt | 4 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 4 +- .../ui/setting/SettingsBrowseController.kt | 4 +- gradle/compose.versions.toml | 2 + 12 files changed, 132 insertions(+), 61 deletions(-) rename app/src/main/java/dev/yokai/presentation/{source/SourceRepoController.kt => extension/repo/ExtensionRepoController.kt} (80%) rename app/src/main/java/dev/yokai/presentation/{source/SourceRepoScreen.kt => extension/repo/ExtensionRepoScreen.kt} (94%) create mode 100644 app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt rename app/src/main/java/eu/kanade/tachiyomi/extension/api/{ExtensionGithubApi.kt => ExtensionApi.kt} (71%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 94c1f251ca..3b471557ec 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,8 +35,8 @@ android { minSdk = AndroidConfig.minSdk targetSdk = AndroidConfig.targetSdk applicationId = "eu.kanade.tachiyomi" - versionCode = 111 - versionName = "1.7.4" + versionCode = 112 + versionName = "1.7.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled = true @@ -179,6 +179,7 @@ dependencies { implementation(libs.firebase.crashlytics) implementation(androidx.lifecycle.viewmodel) + implementation(compose.lifecycle.viewmodel) implementation(androidx.lifecycle.livedata) implementation(androidx.lifecycle.common) implementation(androidx.lifecycle.process) diff --git a/app/src/main/java/dev/yokai/presentation/source/SourceRepoController.kt b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoController.kt similarity index 80% rename from app/src/main/java/dev/yokai/presentation/source/SourceRepoController.kt rename to app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoController.kt index d87e616915..8055879e20 100644 --- a/app/src/main/java/dev/yokai/presentation/source/SourceRepoController.kt +++ b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoController.kt @@ -1,10 +1,10 @@ -package dev.yokai.presentation.source +package dev.yokai.presentation.extension.repo import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController -class SourceRepoController : +class ExtensionRepoController : BaseComposeController() { override fun getTitle(): String { @@ -14,7 +14,7 @@ class SourceRepoController : @Preview @Composable override fun ScreenContent() { - SourceRepoScreen( + ExtensionRepoScreen( title = getTitle(), onBackPress = router::handleBack, ) diff --git a/app/src/main/java/dev/yokai/presentation/source/SourceRepoScreen.kt b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoScreen.kt similarity index 94% rename from app/src/main/java/dev/yokai/presentation/source/SourceRepoScreen.kt rename to app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoScreen.kt index 072df24b7e..efc5e641b4 100644 --- a/app/src/main/java/dev/yokai/presentation/source/SourceRepoScreen.kt +++ b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoScreen.kt @@ -1,4 +1,4 @@ -package dev.yokai.presentation.source +package dev.yokai.presentation.extension.repo import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -14,7 +14,7 @@ import dev.yokai.presentation.YokaiScaffold import eu.kanade.tachiyomi.util.system.toast @Composable -fun SourceRepoScreen( +fun ExtensionRepoScreen( title: String, onBackPress: () -> Unit, ) { diff --git a/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt new file mode 100644 index 0000000000..caf1d35b49 --- /dev/null +++ b/app/src/main/java/dev/yokai/presentation/extension/repo/ExtensionRepoViewModel.kt @@ -0,0 +1,72 @@ +package dev.yokai.presentation.extension.repo + +import androidx.compose.runtime.Immutable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dev.yokai.domain.source.SourcePreferences +import eu.kanade.tachiyomi.data.preference.minusAssign +import eu.kanade.tachiyomi.data.preference.plusAssign +import eu.kanade.tachiyomi.util.system.launchIO +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import okhttp3.internal.toImmutableList +import uy.kohesive.injekt.injectLazy + +class ExtensionRepoViewModel : + ViewModel() { + + private val sourcePreferences: SourcePreferences by injectLazy() + private val _repoState: MutableStateFlow = MutableStateFlow(ExtensionRepoState.Loading) + val repoState: StateFlow = _repoState.asStateFlow() + + init { + viewModelScope.launchIO { + getRepo().collectLatest { repos -> + _repoState.value = ExtensionRepoState.Success(repos = repos.toImmutableList()) + } + } + } + + /* + fun addRepo(url: String): Result { + viewModelScope.launchIO { + if (!url.matches(repoRegex)) + return Result.InvalidUrl + + sourcePreferences.extensionRepos() += url.substringBeforeLast("/index.min.json") + + return Result.Success + } + } + */ + + fun deleteRepo(repo: String) { + viewModelScope.launchIO { + sourcePreferences.extensionRepos() -= repo + } + } + + fun getRepo() = + sourcePreferences.extensionRepos().changes() + .map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) } +} + +sealed class ExtensionRepoState { + + @Immutable + data object Loading : ExtensionRepoState() + + @Immutable + data class Success( + val repos: List, + ) : ExtensionRepoState() { + + val isEmpty: Boolean + get() = repos.isEmpty() + } +} + +private val repoRegex = """^https://.*/index\.min\.json$""".toRegex() diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index b7a4fa67c9..9e67d1975f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -4,8 +4,11 @@ import android.content.Context import android.graphics.drawable.Drawable import android.os.Build import android.os.Parcelable +import dev.yokai.domain.source.SourcePreferences import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi +import eu.kanade.tachiyomi.data.preference.minusAssign +import eu.kanade.tachiyomi.data.preference.plusAssign +import eu.kanade.tachiyomi.extension.api.ExtensionApi import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.LoadResult @@ -20,6 +23,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.parcelize.Parcelize import timber.log.Timber import uy.kohesive.injekt.Injekt @@ -44,7 +48,7 @@ class ExtensionManager( /** * API where all the available extensions can be found. */ - private val api = ExtensionGithubApi() + private val api = ExtensionApi() /** * The installer which installs, updates and uninstalls the extensions. @@ -435,6 +439,7 @@ class ExtensionManager( val name: String, val versionCode: Long, val libVersion: Double, + val repoUrl: String? = null, ) : 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 1a80bb6024..7774dad166 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 71% 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 59ae42b4f4..bcc9f126f8 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 dev.yokai.domain.source.SourcePreferences 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 sourcePreferences: SourcePreferences 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 = sourcePreferences.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.size < 50) { throw Exception() } @@ -62,6 +38,25 @@ 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() @@ -89,7 +84,7 @@ internal class ExtensionGithubApi { } } - private fun List.toExtensions(): List { + private fun List.toExtensions(repoUrl: String): List { return this .filter { val libVersion = it.extractLibVersion() @@ -108,21 +103,14 @@ internal class ExtensionGithubApi { 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,8 +118,10 @@ internal class ExtensionGithubApi { } } -const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/keiyoushi/extensions/repo/" -const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/keiyoushi/extensions@repo/" +private const val BASE_URL = "https://raw.githubusercontent.com/" +private const val REPO_URL_PREFIX = "${BASE_URL}keiyoushi/extensions/repo/" +private const val FALLBACK_BASE_URL = "https://gcore.jsdelivr.net/gh/" +private const val FALLBACK_REPO_URL_PREFIX = "${FALLBACK_BASE_URL}keiyoushi/extensions@repo/" @Serializable private data class ExtensionJsonObject( 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 aaedfa5167..5bca1249c1 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 @@ -33,6 +33,7 @@ sealed class Extension { val isObsolete: Boolean = false, val isUnofficial: Boolean = false, val isShared: Boolean, + val repoUrl: String? = null, ) : Extension() data class Available( @@ -48,6 +49,7 @@ sealed class Extension { val apkName: String, val iconUrl: String, val sources: List, + val repoUrl: String? = null, ) : Extension() @Serializable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt index 672323e38c..f3b7b1fe40 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/details/ExtensionDetailsController.kt @@ -164,9 +164,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : } private fun openRepo() { - // TODO - // val url = getUrl(extension.repoUrl) - val url = getUrl() + val url = getUrl(presenter.extension?.repoUrl) openInBrowser(url) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index d1442a3911..b60ab1cd05 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -85,7 +85,7 @@ import eu.kanade.tachiyomi.data.updater.AppUpdateResult import eu.kanade.tachiyomi.data.updater.RELEASE_URL import eu.kanade.tachiyomi.databinding.MainActivityBinding import eu.kanade.tachiyomi.extension.ExtensionManager -import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi +import eu.kanade.tachiyomi.extension.api.ExtensionApi import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface @@ -955,7 +955,7 @@ open class MainActivity : BaseActivity() { lifecycleScope.launch(Dispatchers.IO) { try { extensionManager.findAvailableExtensions() - val pendingUpdates = ExtensionGithubApi().checkForUpdates( + val pendingUpdates = ExtensionApi().checkForUpdates( this@MainActivity, extensionManager.availableExtensionsFlow.value.takeIf { it.isNotEmpty() }, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt index fb910d66f2..a5d5bb6cd8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsBrowseController.kt @@ -6,7 +6,7 @@ import android.os.Build import android.provider.Settings import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat -import dev.yokai.presentation.source.SourceRepoController +import dev.yokai.presentation.extension.repo.ExtensionRepoController import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.notification.Notifications @@ -42,7 +42,7 @@ class SettingsBrowseController : SettingsController() { titleRes = R.string.extensions preference { titleRes = R.string.source_repos - onClick { router.pushController(SourceRepoController().withFadeTransaction()) } + onClick { router.pushController(ExtensionRepoController().withFadeTransaction()) } // TODO: Enable once it's finished summary = "Temporarily disabled, will be enabled once it's fully implemented" isEnabled = BuildConfig.DEBUG diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 29dc117ae1..d3198b44b5 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,11 +1,13 @@ [versions] compose = "1.5.3" +lifecycle = "2.6.2" [libraries] animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } material = { module = "androidx.compose.material:material", version.ref = "compose" } material3 = { module = "androidx.compose.material3:material3", version = "1.1.2" } +lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }