mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-12 12:08:56 +01:00
Support external repos
Largely taken from SY. Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user