Support external repos

Largely taken from SY.

Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
This commit is contained in:
arkon
2024-01-05 17:28:08 -05:00
parent 32bed9b041
commit c17ada2c98
20 changed files with 557 additions and 59 deletions

View File

@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
@@ -24,6 +25,7 @@ internal class ExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy()
private val sourcePreferences: SourcePreferences by injectLazy()
private val extensionManager: ExtensionManager by injectLazy()
private val json: Json by injectLazy()
@@ -58,7 +60,20 @@ internal class ExtensionGithubApi {
val extensions = with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions()
.toExtensions() + sourcePreferences.extensionRepos()
.get()
.flatMap { repoPath ->
val url = if (requiresFallbackSource) {
"$FALLBACK_BASE_URL$repoPath@repo/"
} else {
"$BASE_URL$repoPath/repo/"
}
networkService.client
.newCall(GET("${url}index.min.json"))
.awaitSuccess()
.parseAs<List<ExtensionJsonObject>>()
.toExtensions(url, repoSource = true)
}
}
// Sanity check - a small number of extensions probably means something broke
@@ -71,10 +86,7 @@ internal class ExtensionGithubApi {
}
}
suspend fun checkForUpdates(
context: Context,
fromAvailableExtensionList: Boolean = false,
): List<Extension.Installed>? {
suspend fun checkForUpdates(context: Context, fromAvailableExtensionList: Boolean = false): List<Extension.Installed>? {
// Limit checks to once a day at most
if (!fromAvailableExtensionList &&
Instant.now().toEpochMilli() < lastExtCheck.get() + 1.days.inWholeMilliseconds
@@ -111,7 +123,10 @@ internal class ExtensionGithubApi {
return extensionsWithUpdate
}
private fun List<ExtensionJsonObject>.toExtensions(): List<Extension.Available> {
private fun List<ExtensionJsonObject>.toExtensions(
repoUrl: String = getUrlPrefix(),
repoSource: Boolean = false,
): List<Extension.Available> {
return this
.filter {
val libVersion = it.extractLibVersion()
@@ -128,13 +143,15 @@ internal class ExtensionGithubApi {
isNsfw = it.nsfw == 1,
sources = it.sources?.map(extensionSourceMapper).orEmpty(),
apkName = it.apk,
iconUrl = "${getUrlPrefix()}icon/${it.pkg}.png",
iconUrl = "${repoUrl}icon/${it.pkg}.png",
repoUrl = repoUrl,
isRepoSource = repoSource,
)
}
}
fun getApkUrl(extension: Extension.Available): String {
return "${getUrlPrefix()}apk/${extension.apkName}"
return "${extension.repoUrl}/apk/${extension.apkName}"
}
private fun getUrlPrefix(): String {
@@ -150,8 +167,10 @@ 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/"
private const val BASE_URL = "https://raw.githubusercontent.com/"
private const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
private const val FALLBACK_BASE_URL = "https://gcore.jsdelivr.net/gh/"
private const val FALLBACK_REPO_URL_PREFIX = "${FALLBACK_BASE_URL}tachiyomiorg/tachiyomi-extensions@repo/"
@Serializable
private data class ExtensionJsonObject(

View File

@@ -29,6 +29,8 @@ sealed class Extension {
val isObsolete: Boolean = false,
val isUnofficial: Boolean = false,
val isShared: Boolean,
val repoUrl: String? = null,
val isRepoSource: Boolean = false,
) : Extension()
data class Available(
@@ -42,6 +44,8 @@ sealed class Extension {
val sources: List<Source>,
val apkName: String,
val iconUrl: String,
val repoUrl: String,
val isRepoSource: Boolean,
) : Extension() {
data class Source(

View File

@@ -24,6 +24,8 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
@@ -265,7 +267,10 @@ class BrowseSourceScreenModel(
else -> {
val preselectedIds = getCategories.await(manga.id).map { it.id }
setDialog(
Dialog.ChangeMangaCategory(manga, categories.mapAsCheckboxState { it.id in preselectedIds }),
Dialog.ChangeMangaCategory(
manga,
categories.mapAsCheckboxState { it.id in preselectedIds }.toImmutableList(),
),
)
}
}
@@ -338,7 +343,7 @@ class BrowseSourceScreenModel(
data class AddDuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
data class ChangeMangaCategory(
val manga: Manga,
val initialSelection: List<CheckboxState.State<Category>>,
val initialSelection: ImmutableList<CheckboxState.State<Category>>,
) : Dialog
data class Migrate(val newManga: Manga) : Dialog
}

View File

@@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
@@ -15,7 +16,10 @@ import eu.kanade.presentation.category.components.CategoryRenameDialog
import eu.kanade.presentation.category.components.CategorySortAlphabeticallyDialog
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.collectLatest
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
class CategoryScreen : Screen() {
@@ -52,22 +56,24 @@ class CategoryScreen : Screen() {
CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = screenModel::createCategory,
categories = successState.categories,
categories = successState.categories.fastMap { it.name }.toImmutableList(),
title = stringResource(MR.strings.action_add_category),
)
}
is CategoryDialog.Rename -> {
CategoryRenameDialog(
onDismissRequest = screenModel::dismissDialog,
onRename = { screenModel.renameCategory(dialog.category, it) },
categories = successState.categories,
category = dialog.category,
categories = successState.categories.fastMap { it.name }.toImmutableList(),
category = dialog.category.name,
)
}
is CategoryDialog.Delete -> {
CategoryDeleteDialog(
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteCategory(dialog.category.id) },
category = dialog.category,
title = stringResource(MR.strings.delete_category),
text = stringResource(MR.strings.delete_category_confirmation, dialog.category.name),
)
}
is CategoryDialog.SortAlphabetically -> {

View File

@@ -28,9 +28,11 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
@@ -661,13 +663,15 @@ class LibraryScreenModel(
val common = getCommonCategories(mangaList)
// Get indexes of the mix categories to preselect.
val mix = getMixCategories(mangaList)
val preselected = categories.map {
when (it) {
in common -> CheckboxState.State.Checked(it)
in mix -> CheckboxState.TriState.Exclude(it)
else -> CheckboxState.State.None(it)
val preselected = categories
.map {
when (it) {
in common -> CheckboxState.State.Checked(it)
in mix -> CheckboxState.TriState.Exclude(it)
else -> CheckboxState.State.None(it)
}
}
}
.toImmutableList()
mutableState.update { it.copy(dialog = Dialog.ChangeCategory(mangaList, preselected)) }
}
}
@@ -683,7 +687,10 @@ class LibraryScreenModel(
sealed interface Dialog {
data object SettingsSheet : Dialog
data class ChangeCategory(val manga: List<Manga>, val initialSelection: List<CheckboxState<Category>>) : Dialog
data class ChangeCategory(
val manga: List<Manga>,
val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog
data class DeleteManga(val manga: List<Manga>) : Dialog
}

View File

@@ -36,6 +36,8 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.util.chapter.getNextUnread
import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.catch
@@ -360,7 +362,7 @@ class MangaScreenModel(
successState.copy(
dialog = Dialog.ChangeCategory(
manga = manga,
initialSelection = categories.mapAsCheckboxState { it.id in selection },
initialSelection = categories.mapAsCheckboxState { it.id in selection }.toImmutableList(),
),
)
}
@@ -992,7 +994,10 @@ class MangaScreenModel(
// Track sheet - end
sealed interface Dialog {
data class ChangeCategory(val manga: Manga, val initialSelection: List<CheckboxState<Category>>) : Dialog
data class ChangeCategory(
val manga: Manga,
val initialSelection: ImmutableList<CheckboxState<Category>>,
) : Dialog
data class DeleteChapters(val chapters: List<Chapter>) : Dialog
data class DuplicateManga(val manga: Manga, val duplicate: Manga) : Dialog
data class SetFetchInterval(val manga: Manga) : Dialog