From 4b4e46851083c29ca412c114b1b96136fcc21442 Mon Sep 17 00:00:00 2001 From: Maddie Witman Date: Fri, 22 Mar 2024 18:58:35 -0400 Subject: [PATCH] Grab extension repo detail from `repo.json` and include in DB (#506) * WIP Extension Repo DB Support * Wired in to extension screen, browse settings screen * Detekt changes * Ui tweaks and open in browser * Migrate ExtensionRepos on Update * Migration Cleanup * Slight cleanup / error handling * Update ExtensionRepo from Repo.json during extension search. Added Manual refresh in extension repos page. * Split repo fetching into separate API module, major refactor work * Removed development strings * Moved migration to #3 * Fixed rebase * Detekt changes * Added Replace Repository Dialog * Cleanup, removed platform specific code, PR comments * Removed extra function, reverted small change * Detekt cleanup * Apply suggestions from code review Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Fixed error introduced in cleanup * Tweak for multiline when * Moved getCount() to flow * changed getCount to non-suspend, used property delegation * Apply suggestions from code review Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> * Fixed formatting with updated comment string * Big wave of PR comments, renaming/other tweaks * onOpenWebsite changes * onOpenWebsite changes * trying to make single line * Renamed ExtensionRepoApi.kt to ExtensionRepoService.kt --------- Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com> --- .../java/eu/kanade/domain/DomainModule.kt | 17 +++- .../interactor/CreateExtensionRepo.kt | 25 ----- .../interactor/DeleteExtensionRepo.kt | 11 --- .../extension/interactor/GetExtensionRepos.kt | 11 --- .../settings/screen/SettingsBrowseScreen.kt | 9 +- .../screen/browse/ExtensionReposScreen.kt | 18 +++- .../browse/ExtensionReposScreenModel.kt | 62 ++++++++++--- .../components/ExtensionReposContent.kt | 24 ++++- .../components/ExtensionReposDialogs.kt | 38 +++++++- .../browse/components/ExtensionReposScreen.kt | 16 ++++ .../java/eu/kanade/tachiyomi/Migrations.kt | 33 ++++++- .../tachiyomi/extension/api/ExtensionApi.kt | 20 +++- .../kanade/tachiyomi/ui/main/MainActivity.kt | 2 + .../repository/ExtensionRepoRepositoryImpl.kt | 93 +++++++++++++++++++ .../tachiyomi/data/extension_repos.sq | 57 ++++++++++++ .../sqldelight/tachiyomi/migrations/3.sqm | 8 ++ domain/build.gradle.kts | 1 + .../exception/SaveExtensionRepoException.kt | 10 ++ .../interactor/CreateExtensionRepo.kt | 81 ++++++++++++++++ .../interactor/DeleteExtensionRepo.kt | 11 +++ .../interactor/GetExtensionRepo.kt | 13 +++ .../interactor/GetExtensionRepoCount.kt | 9 ++ .../interactor/ReplaceExtensionRepo.kt | 12 +++ .../interactor/UpdateExtensionRepo.kt | 33 +++++++ .../extensionrepo/model/ExtensionRepo.kt | 9 ++ .../repository/ExtensionRepoRepository.kt | 47 ++++++++++ .../service/ExtensionRepoService.kt | 57 ++++++++++++ .../commonMain/resources/MR/base/strings.xml | 3 + 28 files changed, 649 insertions(+), 81 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/domain/extension/interactor/CreateExtensionRepo.kt delete mode 100644 app/src/main/java/eu/kanade/domain/extension/interactor/DeleteExtensionRepo.kt delete mode 100644 app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionRepos.kt create mode 100644 data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt create mode 100644 data/src/main/sqldelight/tachiyomi/data/extension_repos.sq create mode 100644 data/src/main/sqldelight/tachiyomi/migrations/3.sqm create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt create mode 100644 domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 4f0c86eab..1d2a214ac 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -4,10 +4,7 @@ import eu.kanade.domain.chapter.interactor.GetAvailableScanlators import eu.kanade.domain.chapter.interactor.SetReadStatus import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.download.interactor.DeleteDownload -import eu.kanade.domain.extension.interactor.CreateExtensionRepo -import eu.kanade.domain.extension.interactor.DeleteExtensionRepo import eu.kanade.domain.extension.interactor.GetExtensionLanguages -import eu.kanade.domain.extension.interactor.GetExtensionRepos import eu.kanade.domain.extension.interactor.GetExtensionSources import eu.kanade.domain.extension.interactor.GetExtensionsByType import eu.kanade.domain.extension.interactor.TrustExtension @@ -26,6 +23,14 @@ import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.interactor.TrackChapter +import mihon.data.repository.ExtensionRepoRepositoryImpl +import mihon.domain.extensionrepo.interactor.CreateExtensionRepo +import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo +import mihon.domain.extensionrepo.interactor.GetExtensionRepo +import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount +import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo +import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository import tachiyomi.data.category.CategoryRepositoryImpl import tachiyomi.data.chapter.ChapterRepositoryImpl import tachiyomi.data.history.HistoryRepositoryImpl @@ -173,8 +178,12 @@ class DomainModule : InjektModule { addFactory { ToggleSourcePin(get()) } addFactory { TrustExtension(get()) } + addSingletonFactory { ExtensionRepoRepositoryImpl(get()) } + addFactory { GetExtensionRepo(get()) } + addFactory { GetExtensionRepoCount(get()) } addFactory { CreateExtensionRepo(get()) } addFactory { DeleteExtensionRepo(get()) } - addFactory { GetExtensionRepos(get()) } + addFactory { ReplaceExtensionRepo(get()) } + addFactory { UpdateExtensionRepo(get(), get()) } } } diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/CreateExtensionRepo.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/CreateExtensionRepo.kt deleted file mode 100644 index a8083ec00..000000000 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/CreateExtensionRepo.kt +++ /dev/null @@ -1,25 +0,0 @@ -package eu.kanade.domain.extension.interactor - -import eu.kanade.domain.source.service.SourcePreferences -import tachiyomi.core.common.preference.plusAssign - -class CreateExtensionRepo(private val preferences: SourcePreferences) { - - fun await(name: String): Result { - // Do not allow invalid formats - if (!name.matches(repoRegex)) { - return Result.InvalidUrl - } - - preferences.extensionRepos() += name.removeSuffix("/index.min.json") - - return Result.Success - } - - sealed interface Result { - data object InvalidUrl : Result - data object Success : Result - } -} - -private val repoRegex = """^https://.*/index\.min\.json$""".toRegex() diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/DeleteExtensionRepo.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/DeleteExtensionRepo.kt deleted file mode 100644 index 8e50ebeca..000000000 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/DeleteExtensionRepo.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.kanade.domain.extension.interactor - -import eu.kanade.domain.source.service.SourcePreferences -import tachiyomi.core.common.preference.minusAssign - -class DeleteExtensionRepo(private val preferences: SourcePreferences) { - - fun await(repo: String) { - preferences.extensionRepos() -= repo - } -} diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionRepos.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionRepos.kt deleted file mode 100644 index 0d3b0e988..000000000 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionRepos.kt +++ /dev/null @@ -1,11 +0,0 @@ -package eu.kanade.domain.extension.interactor - -import eu.kanade.domain.source.service.SourcePreferences -import kotlinx.coroutines.flow.Flow - -class GetExtensionRepos(private val preferences: SourcePreferences) { - - fun subscribe(): Flow> { - return preferences.extensionRepos().changes() - } -} 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 c8bfd10ca..b47dbe502 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 @@ -2,6 +2,7 @@ package eu.kanade.presentation.more.settings.screen import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext @@ -13,11 +14,11 @@ 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 mihon.domain.extensionrepo.interactor.GetExtensionRepoCount import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.util.collectAsState import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -33,7 +34,9 @@ object SettingsBrowseScreen : SearchableSettings { val navigator = LocalNavigator.currentOrThrow val sourcePreferences = remember { Injekt.get() } - val reposCount by sourcePreferences.extensionRepos().collectAsState() + val getExtensionRepoCount = remember { Injekt.get() } + + val reposCount by getExtensionRepoCount.subscribe().collectAsState(0) return listOf( Preference.PreferenceGroup( @@ -45,7 +48,7 @@ object SettingsBrowseScreen : SearchableSettings { ), Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.label_extension_repos), - subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.size, reposCount.size), + subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount), onClick = { navigator.push(ExtensionReposScreen()) }, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt index 4801829d5..03c9acd7c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt @@ -8,11 +8,14 @@ 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.more.settings.screen.browse.components.ExtensionRepoConflictDialog 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.openInBrowser import eu.kanade.tachiyomi.util.system.toast +import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.flow.collectLatest import tachiyomi.presentation.core.screens.LoadingScreen @@ -42,17 +45,19 @@ class ExtensionReposScreen( ExtensionReposScreen( state = successState, onClickCreate = { screenModel.showDialog(RepoDialog.Create) }, + onOpenWebsite = { context.openInBrowser(it.website) }, onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) }, + onClickRefresh = { screenModel.refreshRepos() }, navigateUp = navigator::pop, ) when (val dialog = successState.dialog) { null -> {} - RepoDialog.Create -> { + is RepoDialog.Create -> { ExtensionRepoCreateDialog( onDismissRequest = screenModel::dismissDialog, onCreate = { screenModel.createRepo(it) }, - repos = successState.repos, + repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(), ) } is RepoDialog.Delete -> { @@ -62,6 +67,15 @@ class ExtensionReposScreen( repo = dialog.repo, ) } + + is RepoDialog.Conflict -> { + ExtensionRepoConflictDialog( + onDismissRequest = screenModel::dismissDialog, + onMigrate = { screenModel.replaceRepo(dialog.newRepo) }, + oldRepo = dialog.oldRepo, + newRepo = dialog.newRepo, + ) + } } LaunchedEffect(Unit) { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt index d694618fb..4131bc6fd 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt @@ -4,24 +4,29 @@ 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.extension.interactor.CreateExtensionRepo -import eu.kanade.domain.extension.interactor.DeleteExtensionRepo -import eu.kanade.domain.extension.interactor.GetExtensionRepos import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update +import mihon.domain.extensionrepo.interactor.CreateExtensionRepo +import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo +import mihon.domain.extensionrepo.interactor.GetExtensionRepo +import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo +import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo +import mihon.domain.extensionrepo.model.ExtensionRepo import tachiyomi.core.common.util.lang.launchIO import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class ExtensionReposScreenModel( - private val getExtensionRepos: GetExtensionRepos = Injekt.get(), + private val getExtensionRepo: GetExtensionRepo = Injekt.get(), private val createExtensionRepo: CreateExtensionRepo = Injekt.get(), private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(), + private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(), + private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(), ) : StateScreenModel(RepoScreenState.Loading) { private val _events: Channel = Channel(Int.MAX_VALUE) @@ -29,7 +34,7 @@ class ExtensionReposScreenModel( init { screenModelScope.launchIO { - getExtensionRepos.subscribe() + getExtensionRepo.subscribeAll() .collectLatest { repos -> mutableState.update { RepoScreenState.Success( @@ -43,25 +48,51 @@ class ExtensionReposScreenModel( /** * Creates and adds a new repo to the database. * - * @param name The name of the repo to create. + * @param baseUrl The baseUrl of the repo to create. */ - fun createRepo(name: String) { + fun createRepo(baseUrl: String) { screenModelScope.launchIO { - when (createExtensionRepo.await(name)) { - is CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl) + when (val result = createExtensionRepo.await(baseUrl)) { + CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl) + CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists) + is CreateExtensionRepo.Result.DuplicateFingerprint -> { + showDialog(RepoDialog.Conflict(result.oldRepo, result.newRepo)) + } else -> {} } } } /** - * Deletes the given repo from the database. + * Inserts a repo to the database, replace a matching repo with the same signing key fingerprint if found. * - * @param repo The repo to delete. + * @param newRepo The repo to insert */ - fun deleteRepo(repo: String) { + fun replaceRepo(newRepo: ExtensionRepo) { screenModelScope.launchIO { - deleteExtensionRepo.await(repo) + replaceExtensionRepo.await(newRepo) + } + } + + /** + * Refreshes information for each repository. + */ + fun refreshRepos() { + val status = state.value + + if (status is RepoScreenState.Success) { + screenModelScope.launchIO { + updateExtensionRepo.awaitAll() + } + } + } + + /** + * Deletes the given repo from the database + */ + fun deleteRepo(baseUrl: String) { + screenModelScope.launchIO { + deleteExtensionRepo.await(baseUrl) } } @@ -87,11 +118,13 @@ class ExtensionReposScreenModel( sealed class RepoEvent { sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent() data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name) + data object RepoAlreadyExists : LocalizedMessage(MR.strings.error_repo_exists) } sealed class RepoDialog { data object Create : RepoDialog() data class Delete(val repo: String) : RepoDialog() + data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog() } sealed class RepoScreenState { @@ -101,7 +134,8 @@ sealed class RepoScreenState { @Immutable data class Success( - val repos: ImmutableSet, + val repos: ImmutableSet, + val oldRepos: ImmutableSet? = null, val dialog: RepoDialog? = null, ) : RepoScreenState() { diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt index 7d837b32d..83be8846d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Label +import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.Delete import androidx.compose.material3.ElevatedCard @@ -22,15 +23,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import eu.kanade.tachiyomi.util.system.copyToClipboard import kotlinx.collections.immutable.ImmutableSet +import mihon.domain.extensionrepo.model.ExtensionRepo import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @Composable fun ExtensionReposContent( - repos: ImmutableSet, + repos: ImmutableSet, lazyListState: LazyListState, paddingValues: PaddingValues, + onOpenWebsite: (ExtensionRepo) -> Unit, onClickDelete: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -44,8 +47,9 @@ fun ExtensionReposContent( item { ExtensionRepoListItem( modifier = Modifier.animateItemPlacement(), - repo = it, - onDelete = { onClickDelete(it) }, + repo = it.name, + onOpenWebsite = { onOpenWebsite(it) }, + onDelete = { onClickDelete(it.baseUrl) }, ) } } @@ -55,6 +59,7 @@ fun ExtensionReposContent( @Composable private fun ExtensionRepoListItem( repo: String, + onOpenWebsite: () -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier, ) { @@ -74,13 +79,24 @@ private fun ExtensionRepoListItem( verticalAlignment = Alignment.CenterVertically, ) { Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null) - Text(text = repo, modifier = Modifier.padding(start = MaterialTheme.padding.medium)) + Text( + text = repo, + modifier = Modifier.padding(start = MaterialTheme.padding.medium), + style = MaterialTheme.typography.titleMedium, + ) } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, ) { + IconButton(onClick = onOpenWebsite) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.OpenInNew, + contentDescription = stringResource(MR.strings.action_open_in_browser), + ) + } + IconButton( onClick = { val url = "$repo/index.min.json" 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 index b4ef8b575..5022f44f0 100644 --- 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 @@ -16,6 +16,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import kotlinx.collections.immutable.ImmutableSet import kotlinx.coroutines.delay +import mihon.domain.extensionrepo.model.ExtensionRepo import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource import kotlin.time.Duration.Companion.seconds @@ -24,12 +25,12 @@ import kotlin.time.Duration.Companion.seconds fun ExtensionRepoCreateDialog( onDismissRequest: () -> Unit, onCreate: (String) -> Unit, - repos: ImmutableSet, + repoUrls: ImmutableSet, ) { var name by remember { mutableStateOf("") } val focusRequester = remember { FocusRequester() } - val nameAlreadyExists = remember(name) { repos.contains(name) } + val nameAlreadyExists = remember(name) { repoUrls.contains(name) } AlertDialog( onDismissRequest = onDismissRequest, @@ -115,3 +116,36 @@ fun ExtensionRepoDeleteDialog( }, ) } + +@Composable +fun ExtensionRepoConflictDialog( + oldRepo: ExtensionRepo, + newRepo: ExtensionRepo, + onDismissRequest: () -> Unit, + onMigrate: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = { + onMigrate() + onDismissRequest() + }, + ) { + Text(text = stringResource(MR.strings.action_replace_repo)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + title = { + Text(text = stringResource(MR.strings.action_replace_repo_title)) + }, + text = { + Text(text = stringResource(MR.strings.action_replace_repo_message, newRepo.name, oldRepo.name)) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt index 1bd680d06..b07ba4101 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt @@ -5,12 +5,17 @@ package eu.kanade.presentation.more.settings.screen.browse.components import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Refresh +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton 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.components.AppBar import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState +import mihon.domain.extensionrepo.model.ExtensionRepo import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding @@ -23,7 +28,9 @@ import tachiyomi.presentation.core.util.plus fun ExtensionReposScreen( state: RepoScreenState.Success, onClickCreate: () -> Unit, + onOpenWebsite: (ExtensionRepo) -> Unit, onClickDelete: (String) -> Unit, + onClickRefresh: () -> Unit, navigateUp: () -> Unit, ) { val lazyListState = rememberLazyListState() @@ -33,6 +40,14 @@ fun ExtensionReposScreen( navigateUp = navigateUp, title = stringResource(MR.strings.label_extension_repos), scrollBehavior = scrollBehavior, + actions = { + IconButton(onClick = onClickRefresh) { + Icon( + imageVector = Icons.Outlined.Refresh, + contentDescription = stringResource(resource = MR.strings.action_webview_refresh), + ) + } + }, ) }, floatingActionButton = { @@ -55,6 +70,7 @@ fun ExtensionReposScreen( lazyListState = lazyListState, paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium), + onOpenWebsite = onOpenWebsite, onClickDelete = onClickDelete, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 154326514..7b617eb9b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -1,10 +1,18 @@ package eu.kanade.tachiyomi import android.content.Context +import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import logcat.LogPriority +import mihon.domain.extensionrepo.exception.SaveExtensionRepoException +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore +import tachiyomi.core.common.util.lang.launchIO +import tachiyomi.core.common.util.system.logcat object Migrations { @@ -13,10 +21,12 @@ object Migrations { * * @return true if a migration is performed, false otherwise. */ - @Suppress("SameReturnValue") + @Suppress("SameReturnValue", "MagicNumber") fun upgrade( context: Context, preferenceStore: PreferenceStore, + sourcePreferences: SourcePreferences, + extensionRepoRepository: ExtensionRepoRepository, ): Boolean { val lastVersionCode = preferenceStore.getInt(Preference.appStateKey("last_version_code"), 0) val oldVersion = lastVersionCode.get() @@ -31,6 +41,27 @@ object Migrations { if (oldVersion == 0) { return false } + + val coroutineScope = CoroutineScope(Dispatchers.IO) + + if (oldVersion < 6) { + coroutineScope.launchIO { + for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) { + try { + extensionRepoRepository.upsertRepository( + source, + "Repo #${index + 1}", + null, + source, + "NOFINGERPRINT-${index + 1}", + ) + } catch (e: SaveExtensionRepoException) { + logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" } + } + } + sourcePreferences.extensionRepos().delete() + } + } } return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt index 0d79fd335..71e6251db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.extension.api import android.content.Context -import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult @@ -10,9 +9,14 @@ import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.awaitSuccess import eu.kanade.tachiyomi.network.parseAs +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import logcat.LogPriority +import mihon.domain.extensionrepo.interactor.GetExtensionRepo +import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo +import mihon.domain.extensionrepo.model.ExtensionRepo import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.util.lang.withIOContext @@ -25,7 +29,8 @@ internal class ExtensionApi { private val networkService: NetworkHelper by injectLazy() private val preferenceStore: PreferenceStore by injectLazy() - private val sourcePreferences: SourcePreferences by injectLazy() + private val getExtensionRepo: GetExtensionRepo by injectLazy() + private val updateExtensionRepo: UpdateExtensionRepo by injectLazy() private val extensionManager: ExtensionManager by injectLazy() private val json: Json by injectLazy() @@ -35,11 +40,15 @@ internal class ExtensionApi { suspend fun findExtensions(): List { return withIOContext { - sourcePreferences.extensionRepos().get().flatMap { getExtensions(it) } + getExtensionRepo.getAll() + .map { async { getExtensions(it) } } + .awaitAll() + .flatten() } } - private suspend fun getExtensions(repoBaseUrl: String): List { + private suspend fun getExtensions(extRepo: ExtensionRepo): List { + val repoBaseUrl = extRepo.baseUrl return try { val response = networkService.client .newCall(GET("$repoBaseUrl/index.min.json")) @@ -67,6 +76,9 @@ internal class ExtensionApi { return null } + // Update extension repo details + updateExtensionRepo.awaitAll() + val extensions = if (fromAvailableExtensionList) { extensionManager.availableExtensionsFlow.value } else { 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 87ab0b4c8..ae05ade4b 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 @@ -134,6 +134,8 @@ class MainActivity : BaseActivity() { Migrations.upgrade( context = applicationContext, preferenceStore = Injekt.get(), + sourcePreferences = Injekt.get(), + extensionRepoRepository = Injekt.get(), ) } else { false diff --git a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt b/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt new file mode 100644 index 000000000..65fcd149c --- /dev/null +++ b/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt @@ -0,0 +1,93 @@ +package mihon.data.repository + +import android.database.sqlite.SQLiteException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import mihon.domain.extensionrepo.exception.SaveExtensionRepoException +import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import tachiyomi.data.DatabaseHandler + +class ExtensionRepoRepositoryImpl( + private val handler: DatabaseHandler, +) : ExtensionRepoRepository { + override fun subscribeAll(): Flow> { + return handler.subscribeToList { extension_reposQueries.findAll(::mapExtensionRepo) } + } + + override suspend fun getAll(): List { + return handler.awaitList { extension_reposQueries.findAll(::mapExtensionRepo) } + } + + override suspend fun getRepository(baseUrl: String): ExtensionRepo? { + return handler.awaitOneOrNull { extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) } + } + + override suspend fun getRepositoryBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? { + return handler.awaitOneOrNull { + extension_reposQueries.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo) + } + } + + override fun getCount(): Flow { + return handler.subscribeToOne { extension_reposQueries.count() }.map { it.toInt() } + } + + override suspend fun insertRepository( + baseUrl: String, + name: String, + shortName: String?, + website: String, + signingKeyFingerprint: String, + ) { + try { + handler.await { extension_reposQueries.insert(baseUrl, name, shortName, website, signingKeyFingerprint) } + } catch (ex: SQLiteException) { + throw SaveExtensionRepoException(ex) + } + } + + override suspend fun upsertRepository( + baseUrl: String, + name: String, + shortName: String?, + website: String, + signingKeyFingerprint: String, + ) { + try { + handler.await { extension_reposQueries.upsert(baseUrl, name, shortName, website, signingKeyFingerprint) } + } catch (ex: SQLiteException) { + throw SaveExtensionRepoException(ex) + } + } + + override suspend fun replaceRepository(newRepo: ExtensionRepo) { + handler.await { + extension_reposQueries.replace( + newRepo.baseUrl, + newRepo.name, + newRepo.shortName, + newRepo.website, + newRepo.signingKeyFingerprint, + ) + } + } + + override suspend fun deleteRepository(baseUrl: String) { + return handler.await { extension_reposQueries.delete(baseUrl) } + } + + private fun mapExtensionRepo( + baseUrl: String, + name: String, + shortName: String?, + website: String, + signingKeyFingerprint: String, + ): ExtensionRepo = ExtensionRepo( + baseUrl = baseUrl, + name = name, + shortName = shortName, + website = website, + signingKeyFingerprint = signingKeyFingerprint, + ) +} diff --git a/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq b/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq new file mode 100644 index 000000000..6db69132a --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq @@ -0,0 +1,57 @@ +CREATE TABLE extension_repos ( + base_url TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + short_name TEXT, + website TEXT NOT NULL, + signing_key_fingerprint TEXT UNIQUE NOT NULL +); + +findOne: +SELECT * +FROM extension_repos +WHERE base_url = :base_url; + +findOneBySigningKeyFingerprint: +SELECT * +FROM extension_repos +WHERE signing_key_fingerprint = :fingerprint; + +findAll: +SELECT * +FROM extension_repos; + +count: +SELECT COUNT(*) +FROM extension_repos; + +insert: +INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint) +VALUES (:base_url, :name, :short_name, :website, :fingerprint); + +upsert: +INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint) +VALUES (:base_url, :name, :short_name, :website, :fingerprint) +ON CONFLICT(base_url) +DO UPDATE +SET + name = :name, + short_name = :short_name, + website =: website, + signing_key_fingerprint = :fingerprint +WHERE base_url = base_url; + +replace: +INSERT INTO extension_repos(base_url, name, short_name, website, signing_key_fingerprint) +VALUES (:base_url, :name, :short_name, :website, :fingerprint) +ON CONFLICT(signing_key_fingerprint) +DO UPDATE +SET + base_url = :base_url, + name = :name, + short_name = :short_name, + website =: website +WHERE signing_key_fingerprint = signing_key_fingerprint; + +delete: +DELETE FROM extension_repos +WHERE base_url = :base_url; diff --git a/data/src/main/sqldelight/tachiyomi/migrations/3.sqm b/data/src/main/sqldelight/tachiyomi/migrations/3.sqm new file mode 100644 index 000000000..ecf3b16a8 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/3.sqm @@ -0,0 +1,8 @@ +-- Create ExtensionRepo table -- +CREATE TABLE extension_repos ( + base_url TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + short_name TEXT, + website TEXT NOT NULL, + signing_key_fingerprint TEXT UNIQUE NOT NULL +); diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index dc8c33315..63f2b9df9 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -33,6 +33,7 @@ tasks { withType { kotlinOptions.freeCompilerArgs += listOf( "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-Xcontext-receivers", ) } } diff --git a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt b/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt new file mode 100644 index 000000000..4c6990be0 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt @@ -0,0 +1,10 @@ +package mihon.domain.extensionrepo.exception + +import java.io.IOException + +/** + * Exception to abstract over SQLiteException and SQLiteConstraintException for multiplatform. + * + * @param throwable the source throwable to include for tracing. + */ +class SaveExtensionRepoException(throwable: Throwable) : IOException("Error Saving Repository to Database", throwable) diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt new file mode 100644 index 000000000..6285d4915 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt @@ -0,0 +1,81 @@ +package mihon.domain.extensionrepo.interactor + +import eu.kanade.tachiyomi.network.NetworkHelper +import logcat.LogPriority +import mihon.domain.extensionrepo.exception.SaveExtensionRepoException +import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import mihon.domain.extensionrepo.service.ExtensionRepoService +import okhttp3.OkHttpClient +import tachiyomi.core.common.util.system.logcat +import uy.kohesive.injekt.injectLazy + +class CreateExtensionRepo( + private val extensionRepoRepository: ExtensionRepoRepository, +) { + private val repoRegex = """^https://.*/index\.min\.json$""".toRegex() + + private val networkService: NetworkHelper by injectLazy() + + private val client: OkHttpClient + get() = networkService.client + + private val extensionRepoService = ExtensionRepoService(client) + + suspend fun await(repoUrl: String): Result { + if (!repoUrl.matches(repoRegex)) { + return Result.InvalidUrl + } + + val baseUrl = repoUrl.removeSuffix("/index.min.json") + return extensionRepoService.fetchRepoDetails(baseUrl)?.let { insert(it) } ?: Result.InvalidUrl + } + + private suspend fun insert(repo: ExtensionRepo): Result { + return try { + extensionRepoRepository.insertRepository( + repo.baseUrl, + repo.name, + repo.shortName, + repo.website, + repo.signingKeyFingerprint, + ) + Result.Success + } catch (e: SaveExtensionRepoException) { + logcat(LogPriority.WARN, e) { "SQL Conflict attempting to add new repository ${repo.baseUrl}" } + return handleInsertionError(repo) + } + } + + /** + * Error Handler for insert when there are trying to create new repositories + * + * SaveExtensionRepoException doesn't provide constraint info in exceptions. + * First check if the conflict was on primary key. if so return RepoAlreadyExists + * Then check if the conflict was on fingerprint. if so Return DuplicateFingerprint + * If neither are found, there was some other Error, and return Result.Error + * + * @param repo Extension Repo holder for passing to DB/Error Dialog + */ + @Suppress("ReturnCount") + private suspend fun handleInsertionError(repo: ExtensionRepo): Result { + val repoExists = extensionRepoRepository.getRepository(repo.baseUrl) + if (repoExists != null) { + return Result.RepoAlreadyExists + } + val matchingFingerprintRepo = + extensionRepoRepository.getRepositoryBySigningKeyFingerprint(repo.signingKeyFingerprint) + if (matchingFingerprintRepo != null) { + return Result.DuplicateFingerprint(matchingFingerprintRepo, repo) + } + return Result.Error + } + + sealed interface Result { + data class DuplicateFingerprint(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : Result + data object InvalidUrl : Result + data object RepoAlreadyExists : Result + data object Success : Result + data object Error : Result + } +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt new file mode 100644 index 000000000..3be5c1ad9 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt @@ -0,0 +1,11 @@ +package mihon.domain.extensionrepo.interactor + +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository + +class DeleteExtensionRepo( + private val extensionRepoRepository: ExtensionRepoRepository, +) { + suspend fun await(baseUrl: String) { + extensionRepoRepository.deleteRepository(baseUrl) + } +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt new file mode 100644 index 000000000..e85bd6c01 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt @@ -0,0 +1,13 @@ +package mihon.domain.extensionrepo.interactor + +import kotlinx.coroutines.flow.Flow +import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository + +class GetExtensionRepo( + private val extensionRepoRepository: ExtensionRepoRepository, +) { + fun subscribeAll(): Flow> = extensionRepoRepository.subscribeAll() + + suspend fun getAll(): List = extensionRepoRepository.getAll() +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt new file mode 100644 index 000000000..6ca59f10b --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt @@ -0,0 +1,9 @@ +package mihon.domain.extensionrepo.interactor + +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository + +class GetExtensionRepoCount( + private val extensionRepoRepository: ExtensionRepoRepository, +) { + fun subscribe() = extensionRepoRepository.getCount() +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt new file mode 100644 index 000000000..6543b8924 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt @@ -0,0 +1,12 @@ +package mihon.domain.extensionrepo.interactor + +import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository + +class ReplaceExtensionRepo( + private val extensionRepoRepository: ExtensionRepoRepository, +) { + suspend fun await(repo: ExtensionRepo) { + extensionRepoRepository.replaceRepository(repo) + } +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt new file mode 100644 index 000000000..90e49307e --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt @@ -0,0 +1,33 @@ +package mihon.domain.extensionrepo.interactor + +import eu.kanade.tachiyomi.network.NetworkHelper +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import mihon.domain.extensionrepo.service.ExtensionRepoService + +class UpdateExtensionRepo( + private val extensionRepoRepository: ExtensionRepoRepository, + networkService: NetworkHelper, +) { + + private val extensionRepoService = ExtensionRepoService(networkService.client) + + suspend fun awaitAll() = coroutineScope { + extensionRepoRepository.getAll() + .map { async { await(it) } } + .awaitAll() + } + + suspend fun await(repo: ExtensionRepo) { + val newRepo = extensionRepoService.fetchRepoDetails(repo.baseUrl) ?: return + if ( + repo.signingKeyFingerprint.startsWith("NOFINGERPRINT") || + repo.signingKeyFingerprint == newRepo.signingKeyFingerprint + ) { + extensionRepoRepository.upsertRepository(newRepo) + } + } +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt new file mode 100644 index 000000000..ec9ccca87 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt @@ -0,0 +1,9 @@ +package mihon.domain.extensionrepo.model + +data class ExtensionRepo( + val baseUrl: String, + val name: String, + val shortName: String?, + val website: String, + val signingKeyFingerprint: String, +) diff --git a/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt b/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt new file mode 100644 index 000000000..8551254be --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt @@ -0,0 +1,47 @@ +package mihon.domain.extensionrepo.repository + +import kotlinx.coroutines.flow.Flow +import mihon.domain.extensionrepo.model.ExtensionRepo + +interface ExtensionRepoRepository { + + fun subscribeAll(): Flow> + + suspend fun getAll(): List + + suspend fun getRepository(baseUrl: String): ExtensionRepo? + + suspend fun getRepositoryBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? + + fun getCount(): Flow + + suspend fun insertRepository( + baseUrl: String, + name: String, + shortName: String?, + website: String, + signingKeyFingerprint: String, + ) + + suspend fun upsertRepository( + baseUrl: String, + name: String, + shortName: String?, + website: String, + signingKeyFingerprint: String, + ) + + suspend fun upsertRepository(repo: ExtensionRepo) { + upsertRepository( + baseUrl = repo.baseUrl, + name = repo.name, + shortName = repo.shortName, + website = repo.website, + signingKeyFingerprint = repo.signingKeyFingerprint, + ) + } + + suspend fun replaceRepository(newRepo: ExtensionRepo) + + suspend fun deleteRepository(baseUrl: String) +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt new file mode 100644 index 000000000..ca061304e --- /dev/null +++ b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt @@ -0,0 +1,57 @@ +package mihon.domain.extensionrepo.service + +import androidx.core.net.toUri +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.HttpException +import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.parseAs +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import mihon.domain.extensionrepo.model.ExtensionRepo +import okhttp3.OkHttpClient +import tachiyomi.core.common.util.lang.withIOContext +import uy.kohesive.injekt.injectLazy + +class ExtensionRepoService( + private val client: OkHttpClient, +) { + + private val json: Json by injectLazy() + + suspend fun fetchRepoDetails( + repo: String, + ): ExtensionRepo? { + return withIOContext { + val url = "$repo/repo.json".toUri() + + try { + val response = with(json) { + client.newCall(GET(url.toString())) + .awaitSuccess() + .parseAs() + } + response["meta"] + ?.jsonObject + ?.let { jsonToExtensionRepo(baseUrl = repo, it) } + } catch (_: HttpException) { + null + } + } + } + + private fun jsonToExtensionRepo(baseUrl: String, obj: JsonObject): ExtensionRepo? { + return try { + ExtensionRepo( + baseUrl = baseUrl, + name = obj["name"]!!.jsonPrimitive.content, + shortName = obj["shortName"]?.jsonPrimitive?.content, + website = obj["website"]!!.jsonPrimitive.content, + signingKeyFingerprint = obj["signingKeyFingerprint"]!!.jsonPrimitive.content, + ) + } catch (_: NullPointerException) { + null + } + } +} diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index 7fc1832ca..23ce8a441 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -347,6 +347,9 @@ Invalid repo URL Do you wish to delete the repo \"%s\"? Open source repo + Replace + Signing Key Fingerprint Already Exists + Repository %1$s has the same Signing Key Fingerprint as %2$s.\nIf this is expected, %2$s will be replaced, otherwise contact your repo maintainer. Fullscreen