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
This commit is contained in:
arkon 2024-01-05 23:13:16 -05:00
parent 556f5a42a7
commit 9c899e97a9
20 changed files with 252 additions and 183 deletions

View file

@ -22,7 +22,7 @@ android {
defaultConfig {
applicationId = "eu.kanade.tachiyomi"
versionCode = 113
versionCode = 114
versionName = "0.14.7"
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()
sealed interface Result {
data object InvalidUrl : 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()
}
}
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,12 +67,9 @@ 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),
@ -87,7 +80,7 @@ fun CategoryCreateDialog(
},
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
alreadyExistsError
MR.strings.error_category_exists
} else {
MR.strings.information_required_plain
}
@ -96,7 +89,6 @@ fun CategoryCreateDialog(
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

@ -9,8 +9,8 @@ import androidx.fragment.app.FragmentActivity
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences
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.util.system.AuthenticatorUtil.authenticate
import kotlinx.collections.immutable.persistentListOf
import tachiyomi.core.i18n.stringResource
@ -47,7 +47,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

@ -405,6 +405,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

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context
import android.graphics.drawable.Drawable
import eu.kanade.domain.source.service.SourcePreferences
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
@ -49,7 +49,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
@ -21,7 +22,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()
@ -33,52 +34,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()
}
@ -86,6 +51,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,
@ -127,8 +112,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 {
@ -146,9 +131,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,
)
}
}
@ -157,24 +142,11 @@ 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()
}
}
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.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
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
@ -337,7 +337,7 @@ class MainActivity : BaseActivity() {
// Extensions updates
LaunchedEffect(Unit) {
try {
ExtensionGithubApi().checkForUpdates(context)
ExtensionApi().checkForUpdates(context)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}

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>