mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 02:34:39 +00:00
feat: Extension repo backend
This commit is contained in:
parent
7c88f3895a
commit
534642ea57
12 changed files with 132 additions and 61 deletions
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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,
|
||||
) {
|
|
@ -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()
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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(
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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() },
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue