Clean up external repos

- Accept full URL as input instead, which allows for non-GitHub
- Remove automatic CDN fallback in favor of adding that as an external repo if needed

(cherry picked from commit 9c899e97a97480545d022974ffd3ea1248634155)

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt
#	app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
This commit is contained in:
arkon 2024-01-05 23:13:16 -05:00 committed by Jobobby04
parent aaeac1b10c
commit 1ec0a22e37
22 changed files with 257 additions and 185 deletions

View File

@ -26,7 +26,7 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi.sy"
versionCode = 59
versionCode = 60
versionName = "1.9.4"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")

View File

@ -12,7 +12,7 @@ import eu.kanade.domain.manga.interactor.SetExcludedScanlators
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.source.interactor.CreateSourceRepo
import eu.kanade.domain.source.interactor.DeleteSourceRepos
import eu.kanade.domain.source.interactor.DeleteSourceRepo
import eu.kanade.domain.source.interactor.GetEnabledSources
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.GetSourceRepos
@ -172,7 +172,7 @@ class DomainModule : InjektModule {
addFactory { ToggleSourcePin(get()) }
addFactory { CreateSourceRepo(get()) }
addFactory { DeleteSourceRepos(get()) }
addFactory { DeleteSourceRepo(get()) }
addFactory { GetSourceRepos(get()) }
}
}

View File

@ -7,28 +7,20 @@ class CreateSourceRepo(private val preferences: SourcePreferences) {
fun await(name: String): Result {
// Do not allow invalid formats
if (!name.matches(repoRegex)) {
return Result.InvalidName
if (!name.matches(repoRegex) || name.startsWith(OFFICIAL_REPO_BASE_URL)) {
return Result.InvalidUrl
}
preferences.extensionRepos() += name
preferences.extensionRepos() += name.substringBeforeLast("/index.min.json")
return Result.Success
}
sealed class Result {
data object InvalidName : Result()
data object Success : Result()
}
/**
* Returns true if a repo with the given name already exists.
*/
private fun repoExists(name: String): Boolean {
return preferences.extensionRepos().get().any { it.equals(name, true) }
}
companion object {
val repoRegex = """^[a-zA-Z0-9-_.]*?\/[a-zA-Z0-9-_.]*?$""".toRegex()
sealed interface Result {
data object InvalidUrl : Result
data object Success : Result
}
}
const val OFFICIAL_REPO_BASE_URL = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo"
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()

View File

@ -0,0 +1,11 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
import tachiyomi.core.preference.minusAssign
class DeleteSourceRepo(private val preferences: SourcePreferences) {
fun await(repo: String) {
preferences.extensionRepos() -= repo
}
}

View File

@ -1,12 +0,0 @@
package eu.kanade.domain.source.interactor
import eu.kanade.domain.source.service.SourcePreferences
class DeleteSourceRepos(private val preferences: SourcePreferences) {
fun await(repos: List<String>) {
preferences.extensionRepos().set(
preferences.extensionRepos().get().filterNot { it in repos }.toSet(),
)
}
}

View File

@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.map
class GetSourceRepos(private val preferences: SourcePreferences) {
fun subscribe(): Flow<List<String>> {
return preferences.extensionRepos().changes().map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
return preferences.extensionRepos().changes()
.map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) }
}
}

View File

