From 9c899e97a97480545d022974ffd3ea1248634155 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 5 Jan 2024 23:13:16 -0500 Subject: [PATCH] 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 --- app/build.gradle.kts | 2 +- .../java/eu/kanade/domain/DomainModule.kt | 4 +- .../source/interactor/CreateSourceRepo.kt | 26 ++-- .../source/interactor/DeleteSourceRepo.kt | 11 ++ .../source/interactor/DeleteSourceRepos.kt | 12 -- .../source/interactor/GetSourceRepos.kt | 3 +- .../category/components/CategoryDialogs.kt | 58 ++++----- .../category/components/CategoryListItem.kt | 6 +- .../settings/screen/SettingsBrowseScreen.kt | 4 +- .../screen/browse/ExtensionReposScreen.kt} | 28 ++--- .../browse/ExtensionReposScreenModel.kt} | 21 ++-- .../components/ExtensionReposContent.kt} | 20 +-- .../components/ExtensionReposDialogs.kt | 117 ++++++++++++++++++ .../components/ExtensionReposScreen.kt} | 11 +- .../java/eu/kanade/tachiyomi/Migrations.kt | 5 + .../tachiyomi/extension/ExtensionManager.kt | 4 +- ...{ExtensionGithubApi.kt => ExtensionApi.kt} | 88 +++++-------- .../tachiyomi/ui/category/CategoryScreen.kt | 6 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 4 +- .../commonMain/resources/MR/base/strings.xml | 5 +- 20 files changed, 252 insertions(+), 183 deletions(-) create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepo.kt delete mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepos.kt rename app/src/main/java/eu/kanade/presentation/{category/repos/RepoScreen.kt => more/settings/screen/browse/ExtensionReposScreen.kt} (65%) rename app/src/main/java/eu/kanade/presentation/{category/repos/RepoScreenModel.kt => more/settings/screen/browse/ExtensionReposScreenModel.kt} (81%) rename app/src/main/java/eu/kanade/presentation/{category/components/repo/SourceRepoContent.kt => more/settings/screen/browse/components/ExtensionReposContent.kt} (87%) create mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt rename app/src/main/java/eu/kanade/presentation/{category/SourceRepoScreen.kt => more/settings/screen/browse/components/ExtensionReposScreen.kt} (89%) rename app/src/main/java/eu/kanade/tachiyomi/extension/api/{ExtensionGithubApi.kt => ExtensionApi.kt} (69%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 96c7ac3856..b9daa2cf70 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 113 + versionCode = 114 versionName = "0.14.7" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 6ef3a44467..5bcaf48e6a 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -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()) } } } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt b/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt index 1140b4eb92..e22d8980f7 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/CreateSourceRepo.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepo.kt b/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepo.kt new file mode 100644 index 0000000000..1bf1098952 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepo.kt @@ -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 + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepos.kt b/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepos.kt deleted file mode 100644 index e8cd4721aa..0000000000 --- a/app/src/main/java/eu/kanade/domain/source/interactor/DeleteSourceRepos.kt +++ /dev/null @@ -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) { - preferences.extensionRepos().set( - preferences.extensionRepos().get().filterNot { it in repos }.toSet(), - ) - } -} diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceRepos.kt b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceRepos.kt index 25e3b3a21d..fdebe81479 100644 --- a/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceRepos.kt +++ b/app/src/main/java/eu/kanade/domain/source/interactor/GetSourceRepos.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.map class GetSourceRepos(private val preferences: SourcePreferences) { fun subscribe(): Flow> { - return preferences.extensionRepos().changes().map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) } + return preferences.extensionRepos().changes() + .map { it.sortedWith(String.CASE_INSENSITIVE_ORDER) } } } diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt index 676a5b1954..fd6396b95d 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt @@ -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, - 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, 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)) }, ) } diff --git a/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt b/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt index e2d5c4261d..5c387e542c 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt +++ b/app/src/main/java/eu/kanade/presentation/category/components/CategoryListItem.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt index 9fb4670758..57508ecbd7 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBrowseScreen.kt @@ -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()) }, ), ), diff --git a/app/src/main/java/eu/kanade/presentation/category/repos/RepoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt similarity index 65% rename from app/src/main/java/eu/kanade/presentation/category/repos/RepoScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt index 7a0d089e66..0cf4f027d9 100644 --- a/app/src/main/java/eu/kanade/presentation/category/repos/RepoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt @@ -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, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/category/repos/RepoScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt similarity index 81% rename from app/src/main/java/eu/kanade/presentation/category/repos/RepoScreenModel.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt index 039990b200..fc1d3893c1 100644 --- a/app/src/main/java/eu/kanade/presentation/category/repos/RepoScreenModel.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt @@ -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.Loading) { private val _events: Channel = 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) { + 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 { diff --git a/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt similarity index 87% rename from app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt index b3cda3fb18..8281f58745 100644 --- a/app/src/main/java/eu/kanade/presentation/category/components/repo/SourceRepoContent.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt @@ -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, 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) } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt new file mode 100644 index 0000000000..9f20b196de --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt @@ -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, +) { + 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)) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt similarity index 89% rename from app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt index 780ad0f0a5..1bd680d06f 100644 --- a/app/src/main/java/eu/kanade/presentation/category/SourceRepoScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt @@ -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 + diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 775bfe78dc..46f7e3812d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 4b43d11ad8..4c342dd3d6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -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. diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt similarity index 69% rename from app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt rename to app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt index 2bf6a708e3..e885652b31 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt @@ -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 { 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>() - .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>() - .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 { + return try { + val response = networkService.client + .newCall(GET("$repoBaseUrl/index.min.json")) + .awaitSuccess() + + with(json) { + response + .parseAs>() + .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.toExtensions( - repoUrl: String = getUrlPrefix(), - repoSource: Boolean = false, + repoUrl: String, + isRepoSource: Boolean, ): List { 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, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt index dcd0246bb2..20ebdba0ff 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt @@ -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 -> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 78c688c2ef..3ad313c4f9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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) } diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index a724bac5e2..e4aa74e4b9 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -340,10 +340,11 @@ Extension repos You have no repos set. Add repo - 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. + Repo URL + Add additional repos to Tachiyomi. This should be a URL that ends with \"index.min.json\". This repo already exists! Delete repo - Invalid repo name + Invalid repo URL Do you wish to delete the repo \"%s\"? This extension is from an external repo. Tap to view the repo.