feat: Extension repo backend

This commit is contained in:
ziro 2024-01-12 21:08:09 +07:00
parent 7c88f3895a
commit 534642ea57
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
12 changed files with 132 additions and 61 deletions

View file

@ -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)

View file

@ -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,
)

View file

@ -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,
) {

View file

@ -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<ExtensionRepoState> = MutableStateFlow(ExtensionRepoState.Loading)
val repoState: StateFlow<ExtensionRepoState> = _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<String>,
) : ExtensionRepoState() {
val isEmpty: Boolean
get() = repos.isEmpty()
}
}
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()

View file

@ -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,
)
}

View file

@ -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()
}

View file

@ -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<Extension.Available> {
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<List<ExtensionJsonObject>>()
.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<Extension.Available> {
return try {
val response = networkService.client
.newCall(GET("$repoBaseUrl/index.min.json"))
.awaitSuccess()
with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions(repoBaseUrl)
}
} catch (e: Throwable) {
Timber.e(e, "Failed to get extensions from $repoBaseUrl")
emptyList()
}
}
suspend fun checkForUpdates(context: Context, prefetchedExtensions: List<Extension.Available>? = null): List<Extension.Available> {
return withIOContext {
val extensions = prefetchedExtensions ?: findExtensions()
@ -89,7 +84,7 @@ internal class ExtensionGithubApi {
}
}
private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
private fun List<ExtensionJsonObject>.toExtensions(repoUrl: String): List<Extension.Available> {
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(

View file

@ -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<AvailableSource>,
val repoUrl: String? = null,
) : Extension()
@Serializable

View file

@ -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)
}

View file

@ -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<MainActivityBinding>() {
lifecycleScope.launch(Dispatchers.IO) {
try {
extensionManager.findAvailableExtensions()
val pendingUpdates = ExtensionGithubApi().checkForUpdates(
val pendingUpdates = ExtensionApi().checkForUpdates(
this@MainActivity,
extensionManager.availableExtensionsFlow.value.takeIf { it.isNotEmpty() },
)

View file

@ -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

View file

@ -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" }