@ -25,7 +25,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import dev.icerock.moko.resources.StringResource
import eu.kanade.core.preference.asToggleableState
import eu.kanade.presentation.category.visualName
import kotlinx.collections.immutable.ImmutableList
@ -43,9 +42,6 @@ fun CategoryCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
categories: ImmutableList<String>,
title: String,
extraMessage: String? = null,
alreadyExistsError: StringResource = MR.strings.error_category_exists,
) {
var name by remember { mutableStateOf("") }
@ -71,32 +67,28 @@ fun CategoryCreateDialog(
}
},
title = {
Text(text = title)
Text(text = stringResource(MR.strings.action_add_category))
},
text = {
Column {
extraMessage?.let { Text(it) }
OutlinedTextField(
modifier = Modifier
.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
label = {
Text(text = stringResource(MR.strings.name))
},
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
alreadyExistsError
} else {
MR.strings.information_required_plain
}
Text(text = stringResource(msgRes))
},
isError = name.isNotEmpty() && nameAlreadyExists,
singleLine = true,
)
}
OutlinedTextField(
modifier = Modifier
.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
label = {
Text(text = stringResource(MR.strings.name))
},
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
MR.strings.error_category_exists
} else {
MR.strings.information_required_plain
}
Text(text = stringResource(msgRes))
},
isError = name.isNotEmpty() && nameAlreadyExists,
singleLine = true,
)
},
)
@ -113,7 +105,6 @@ fun CategoryRenameDialog(
onRename: (String) -> Unit,
categories: ImmutableList<String>,
category: String,
alreadyExistsError: StringResource = MR.strings.error_category_exists,
) {
var name by remember { mutableStateOf(category) }
var valueHasChanged by remember { mutableStateOf(false) }
@ -153,7 +144,7 @@ fun CategoryRenameDialog(
label = { Text(text = stringResource(MR.strings.name)) },
supportingText = {
val msgRes = if (valueHasChanged && nameAlreadyExists) {
alreadyExistsError
MR.strings.error_category_exists
} else {
MR.strings.information_required_plain
}
@ -176,8 +167,7 @@ fun CategoryRenameDialog(
fun CategoryDeleteDialog(
onDismissRequest: () -> Unit,
onDelete: () -> Unit,
title: String,
text: String,
category: String,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
@ -195,10 +185,10 @@ fun CategoryDeleteDialog(
}
},
title = {
Text(text = title)
Text(text = stringResource(MR.strings.delete_category))
},
text = {
Text(text = text)
Text(text = stringResource(MR.strings.delete_category_confirmation, category))
},
)
}

View File

@ -49,7 +49,7 @@ fun CategoryListItem(
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Text(
text = category.name,
modifier = Modifier
@ -61,13 +61,13 @@ fun CategoryListItem(
onClick = { onMoveUp(category) },
enabled = canMoveUp,
) {
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
}
IconButton(
onClick = { onMoveDown(category) },
enabled = canMoveDown,
) {
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
}
Spacer(modifier = Modifier.weight(1f))
IconButton(onClick = onRename) {

View File

@ -10,8 +10,8 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.presentation.category.repos.RepoScreen
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
import kotlinx.collections.immutable.persistentListOf
@ -97,7 +97,7 @@ object SettingsBrowseScreen : SearchableSettings {
title = stringResource(MR.strings.label_extension_repos),
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.size, reposCount.size),
onClick = {
navigator.push(RepoScreen())
navigator.push(ExtensionReposScreen())
},
),
),

View File

@ -1,4 +1,4 @@
package eu.kanade.presentation.category.repos
package eu.kanade.presentation.more.settings.screen.browse
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -8,23 +8,21 @@ import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.presentation.category.SourceRepoScreen
import eu.kanade.presentation.category.components.CategoryCreateDialog
import eu.kanade.presentation.category.components.CategoryDeleteDialog
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.LoadingScreen
class RepoScreen : Screen() {
class ExtensionReposScreen : Screen() {
@Composable
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { RepoScreenModel() }
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
val state by screenModel.state.collectAsState()
@ -35,7 +33,7 @@ class RepoScreen : Screen() {
val successState = state as RepoScreenState.Success
SourceRepoScreen(
ExtensionReposScreen(
state = successState,
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
@ -45,21 +43,17 @@ class RepoScreen : Screen() {
when (val dialog = successState.dialog) {
null -> {}
RepoDialog.Create -> {
CategoryCreateDialog(
ExtensionRepoCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createRepo(it) },
categories = successState.repos,
title = stringResource(MR.strings.action_add_repo),
extraMessage = stringResource(MR.strings.action_add_repo_message),
alreadyExistsError = MR.strings.error_repo_exists,
)
}
is RepoDialog.Delete -> {
CategoryDeleteDialog(
ExtensionRepoDeleteDialog(
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteRepos(listOf(dialog.repo)) },
title = stringResource(MR.strings.action_delete_repo),
text = stringResource(MR.strings.delete_repo_confirmation, dialog.repo),
onDelete = { screenModel.deleteRepo(dialog.repo) },
repo = dialog.repo,
)
}
}

View File

@ -1,11 +1,11 @@
package eu.kanade.presentation.category.repos
package eu.kanade.presentation.more.settings.screen.browse
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import dev.icerock.moko.resources.StringResource
import eu.kanade.domain.source.interactor.CreateSourceRepo
import eu.kanade.domain.source.interactor.DeleteSourceRepos
import eu.kanade.domain.source.interactor.DeleteSourceRepo
import eu.kanade.domain.source.interactor.GetSourceRepos
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -18,10 +18,10 @@ import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class RepoScreenModel(
class ExtensionReposScreenModel(
private val getSourceRepos: GetSourceRepos = Injekt.get(),
private val createSourceRepo: CreateSourceRepo = Injekt.get(),
private val deleteSourceRepos: DeleteSourceRepos = Injekt.get(),
private val deleteSourceRepo: DeleteSourceRepo = Injekt.get(),
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
@ -48,20 +48,20 @@ class RepoScreenModel(
fun createRepo(name: String) {
screenModelScope.launchIO {
when (createSourceRepo.await(name)) {
is CreateSourceRepo.Result.InvalidName -> _events.send(RepoEvent.InvalidName)
is CreateSourceRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
else -> {}
}
}
}
/**
* Deletes the given repos from the database.
* Deletes the given repo from the database.
*
* @param repos The list of repos to delete.
* @param repo The repo to delete.
*/
fun deleteRepos(repos: List<String>) {
fun deleteRepo(repo: String) {
screenModelScope.launchIO {
deleteSourceRepos.await(repos)
deleteSourceRepo.await(repo)
}
}
@ -86,8 +86,7 @@ class RepoScreenModel(
sealed class RepoEvent {
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
data object InvalidName : LocalizedMessage(MR.strings.invalid_repo_name)
data object InternalError : LocalizedMessage(MR.strings.internal_error)
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
}
sealed class RepoDialog {

View File

@ -1,9 +1,8 @@
package eu.kanade.presentation.category.components.repo
package eu.kanade.presentation.more.settings.screen.browse.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -24,7 +23,7 @@ import kotlinx.collections.immutable.ImmutableList
import tachiyomi.presentation.core.components.material.padding
@Composable
fun SourceRepoContent(
fun ExtensionReposContent(
repos: ImmutableList<String>,
lazyListState: LazyListState,
paddingValues: PaddingValues,
@ -38,7 +37,7 @@ fun SourceRepoContent(
modifier = modifier,
) {
items(repos) { repo ->
SourceRepoListItem(
ExtensionRepoListItem(
modifier = Modifier.animateItemPlacement(),
repo = repo,
onDelete = { onClickDelete(repo) },
@ -48,7 +47,7 @@ fun SourceRepoContent(
}
@Composable
private fun SourceRepoListItem(
private fun ExtensionRepoListItem(
repo: String,
onDelete: () -> Unit,
modifier: Modifier = Modifier,
@ -66,13 +65,16 @@ private fun SourceRepoListItem(
),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = "")
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Text(text = repo, modifier = Modifier.padding(start = MaterialTheme.padding.medium))
}
Row {
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
IconButton(onClick = onDelete) {
Icon(imageVector = Icons.Outlined.Delete, contentDescription = "")
Icon(imageVector = Icons.Outlined.Delete, contentDescription = null)
}
}
}

View File

@ -0,0 +1,117 @@
package eu.kanade.presentation.more.settings.screen.browse.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import kotlin.time.Duration.Companion.seconds
@Composable
fun ExtensionRepoCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
categories: ImmutableList<String>,
) {
var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.contains(name) }
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
enabled = name.isNotEmpty() && !nameAlreadyExists,
onClick = {
onCreate(name)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_add))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_add_repo))
},
text = {
Column {
Text(text = stringResource(MR.strings.action_add_repo_message))
OutlinedTextField(
modifier = Modifier
.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
label = {
Text(text = stringResource(MR.strings.label_add_repo_input))
},
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
MR.strings.error_repo_exists
} else {
MR.strings.information_required_plain
}
Text(text = stringResource(msgRes))
},
isError = name.isNotEmpty() && nameAlreadyExists,
singleLine = true,
)
}
},
)
LaunchedEffect(focusRequester) {
// TODO: https://issuetracker.google.com/issues/204502668
delay(0.1.seconds)
focusRequester.requestFocus()
}
}
@Composable
fun ExtensionRepoDeleteDialog(
onDismissRequest: () -> Unit,
onDelete: () -> Unit,
repo: String,
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = {
onDelete()
onDismissRequest()
}) {
Text(text = stringResource(MR.strings.action_ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
title = {
Text(text = stringResource(MR.strings.action_delete_repo))
},
text = {
Text(text = stringResource(MR.strings.delete_repo_confirmation, repo))
},
)
}

View File

@ -1,4 +1,6 @@
package eu.kanade.presentation.category
@file:JvmName("ExtensionReposScreenKt")
package eu.kanade.presentation.more.settings.screen.browse.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
@ -7,9 +9,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.category.components.repo.SourceRepoContent
import eu.kanade.presentation.category.repos.RepoScreenState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
@ -19,7 +20,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.util.plus
@Composable
fun SourceRepoScreen(
fun ExtensionReposScreen(
state: RepoScreenState.Success,
onClickCreate: () -> Unit,
onClickDelete: (String) -> Unit,
@ -49,7 +50,7 @@ fun SourceRepoScreen(
return@Scaffold
}
SourceRepoContent(
ExtensionReposContent(
repos = state.repos,
lazyListState = lazyListState,
paddingValues = paddingValues + topSmallPaddingValues +

View File

@ -408,6 +408,11 @@ object Migrations {
// Deleting old download cache index files, but might as well clear it all out
context.cacheDir.deleteRecursively()
}
if (oldVersion < 114) {
sourcePreferences.extensionRepos().getAndSet {
it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet()
}
}
return true
}

View File

@ -5,7 +5,7 @@ import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
@ -61,7 +61,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.

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
import eu.kanade.domain.source.interactor.OFFICIAL_REPO_BASE_URL
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
@ -22,7 +23,7 @@ import uy.kohesive.injekt.injectLazy
import java.time.Instant
import kotlin.time.Duration.Companion.days
internal class ExtensionGithubApi {
internal class ExtensionApi {
private val networkService: NetworkHelper by injectLazy()
private val preferenceStore: PreferenceStore by injectLazy()
@ -34,52 +35,16 @@ internal class ExtensionGithubApi {
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
}
private var requiresFallbackSource = false
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"))
.awaitSuccess()
} catch (e: Throwable) {
logcat(LogPriority.ERROR, 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"))
.awaitSuccess()
}
val extensions = with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.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)
}
val extensions = buildList {
addAll(getExtensions(OFFICIAL_REPO_BASE_URL, true))
sourcePreferences.extensionRepos().get().map { addAll(getExtensions(it, false)) }
}
// 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()
}
@ -87,6 +52,26 @@ internal class ExtensionGithubApi {
}
}
private suspend fun getExtensions(
repoBaseUrl: String,
isOfficialRepo: Boolean,
): List<Extension.Available> {
return try {
val response = networkService.client
.newCall(GET("$repoBaseUrl/index.min.json"))
.awaitSuccess()
with(json) {
response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions(repoBaseUrl, isRepoSource = !isOfficialRepo)
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" }
emptyList()
}
}
suspend fun checkForUpdates(
context: Context,
fromAvailableExtensionList: Boolean = false,
@ -135,8 +120,8 @@ internal class ExtensionGithubApi {
}
private fun List<ExtensionJsonObject>.toExtensions(
repoUrl: String = getUrlPrefix(),
repoSource: Boolean = false,
repoUrl: String,
isRepoSource: Boolean,
): List<Extension.Available> {
return this
.filter {
@ -154,9 +139,9 @@ internal class ExtensionGithubApi {
isNsfw = it.nsfw == 1,
sources = it.sources?.map(extensionSourceMapper).orEmpty(),
apkName = it.apk,
iconUrl = "${repoUrl}icon/${it.pkg}.png",
iconUrl = "$repoUrl/icon/${it.pkg}.png",
repoUrl = repoUrl,
isRepoSource = repoSource,
isRepoSource = isRepoSource,
)
}
}
@ -165,14 +150,6 @@ internal class ExtensionGithubApi {
return "${extension.repoUrl}/apk/${extension.apkName}"
}
private fun getUrlPrefix(): String {
return if (requiresFallbackSource) {
FALLBACK_REPO_URL_PREFIX
} else {
REPO_URL_PREFIX
}
}
private fun ExtensionJsonObject.extractLibVersion(): Double {
return version.substringBeforeLast('.').toDouble()
}
@ -186,11 +163,6 @@ internal class ExtensionGithubApi {
// SY <--
}
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(
val name: String,

View File

@ -18,8 +18,6 @@ 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() {
@ -57,7 +55,6 @@ class CategoryScreen : Screen() {
onDismissRequest = screenModel::dismissDialog,
onCreate = screenModel::createCategory,
categories = successState.categories.fastMap { it.name }.toImmutableList(),
title = stringResource(MR.strings.action_add_category),
)
}
is CategoryDialog.Rename -> {
@ -72,8 +69,7 @@ class CategoryScreen : Screen() {
CategoryDeleteDialog(
onDismissRequest = screenModel::dismissDialog,
onDelete = { screenModel.deleteCategory(dialog.category.id) },
title = stringResource(MR.strings.delete_category),
text = stringResource(MR.strings.delete_category_confirmation, dialog.category.name),
category = dialog.category.name,
)
}
is CategoryDialog.SortAlphabetically -> {

View File

@ -65,7 +65,7 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreen
@ -402,7 +402,7 @@ class MainActivity : BaseActivity() {
// Extensions updates
LaunchedEffect(Unit) {
try {
ExtensionGithubApi().checkForUpdates(context)
ExtensionApi().checkForUpdates(context)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}

View File

@ -653,6 +653,11 @@ object EXHMigrations {
// Deleting old download cache index files, but might as well clear it all out
context.cacheDir.deleteRecursively()
}
if (oldVersion under 60) {
sourcePreferences.extensionRepos().getAndSet {
it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet()
}
}
// if (oldVersion under 1) { } (1 is current release version)
// do stuff here when releasing changed crap

View File

@ -91,6 +91,4 @@ class UnsortedPreferences(
)
fun allowLocalSourceHiddenFolders() = preferenceStore.getBoolean("allow_local_source_hidden_folders", false)
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
}

View File

@ -340,10 +340,11 @@
<string name="label_extension_repos">Extension repos</string>
<string name="information_empty_repos">You have no repos set.</string>
<string name="action_add_repo">Add repo</string>
<string name="action_add_repo_message">Add additional repos to Tachiyomi, the format of a repo is \"username/repo\", with username being the repo owner, and repo being the repo name.</string>
<string name="label_add_repo_input">Repo URL</string>
<string name="action_add_repo_message">Add additional repos to Tachiyomi. This should be a URL that ends with \"index.min.json\".</string>
<string name="error_repo_exists">This repo already exists!</string>
<string name="action_delete_repo">Delete repo</string>
<string name="invalid_repo_name">Invalid repo name</string>
<string name="invalid_repo_name">Invalid repo URL</string>
<string name="delete_repo_confirmation">Do you wish to delete the repo \"%s\"?</string>
<string name="repo_extension_message">This extension is from an external repo. Tap to view the repo.</string>