diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 6d3ddc35b91..b69d6018646 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -25,16 +25,15 @@ 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.data.extension.repository.ExtensionStoreRepositoryImpl +import mihon.data.extension.service.ExtensionStoreService import mihon.domain.chapter.interactor.FilterChaptersForDownload -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 mihon.domain.extensionrepo.service.ExtensionRepoService +import mihon.domain.extension.interactor.AddExtensionStore +import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow +import mihon.domain.extension.interactor.GetExtensionStores +import mihon.domain.extension.interactor.RemoveExtensionStore +import mihon.domain.extension.interactor.UpdateExtensionStores +import mihon.domain.extension.repository.ExtensionStoreRepository import mihon.domain.migration.usecases.MigrateMangaUseCase import mihon.domain.upcoming.interactor.GetUpcomingManga import tachiyomi.data.category.CategoryRepositoryImpl @@ -195,14 +194,14 @@ class DomainModule : InjektModule { addFactory { ToggleSourcePin(get()) } addFactory { TrustExtension(get(), get()) } - addSingletonFactory { ExtensionRepoRepositoryImpl(get()) } - addFactory { ExtensionRepoService(get(), get()) } - addFactory { GetExtensionRepo(get()) } - addFactory { GetExtensionRepoCount(get()) } - addFactory { CreateExtensionRepo(get(), get()) } - addFactory { DeleteExtensionRepo(get()) } - addFactory { ReplaceExtensionRepo(get()) } - addFactory { UpdateExtensionRepo(get(), get()) } + addSingletonFactory { ExtensionStoreService(get(), get(), get()) } + addSingletonFactory { ExtensionStoreRepositoryImpl(get(), get()) } + addFactory { AddExtensionStore(get()) } + addFactory { GetExtensionStoreCountAsFlow(get()) } + addFactory { GetExtensionStores(get()) } + addFactory { RemoveExtensionStore(get()) } + addFactory { UpdateExtensionStores(get()) } + addFactory { ToggleIncognito(get()) } addFactory { GetIncognitoState(get(), get(), get()) } } diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt index c155af2480b..4e5c7c926ab 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt @@ -17,11 +17,7 @@ class GetExtensionLanguages( ) { enabledLanguage, availableExtensions -> availableExtensions .flatMap { ext -> - if (ext.sources.isEmpty()) { - listOf(ext.lang) - } else { - ext.sources.map { it.lang } - } + ext.sources.map { it.lang } } .distinct() .sortedWith( diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt index c2bfd49f8e5..ce689a7f413 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt @@ -39,9 +39,6 @@ class GetExtensionsByType( (showNsfwSources || !extension.isNsfw) } .flatMap { ext -> - if (ext.sources.isEmpty()) { - return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList() - } ext.sources.filter { it.lang in enabledLanguages } .map { ext.copy( diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt index 40871be055c..3b4351be5e5 100644 --- a/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt +++ b/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt @@ -3,16 +3,16 @@ package eu.kanade.domain.extension.interactor import android.content.pm.PackageInfo import androidx.core.content.pm.PackageInfoCompat import eu.kanade.domain.source.service.SourcePreferences -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import mihon.domain.extension.repository.ExtensionStoreRepository import tachiyomi.core.common.preference.getAndSet class TrustExtension( - private val extensionRepoRepository: ExtensionRepoRepository, + private val repository: ExtensionStoreRepository, private val preferences: SourcePreferences, ) { suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List): Boolean { - val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet() + val trustedFingerprints = repository.getAll().map { it.signingKey }.toHashSet() val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}" return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions.get() } diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt index c2219c39553..2848bde480b 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -80,12 +80,12 @@ fun ExtensionDetailsScreen( val uriHandler = LocalUriHandler.current val url = remember(state.extension) { val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex() - regex.find(state.extension?.repoUrl.orEmpty()) + regex.find(state.extension?.store?.indexUrl.orEmpty()) ?.let { val (user, repo) = it.destructured "https://github.com/$user/$repo" } - ?: state.extension?.repoUrl + ?: state.extension?.store?.indexUrl } Scaffold( @@ -248,14 +248,17 @@ private fun DetailsHeader( if (extension is Extension.Installed) { append("\n\n") - append( + appendLine( """ Update available: ${extension.hasUpdate} Obsolete: ${extension.isObsolete} Shared: ${extension.isShared} - Repository: ${extension.repoUrl} """.trimIndent(), ) + val store = extension.store + if (store != null) { + append("Repository: ${store.indexUrl}") + } } } context.copyToClipboard("Extension Debug information", extDebugInfo) 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 25e2b384a8b..4326c08c928 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 @@ -14,7 +14,7 @@ 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 mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow import tachiyomi.core.common.i18n.stringResource import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.pluralStringResource @@ -34,9 +34,9 @@ object SettingsBrowseScreen : SearchableSettings { val navigator = LocalNavigator.currentOrThrow val sourcePreferences = remember { Injekt.get() } - val getExtensionRepoCount = remember { Injekt.get() } + val getExtensionStoreCountAsFlow = remember { Injekt.get() } - val reposCount by getExtensionRepoCount.subscribe().collectAsState(0) + val reposCount by getExtensionStoreCountAsFlow().collectAsState(0) return listOf( Preference.PreferenceGroup( @@ -48,7 +48,7 @@ object SettingsBrowseScreen : SearchableSettings { ), Preference.PreferenceItem.TextPreference( title = stringResource(MR.strings.label_extension_repos), - subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount), + subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.toInt(), 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 38f4e72c838..9a725432b22 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 @@ -9,11 +9,11 @@ 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.ExtensionRepoConfirmDialog -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.copyToClipboard import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.toast import kotlinx.collections.immutable.toImmutableSet @@ -46,8 +46,10 @@ class ExtensionReposScreen( ExtensionReposScreen( state = successState, onClickCreate = { screenModel.showDialog(RepoDialog.Create) }, - onOpenWebsite = { context.openInBrowser(it.website) }, - onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) }, + onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) }, + onOpenWebsite = { it.contact.website?.let(context::openInBrowser) }, + onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) }, + onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it.indexUrl)) }, onClickRefresh = { screenModel.refreshRepos() }, navigateUp = navigator::pop, ) @@ -58,7 +60,7 @@ class ExtensionReposScreen( ExtensionRepoCreateDialog( onDismissRequest = screenModel::dismissDialog, onCreate = { screenModel.createRepo(it) }, - repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(), + repoUrls = successState.stores.map { it.indexUrl }.toImmutableSet(), ) } is RepoDialog.Delete -> { @@ -68,14 +70,6 @@ class ExtensionReposScreen( repo = dialog.repo, ) } - is RepoDialog.Conflict -> { - ExtensionRepoConflictDialog( - onDismissRequest = screenModel::dismissDialog, - onMigrate = { screenModel.replaceRepo(dialog.newRepo) }, - oldRepo = dialog.oldRepo, - newRepo = dialog.newRepo, - ) - } is RepoDialog.Confirm -> { ExtensionRepoConfirmDialog( onDismissRequest = screenModel::dismissDialog, 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 3c5212473c1..98f22febae3 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 @@ -5,29 +5,26 @@ import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.screenModelScope import dev.icerock.moko.resources.StringResource import eu.kanade.tachiyomi.extension.ExtensionManager -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 kotlinx.coroutines.launch +import mihon.domain.extension.interactor.AddExtensionStore +import mihon.domain.extension.interactor.GetExtensionStores +import mihon.domain.extension.interactor.RemoveExtensionStore +import mihon.domain.extension.interactor.UpdateExtensionStores +import mihon.domain.extension.model.ExtensionStore 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 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(), + private val getExtensionStores: GetExtensionStores = Injekt.get(), + private val addExtensionStore: AddExtensionStore = Injekt.get(), + private val removeExtensionStore: RemoveExtensionStore = Injekt.get(), + private val updateExtensionStores: UpdateExtensionStores = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(), ) : StateScreenModel(RepoScreenState.Loading) { @@ -36,11 +33,11 @@ class ExtensionReposScreenModel( init { screenModelScope.launchIO { - getExtensionRepo.subscribeAll() - .collectLatest { repos -> + getExtensionStores.subscribe() + .collectLatest { stores -> mutableState.update { RepoScreenState.Success( - repos = repos.toImmutableSet(), + stores = stores, ) } } @@ -53,27 +50,11 @@ class ExtensionReposScreenModel( * @param baseUrl The baseUrl of the repo to create. */ fun createRepo(baseUrl: String) { - screenModelScope.launchIO { - when (val result = createExtensionRepo.await(baseUrl)) { - CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions() - 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 -> {} - } - } - } - - /** - * Inserts a repo to the database, replace a matching repo with the same signing key fingerprint if found. - * - * @param newRepo The repo to insert - */ - fun replaceRepo(newRepo: ExtensionRepo) { - screenModelScope.launchIO { - replaceExtensionRepo.await(newRepo) + screenModelScope.launch { + addExtensionStore(baseUrl).fold( + onSuccess = { extensionManager.findAvailableExtensions() }, + onFailure = {}, + ) } } @@ -85,7 +66,7 @@ class ExtensionReposScreenModel( if (status is RepoScreenState.Success) { screenModelScope.launchIO { - updateExtensionRepo.awaitAll() + updateExtensionStores() } } } @@ -95,7 +76,7 @@ class ExtensionReposScreenModel( */ fun deleteRepo(baseUrl: String) { screenModelScope.launchIO { - deleteExtensionRepo.await(baseUrl) + removeExtensionStore(baseUrl) extensionManager.findAvailableExtensions() } } @@ -121,14 +102,12 @@ 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) + data object FailedToAddStore : LocalizedMessage(MR.strings.invalid_repo_name) } sealed class RepoDialog { data object Create : RepoDialog() data class Delete(val repo: String) : RepoDialog() - data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog() data class Confirm(val url: String) : RepoDialog() } @@ -139,12 +118,11 @@ sealed class RepoScreenState { @Immutable data class Success( - val repos: ImmutableSet, - val oldRepos: ImmutableSet? = null, + val stores: List, val dialog: RepoDialog? = null, ) : RepoScreenState() { val isEmpty: Boolean - get() = repos.isEmpty() + get() = stores.isEmpty() } } 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 e6b2a227f61..f5bc1226622 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,9 +9,9 @@ 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.material.icons.outlined.Public import androidx.compose.material3.ElevatedCard import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -21,20 +21,22 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment 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 mihon.domain.extension.model.ExtensionStore import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource +import tachiyomi.presentation.core.icons.CustomIcons +import tachiyomi.presentation.core.icons.Discord @Composable fun ExtensionReposContent( - repos: ImmutableSet, + repos: List, lazyListState: LazyListState, paddingValues: PaddingValues, - onOpenWebsite: (ExtensionRepo) -> Unit, - onClickDelete: (String) -> Unit, + onCopy: (ExtensionStore) -> Unit, + onOpenWebsite: (ExtensionStore) -> Unit, + onOpenDiscord: (ExtensionStore) -> Unit, + onClickDelete: (ExtensionStore) -> Unit, modifier: Modifier = Modifier, ) { LazyColumn( @@ -47,9 +49,11 @@ fun ExtensionReposContent( item { ExtensionRepoListItem( modifier = Modifier.animateItem(), - repo = it, + store = it, onOpenWebsite = { onOpenWebsite(it) }, - onDelete = { onClickDelete(it.baseUrl) }, + onOpenDiscord = { onOpenDiscord(it) }, + onCopy = { onCopy(it) }, + onDelete = { onClickDelete(it) }, ) } } @@ -58,8 +62,10 @@ fun ExtensionReposContent( @Composable private fun ExtensionRepoListItem( - repo: ExtensionRepo, + store: ExtensionStore, onOpenWebsite: () -> Unit, + onOpenDiscord: () -> Unit, + onCopy: () -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier, ) { @@ -80,7 +86,7 @@ private fun ExtensionRepoListItem( ) { Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null) Text( - text = repo.name, + text = store.name, modifier = Modifier.padding(start = MaterialTheme.padding.medium), style = MaterialTheme.typography.titleMedium, ) @@ -90,19 +96,25 @@ private fun ExtensionRepoListItem( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, ) { - IconButton(onClick = onOpenWebsite) { - Icon( - imageVector = Icons.AutoMirrored.Outlined.OpenInNew, - contentDescription = stringResource(MR.strings.action_open_in_browser), - ) + if (store.contact.website != null) { + IconButton(onClick = onOpenWebsite) { + Icon( + imageVector = Icons.Outlined.Public, + contentDescription = stringResource(MR.strings.action_open_in_browser), + ) + } + } + + if (store.contact.discord != null) { + IconButton(onClick = onOpenDiscord) { + Icon( + imageVector = CustomIcons.Discord, + contentDescription = null, + ) + } } - IconButton( - onClick = { - val url = "${repo.baseUrl}/index.min.json" - context.copyToClipboard(url, url) - }, - ) { + IconButton(onClick = onCopy) { Icon( imageVector = Icons.Outlined.ContentCopy, contentDescription = stringResource(MR.strings.action_copy_to_clipboard), 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 bd7c1ceddde..d9efbd93349 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 @@ -19,7 +19,6 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.input.KeyboardType 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 @@ -122,39 +121,6 @@ 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)) - }, - ) -} - @Composable fun ExtensionRepoConfirmDialog( onDismissRequest: () -> Unit, 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 b07ba4101e9..b4bbdcd8aa3 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 @@ -15,7 +15,7 @@ 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 mihon.domain.extension.model.ExtensionStore import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding @@ -28,8 +28,10 @@ import tachiyomi.presentation.core.util.plus fun ExtensionReposScreen( state: RepoScreenState.Success, onClickCreate: () -> Unit, - onOpenWebsite: (ExtensionRepo) -> Unit, - onClickDelete: (String) -> Unit, + onCopy: (ExtensionStore) -> Unit, + onOpenWebsite: (ExtensionStore) -> Unit, + onOpenDiscord: (ExtensionStore) -> Unit, + onClickDelete: (ExtensionStore) -> Unit, onClickRefresh: () -> Unit, navigateUp: () -> Unit, ) { @@ -66,11 +68,13 @@ fun ExtensionReposScreen( } ExtensionReposContent( - repos = state.repos, + repos = state.stores, lazyListState = lazyListState, paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium), + onCopy = onCopy, onOpenWebsite = onOpenWebsite, + onOpenDiscord = onOpenDiscord, onClickDelete = onClickDelete, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt index 2db6a6f06ff..4f3a3ab6cc2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt @@ -2,16 +2,16 @@ package eu.kanade.tachiyomi.data.backup.create.creators import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos import eu.kanade.tachiyomi.data.backup.models.backupExtensionReposMapper -import mihon.domain.extensionrepo.interactor.GetExtensionRepo +import mihon.domain.extension.interactor.GetExtensionStores import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class ExtensionRepoBackupCreator( - private val getExtensionRepos: GetExtensionRepo = Injekt.get(), + private val getExtensionStores: GetExtensionStores = Injekt.get(), ) { suspend operator fun invoke(): List { - return getExtensionRepos.getAll() + return getExtensionStores.get() .map(backupExtensionReposMapper) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt index 256def7b55b..30695095357 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt @@ -2,23 +2,27 @@ package eu.kanade.tachiyomi.data.backup.models import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber -import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extension.model.ExtensionStore @Serializable class BackupExtensionRepos( - @ProtoNumber(1) var baseUrl: String, + @ProtoNumber(1) var indexUrl: String, @ProtoNumber(2) var name: String, - @ProtoNumber(3) var shortName: String?, - @ProtoNumber(4) var website: String, - @ProtoNumber(5) var signingKeyFingerprint: String, + @ProtoNumber(3) var badgeLabel: String?, + @ProtoNumber(5) var signingKey: String, + @ProtoNumber(4) var contactWebsite: String?, + @ProtoNumber(6) var contactDiscord: String?, + @ProtoNumber(7) var isLegacy: Boolean, ) -val backupExtensionReposMapper = { repo: ExtensionRepo -> +val backupExtensionReposMapper = { repo: ExtensionStore -> BackupExtensionRepos( - baseUrl = repo.baseUrl, + indexUrl = repo.indexUrl, name = repo.name, - shortName = repo.shortName, - website = repo.website, - signingKeyFingerprint = repo.signingKeyFingerprint, + badgeLabel = repo.badgeLabel, + signingKey = repo.signingKey, + contactWebsite = repo.contact.website, + contactDiscord = repo.contact.discord, + isLegacy = repo.isLegacy, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt index 7dbc0631f23..c1620a5a2db 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt @@ -1,38 +1,25 @@ package eu.kanade.tachiyomi.data.backup.restore.restorers import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos -import mihon.domain.extensionrepo.interactor.GetExtensionRepo import tachiyomi.data.Database import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class ExtensionRepoRestorer( private val database: Database = Injekt.get(), - private val getExtensionRepos: GetExtensionRepo = Injekt.get(), ) { suspend operator fun invoke( backupRepo: BackupExtensionRepos, ) { - val dbRepos = getExtensionRepos.getAll() - val existingReposBySHA = dbRepos.associateBy { it.signingKeyFingerprint } - val existingReposByUrl = dbRepos.associateBy { it.baseUrl } - - val urlExists = existingReposByUrl[backupRepo.baseUrl] - val shaExists = existingReposBySHA[backupRepo.signingKeyFingerprint] - - if (urlExists != null && urlExists.signingKeyFingerprint != backupRepo.signingKeyFingerprint) { - error("Already Exists with different signing key fingerprint") - } else if (shaExists != null) { - error("${shaExists.name} has the same signing key fingerprint") - } else { - database.extension_reposQueries.insert( - backupRepo.baseUrl, - backupRepo.name, - backupRepo.shortName, - backupRepo.website, - backupRepo.signingKeyFingerprint, - ) - } + database.extension_storeQueries.upsert( + indexUrl = backupRepo.indexUrl, + name = backupRepo.name, + badgeLabel = backupRepo.badgeLabel ?: backupRepo.name, + signingKey = backupRepo.signingKey, + contactWebsite = backupRepo.contactWebsite ?: backupRepo.indexUrl, + contactDiscord = backupRepo.contactDiscord, + isLegacy = backupRepo.isLegacy, + ) } } 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 a631056237f..4f9856ccd3b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -204,11 +204,11 @@ class ExtensionManager( if (extension.hasUpdate != hasUpdate) { installedExtensionsMap[pkgName] = extension.copy( hasUpdate = hasUpdate, - repoUrl = availableExt.repoUrl, + store = availableExt.store, ) } else { installedExtensionsMap[pkgName] = extension.copy( - repoUrl = availableExt.repoUrl, + store = availableExt.store, ) } changed = true @@ -228,7 +228,7 @@ class ExtensionManager( * @param extension The extension to be installed. */ fun installExtension(extension: Extension.Available): Flow { - return installer.downloadAndInstall(api.getApkUrl(extension), extension) + return installer.downloadAndInstall(extension.apkUrl, extension) } /** 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 71e6251dbfe..c3897e431a5 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 @@ -5,64 +5,29 @@ import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.util.ExtensionLoader -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 mihon.domain.extension.interactor.UpdateExtensionStores +import mihon.domain.extension.repository.ExtensionStoreRepository import tachiyomi.core.common.preference.Preference import tachiyomi.core.common.preference.PreferenceStore import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.injectLazy import java.time.Instant import kotlin.time.Duration.Companion.days internal class ExtensionApi { - private val networkService: NetworkHelper by injectLazy() + private val repository: ExtensionStoreRepository by injectLazy() + private val preferenceStore: PreferenceStore by injectLazy() - private val getExtensionRepo: GetExtensionRepo by injectLazy() - private val updateExtensionRepo: UpdateExtensionRepo by injectLazy() + private val updateExtensionStores: UpdateExtensionStores by injectLazy() private val extensionManager: ExtensionManager by injectLazy() - private val json: Json by injectLazy() private val lastExtCheck: Preference by lazy { preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0) } suspend fun findExtensions(): List { - return withIOContext { - getExtensionRepo.getAll() - .map { async { getExtensions(it) } } - .awaitAll() - .flatten() - } - } - - private suspend fun getExtensions(extRepo: ExtensionRepo): List { - val repoBaseUrl = extRepo.baseUrl - return try { - val response = networkService.client - .newCall(GET("$repoBaseUrl/index.min.json")) - .awaitSuccess() - - with(json) { - response - .parseAs>() - .toExtensions(repoBaseUrl) - } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" } - emptyList() - } + return withIOContext { repository.fetchExtensions() } } suspend fun checkForUpdates( @@ -77,7 +42,7 @@ internal class ExtensionApi { } // Update extension repo details - updateExtensionRepo.awaitAll() + updateExtensionStores() val extensions = if (fromAvailableExtensionList) { extensionManager.availableExtensionsFlow.value @@ -107,64 +72,4 @@ internal class ExtensionApi { return extensionsWithUpdate } - - private fun List.toExtensions(repoUrl: String): List { - return this - .filter { - val libVersion = it.extractLibVersion() - libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX - } - .map { - Extension.Available( - name = it.name.substringAfter("Tachiyomi: "), - pkgName = it.pkg, - versionName = it.version, - versionCode = it.code, - libVersion = it.extractLibVersion(), - lang = it.lang, - isNsfw = it.nsfw == 1, - sources = it.sources?.map(extensionSourceMapper).orEmpty(), - apkName = it.apk, - iconUrl = "$repoUrl/icon/${it.pkg}.png", - repoUrl = repoUrl, - ) - } - } - - fun getApkUrl(extension: Extension.Available): String { - return "${extension.repoUrl}/apk/${extension.apkName}" - } - - private fun ExtensionJsonObject.extractLibVersion(): Double { - return version.substringBeforeLast('.').toDouble() - } -} - -@Serializable -private data class ExtensionJsonObject( - val name: String, - val pkg: String, - val apk: String, - val lang: String, - val code: Long, - val version: String, - val nsfw: Int, - val sources: List?, -) - -@Serializable -private data class ExtensionSourceJsonObject( - val id: Long, - val lang: String, - val name: String, - val baseUrl: String, -) - -private val extensionSourceMapper: (ExtensionSourceJsonObject) -> Extension.Available.Source = { - Extension.Available.Source( - id = it.id, - lang = it.lang, - name = it.name, - baseUrl = it.baseUrl, - ) } diff --git a/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt b/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt index 35b800a4bf1..e4335d59591 100644 --- a/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt +++ b/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt @@ -4,8 +4,7 @@ import eu.kanade.domain.source.service.SourcePreferences import logcat.LogPriority import mihon.core.migration.Migration import mihon.core.migration.MigrationContext -import mihon.domain.extensionrepo.exception.SaveExtensionRepoException -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import mihon.domain.extension.repository.ExtensionStoreRepository import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat @@ -14,18 +13,14 @@ class TrustExtensionRepositoryMigration : Migration { override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext { val sourcePreferences = migrationContext.get() ?: return@withIOContext false - val extensionRepositoryRepository = - migrationContext.get() ?: return@withIOContext false + val repository = migrationContext.get() ?: return@withIOContext false for ((index, source) in sourcePreferences.extensionRepos.get().withIndex()) { try { - extensionRepositoryRepository.upsertRepo( - source, - "Repo #${index + 1}", - null, - source, - "NOFINGERPRINT-${index + 1}", + repository.insertFromPreference( + indexUrl = source.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json", + name = "Repo #${index + 1}", ) - } catch (e: SaveExtensionRepoException) { + } catch (e: Exception) { logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" } } } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 002c1a0b3c6..88afea42559 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -23,7 +23,7 @@ android { kotlin { compilerOptions { - freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi") + optIn.add("kotlinx.serialization.ExperimentalSerializationApi") } } @@ -32,5 +32,9 @@ dependencies { implementation(projects.domain) implementation(projects.core.common) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.jsonOkio) + implementation(libs.kotlinx.serialization.protobuf) + api(libs.bundles.sqldelight) } diff --git a/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt b/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt new file mode 100644 index 00000000000..fa04cfc7598 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt @@ -0,0 +1,7 @@ +package mihon.data.extension.model + +import mihon.domain.extension.model.ExtensionStore + +interface BaseNetworkExtensionStore { + fun toExtensionStore(indexUrl: String): ExtensionStore +} diff --git a/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt b/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt new file mode 100644 index 00000000000..0123526e2a2 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt @@ -0,0 +1,110 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames +import kotlinx.serialization.protobuf.ProtoNumber +import mihon.domain.extension.model.ExtensionStore + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class NetworkExtensionStore( + @ProtoNumber(1) val name: String, + @ProtoNumber(2) val badgeLabel: String, + @ProtoNumber(3) val signingKey: String, + @ProtoNumber(4) val contact: Contact, + @ProtoNumber(5) val extensions: List, +) : BaseNetworkExtensionStore { + @Serializable + data class Contact( + @ProtoNumber(1) val website: String, + @ProtoNumber(2) val discord: String?, + ) + + @Serializable + data class Extension( + @ProtoNumber(1) val name: String, + @ProtoNumber(2) val packageName: String, + @ProtoNumber(3) val resources: Resources, + @ProtoNumber(4) val extensionLib: Double, + @ProtoNumber(5) val versionCode: Long, + @ProtoNumber(6) val versionName: String, + @ProtoNumber(7) val sources: List, + ) + + @Serializable + data class Resources( + @ProtoNumber(1) val apkUrl: String, + @ProtoNumber(2) val iconUrl: String, + ) + + @Serializable + data class Source( + @ProtoNumber(1) val id: Long, + @ProtoNumber(2) val name: String, + @ProtoNumber(3) val language: String, + @ProtoNumber(4) val homeUrl: String, + @ProtoNumber(5) val mirrorUrls: List, + @ProtoNumber(6) val contentRating: ContentRating, + @ProtoNumber(7) val message: String?, + ) + + @Suppress("Unused") + enum class ContentRating { + @ProtoNumber(0) + @JsonNames("CONTENT_RATING_SAFE") + SAFE, + + @ProtoNumber(1) + @JsonNames("CONTENT_RATING_SUGGESTIVE") + SUGGESTIVE, + + @ProtoNumber(2) + @JsonNames("CONTENT_RATING_EROTICA") + EROTICA, + + @ProtoNumber(3) + @JsonNames("CONTENT_RATING_PORNOGRAPHIC") + PORNOGRAPHIC, + } + + override fun toExtensionStore(indexUrl: String): ExtensionStore { + return ExtensionStore( + indexUrl = indexUrl, + name = name, + badgeLabel = badgeLabel, + signingKey = signingKey, + contact = ExtensionStore.Contact( + website = contact.website, + discord = contact.discord, + ), + isLegacy = false, + ) + } + + fun toAvailableExtensions(store: ExtensionStore): List { + return extensions.map { extension -> + val lang = extension.sources.map { it.language }.toSet() + eu.kanade.tachiyomi.extension.model.Extension.Available( + name = extension.name, + pkgName = extension.packageName, + apkUrl = extension.resources.apkUrl, + iconUrl = extension.resources.iconUrl, + libVersion = extension.extensionLib, + versionCode = extension.versionCode, + versionName = extension.versionName, + lang = if (lang.size == 1) lang.first() else "all", + isNsfw = extension.sources.maxOfOrNull { it.contentRating } == ContentRating.PORNOGRAPHIC, + sources = extension.sources.map { source -> + eu.kanade.tachiyomi.extension.model.Extension.Available.Source( + id = source.id, + name = source.name, + lang = source.language, + baseUrl = source.homeUrl, + ) + }, + store = store, + ) + } + } +} diff --git a/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt new file mode 100644 index 00000000000..9a47907ced1 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt @@ -0,0 +1,61 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import eu.kanade.tachiyomi.extension.model.Extension +import kotlinx.serialization.Serializable +import mihon.domain.extension.model.ExtensionStore + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class NetworkLegacyExtension( + val name: String, + val pkg: String, + val apk: String, + val lang: String, + val code: Long, + val version: String, + val nsfw: Int, + val sources: List?, +) { + @Serializable + data class Source( + val id: Long, + val lang: String, + val name: String, + val baseUrl: String, + ) + + fun toAvailableExtension(store: ExtensionStore, storeBaseUrl: String): Extension.Available { + return Extension.Available( + name = name.substringAfter("Tachiyomi: "), + pkgName = pkg, + apkUrl = "$storeBaseUrl/apk/$apk", + iconUrl = "$storeBaseUrl/icon/$pkg.png", + libVersion = version.substringBeforeLast('.').toDouble(), + versionCode = code, + versionName = version, + lang = lang, + isNsfw = nsfw == 1, + sources = if (sources.isNullOrEmpty()) { + listOf( + Extension.Available.Source( + id = 0, + name = name, + lang = lang, + baseUrl = "", + ), + ) + } else { + sources.map { source -> + Extension.Available.Source( + id = source.id, + name = source.name, + lang = source.lang, + baseUrl = source.baseUrl, + ) + } + }, + store = store, + ) + } +} diff --git a/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt new file mode 100644 index 00000000000..fcf3c0e6447 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt @@ -0,0 +1,34 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import mihon.domain.extension.model.ExtensionStore + +@SuppressLint("UnsafeOptInUsageError") +@Serializable +data class NetworkLegacyExtensionRepo( + val meta: Meta, +) : BaseNetworkExtensionStore { + @Serializable + data class Meta( + val name: String, + val shortName: String?, + val website: String, + val signingKeyFingerprint: String, + ) + + override fun toExtensionStore(indexUrl: String): ExtensionStore { + return ExtensionStore( + indexUrl = indexUrl, + name = meta.name, + badgeLabel = meta.shortName ?: meta.name, + signingKey = meta.signingKeyFingerprint, + contact = ExtensionStore.Contact( + website = meta.website, + discord = null, + ), + isLegacy = true, + ) + } +} diff --git a/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt new file mode 100644 index 00000000000..140f353aada --- /dev/null +++ b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt @@ -0,0 +1,124 @@ +package mihon.data.extension.repository + +import app.cash.sqldelight.async.coroutines.awaitAsList +import eu.kanade.tachiyomi.extension.model.Extension +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.supervisorScope +import logcat.LogPriority +import mihon.data.extension.service.ExtensionStoreService +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.repository.ExtensionStoreRepository +import tachiyomi.core.common.util.system.logcat +import tachiyomi.data.Database +import tachiyomi.data.subscribeToList +import tachiyomi.data.subscribeToOne + +class ExtensionStoreRepositoryImpl( + private val service: ExtensionStoreService, + private val database: Database, +) : ExtensionStoreRepository { + override suspend fun insert(indexUrl: String): Result { + return service.fetch(indexUrl).mapCatching { upsert(it) } + } + + override suspend fun insertFromPreference(indexUrl: String, name: String) { + database.extension_storeQueries.upsert( + indexUrl = indexUrl, + name = name, + badgeLabel = name, + signingKey = "NO_SIGNING_KEY", + contactWebsite = indexUrl, + contactDiscord = null, + isLegacy = false, + ) + } + + override suspend fun refreshAll() { + try { + database.extension_storeQueries.getAll().awaitAsList().forEach { store -> + service.fetch(store.index_url) + .mapCatching { upsert(it) } + .onFailure { + logcat(LogPriority.ERROR, it) { + "Failed to refresh extension store '${store.name} (${store.index_url})'" + } + } + } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + } + } + + private suspend fun upsert(store: ExtensionStore) { + database.extension_storeQueries.upsert( + indexUrl = store.indexUrl, + name = store.name, + badgeLabel = store.badgeLabel, + signingKey = store.signingKey, + contactWebsite = store.contact.website, + contactDiscord = store.contact.discord, + isLegacy = store.isLegacy, + ) + } + + override suspend fun fetchExtensions(): List { + return try { + supervisorScope { + database.extension_storeQueries.getAll(::extensionStoreMapper).awaitAsList().map { store -> + async { + service.getExtensions(store).onFailure { + this@ExtensionStoreRepositoryImpl.logcat(LogPriority.ERROR, it) { + "Failed to fetch extensions for store '${store.name} (${store.indexUrl})'" + } + } + } + } + .awaitAll() + .flatMap { it.getOrDefault(emptyList()) } + } + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + emptyList() + } + } + + override suspend fun getAll(): List { + return database.extension_storeQueries.getAll(::extensionStoreMapper).awaitAsList() + } + + override fun getAllAsFlow(): Flow> { + return database.extension_storeQueries.getAll(::extensionStoreMapper).subscribeToList() + } + + override fun getCountAsFlow(): Flow { + return database.extension_storeQueries + .getCount() + .subscribeToOne() + } + + override suspend fun remove(indexUrl: String) { + database.extension_storeQueries.delete(indexUrl) + } + + private fun extensionStoreMapper( + indexUrl: String, + name: String, + badgeLabel: String, + signingKey: String, + contactWebsite: String, + contactDiscord: String?, + isLegacy: Boolean, + ): ExtensionStore = ExtensionStore( + indexUrl = indexUrl, + name = name, + badgeLabel = badgeLabel, + signingKey = signingKey, + contact = ExtensionStore.Contact( + website = contactWebsite, + discord = contactDiscord, + ), + isLegacy = isLegacy, + ) +} diff --git a/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt new file mode 100644 index 00000000000..3c3116d23a5 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt @@ -0,0 +1,73 @@ +package mihon.data.extension.service + +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.network.awaitSuccess +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.okio.decodeFromBufferedSource +import kotlinx.serialization.protobuf.ProtoBuf +import mihon.data.extension.model.NetworkExtensionStore +import mihon.data.extension.model.NetworkLegacyExtension +import mihon.data.extension.model.NetworkLegacyExtensionRepo +import mihon.domain.extension.model.ExtensionStore +import kotlin.coroutines.cancellation.CancellationException + +class ExtensionStoreService( + private val network: NetworkHelper, + private val json: Json, + private val protoBuf: ProtoBuf, +) { + suspend fun fetch(indexUrl: String): Result { + return try { + val response = network.client.newCall(GET(indexUrl)).awaitSuccess() + val store = response.body.source().use { source -> + try { + protoBuf.decodeFromByteArray(source.peek().readByteArray()) + } catch (_: IllegalArgumentException) { + try { + json.decodeFromBufferedSource(source.peek()) + } catch (_: IllegalArgumentException) { + json.decodeFromBufferedSource(source.peek()) + } + } + .toExtensionStore(indexUrl) + } + Result.success(store) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun getExtensions(store: ExtensionStore): Result> { + return try { + val extensions = if (!store.isLegacy) { + val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess() + response.body.source().use { source -> + try { + protoBuf.decodeFromByteArray(source.peek().readByteArray()) + .toAvailableExtensions(store) + } catch (_: IllegalArgumentException) { + json.decodeFromBufferedSource(source.peek()) + .toAvailableExtensions(store) + } + } + } else { + val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json") + val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess() + response.body.source().use { source -> + json.decodeFromBufferedSource>(source) + .map { it.toAvailableExtension(store, storeBaseUrl) } + } + } + Result.success(extensions) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + Result.failure(e) + } + } +} diff --git a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt b/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt deleted file mode 100644 index e60f0815e90..00000000000 --- a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt +++ /dev/null @@ -1,116 +0,0 @@ -package mihon.data.repository - -import android.database.SQLException -import app.cash.sqldelight.async.coroutines.awaitAsList -import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull -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.Database -import tachiyomi.data.subscribeToList -import tachiyomi.data.subscribeToOne - -class ExtensionRepoRepositoryImpl( - private val database: Database, -) : ExtensionRepoRepository { - override fun subscribeAll(): Flow> { - return database.extension_reposQueries - .findAll(::mapExtensionRepo) - .subscribeToList() - } - - override suspend fun getAll(): List { - return database.extension_reposQueries - .findAll(::mapExtensionRepo) - .awaitAsList() - } - - override suspend fun getRepo(baseUrl: String): ExtensionRepo? { - return database.extension_reposQueries - .findOne(baseUrl, ::mapExtensionRepo) - .awaitAsOneOrNull() - } - - override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? { - return database.extension_reposQueries - .findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo) - .awaitAsOneOrNull() - } - - override fun getCount(): Flow { - return database.extension_reposQueries - .count() - .subscribeToOne() - .map { it.toInt() } - } - - override suspend fun insertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) { - try { - database.extension_reposQueries.insert( - baseUrl, - name, - shortName, - website, - signingKeyFingerprint, - ) - } catch (ex: SQLException) { - throw SaveExtensionRepoException(ex) - } - } - - override suspend fun upsertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) { - try { - database.extension_reposQueries.upsert( - baseUrl, - name, - shortName, - website, - signingKeyFingerprint, - ) - } catch (ex: SQLException) { - throw SaveExtensionRepoException(ex) - } - } - - override suspend fun replaceRepo(newRepo: ExtensionRepo) { - database.extension_reposQueries.replace( - newRepo.baseUrl, - newRepo.name, - newRepo.shortName, - newRepo.website, - newRepo.signingKeyFingerprint, - ) - } - - override suspend fun deleteRepo(baseUrl: String) { - database.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 deleted file mode 100644 index 6db69132a00..00000000000 --- a/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq +++ /dev/null @@ -1,57 +0,0 @@ -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/data/extension_store.sq b/data/src/main/sqldelight/tachiyomi/data/extension_store.sq new file mode 100644 index 00000000000..82795ddc7b9 --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/data/extension_store.sq @@ -0,0 +1,42 @@ +import kotlin.Boolean; + +CREATE TABLE extension_store( + index_url TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + badge_label TEXT NOT NULL, + signing_key TEXT NOT NULL, + contact_website TEXT NOT NULL, + contact_discord TEXT, + is_legacy INTEGER AS Boolean NOT NULL +); + +get: +SELECT * +FROM extension_store +WHERE index_url = :indexUrl; + +getAll: +SELECT * +FROM extension_store; + +getCount: +SELECT COUNT(*) +FROM extension_store; + +upsert: +INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy) +VALUES (:indexUrl, :name, :badgeLabel, :signingKey, :contactWebsite, :contactDiscord, :isLegacy) +ON CONFLICT(index_url) +DO UPDATE +SET + name = :name, + badge_label = :badgeLabel, + signing_key =: signingKey, + contact_website = :contactWebsite, + contact_discord = :contactDiscord, + is_legacy = :isLegacy +WHERE index_url = :indexUrl; + +delete: +DELETE FROM extension_store +WHERE index_url = :indexUrl; diff --git a/data/src/main/sqldelight/tachiyomi/migrations/11.sqm b/data/src/main/sqldelight/tachiyomi/migrations/11.sqm new file mode 100644 index 00000000000..bc7ab52896a --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/11.sqm @@ -0,0 +1,16 @@ +import kotlin.Boolean; + +CREATE TABLE extension_store( + index_url TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + badge_label TEXT NOT NULL, + signing_key TEXT NOT NULL, + contact_website TEXT NOT NULL, + contact_discord TEXT, + is_legacy INTEGER AS Boolean NOT NULL +); + +INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy) +SELECT base_url || '/repo.json', name, short_name, signing_key_fingerprint, website, NULL, 1 FROM extension_repos; + +DROP TABLE extension_repos; diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt b/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt similarity index 93% rename from app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt rename to domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt index a8e80d0a528..2d6d2041e3f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt +++ b/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.model import android.graphics.drawable.Drawable import eu.kanade.tachiyomi.source.Source +import mihon.domain.extension.model.ExtensionStore import tachiyomi.domain.source.model.StubSource sealed class Extension { @@ -28,7 +29,7 @@ sealed class Extension { val hasUpdate: Boolean = false, val isObsolete: Boolean = false, val isShared: Boolean, - val repoUrl: String? = null, + val store: ExtensionStore? = null, ) : Extension() data class Available( @@ -40,9 +41,9 @@ sealed class Extension { override val lang: String, override val isNsfw: Boolean, val sources: List, - val apkName: String, + val apkUrl: String, val iconUrl: String, - val repoUrl: String, + val store: ExtensionStore, ) : Extension() { data class Source( diff --git a/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt new file mode 100644 index 00000000000..7c10703e55a --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt @@ -0,0 +1,11 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class AddExtensionStore( + private val repository: ExtensionStoreRepository, +) { + suspend operator fun invoke(indexUrl: String): Result { + return repository.insert(indexUrl) + } +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt new file mode 100644 index 00000000000..2871e7f67ac --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStoreCountAsFlow.kt @@ -0,0 +1,9 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class GetExtensionStoreCountAsFlow( + private val repository: ExtensionStoreRepository, +) { + operator fun invoke() = repository.getCountAsFlow() +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt new file mode 100644 index 00000000000..e463af8d648 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/GetExtensionStores.kt @@ -0,0 +1,13 @@ +package mihon.domain.extension.interactor + +import kotlinx.coroutines.flow.Flow +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.repository.ExtensionStoreRepository + +class GetExtensionStores( + private val repository: ExtensionStoreRepository, +) { + suspend fun get(): List = repository.getAll() + + fun subscribe(): Flow> = repository.getAllAsFlow() +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt new file mode 100644 index 00000000000..42a8c07e250 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/RemoveExtensionStore.kt @@ -0,0 +1,11 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class RemoveExtensionStore( + private val repository: ExtensionStoreRepository, +) { + suspend operator fun invoke(indexUrl: String) { + repository.remove(indexUrl) + } +} diff --git a/domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt b/domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt new file mode 100644 index 00000000000..7bbd2812efc --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/interactor/UpdateExtensionStores.kt @@ -0,0 +1,11 @@ +package mihon.domain.extension.interactor + +import mihon.domain.extension.repository.ExtensionStoreRepository + +class UpdateExtensionStores( + private val repository: ExtensionStoreRepository, +) { + suspend operator fun invoke() { + repository.refreshAll() + } +} diff --git a/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt new file mode 100644 index 00000000000..d2cadd0b68c --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt @@ -0,0 +1,15 @@ +package mihon.domain.extension.model + +data class ExtensionStore( + val indexUrl: String, + val name: String, + val badgeLabel: String, + val signingKey: String, + val contact: Contact, + val isLegacy: Boolean, +) { + data class Contact( + val website: String, + val discord: String?, + ) +} diff --git a/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt b/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt new file mode 100644 index 00000000000..3ab4770811a --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt @@ -0,0 +1,23 @@ +package mihon.domain.extension.repository + +import eu.kanade.tachiyomi.extension.model.Extension +import kotlinx.coroutines.flow.Flow +import mihon.domain.extension.model.ExtensionStore + +interface ExtensionStoreRepository { + suspend fun insert(indexUrl: String): Result + + suspend fun insertFromPreference(indexUrl: String, name: String) + + suspend fun refreshAll() + + suspend fun fetchExtensions(): List + + suspend fun getAll(): List + + fun getAllAsFlow(): Flow> + + fun getCountAsFlow(): Flow + + suspend fun remove(indexUrl: String) +} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt b/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt deleted file mode 100644 index 4c6990be09c..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 02623d8af14..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt +++ /dev/null @@ -1,72 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -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.HttpUrl.Companion.toHttpUrlOrNull -import tachiyomi.core.common.util.system.logcat - -class CreateExtensionRepo( - private val repository: ExtensionRepoRepository, - private val service: ExtensionRepoService, -) { - private val repoRegex = """^https://.*/index\.min\.json$""".toRegex() - - suspend fun await(indexUrl: String): Result { - val formattedIndexUrl = indexUrl.toHttpUrlOrNull() - ?.toString() - ?.takeIf { it.matches(repoRegex) } - ?: return Result.InvalidUrl - - val baseUrl = formattedIndexUrl.removeSuffix("/index.min.json") - return service.fetchRepoDetails(baseUrl)?.let { insert(it) } ?: Result.InvalidUrl - } - - private suspend fun insert(repo: ExtensionRepo): Result { - return try { - repository.insertRepo( - 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 - */ - private suspend fun handleInsertionError(repo: ExtensionRepo): Result { - val repoExists = repository.getRepo(repo.baseUrl) - if (repoExists != null) { - return Result.RepoAlreadyExists - } - val matchingFingerprintRepo = repository.getRepoBySigningKeyFingerprint(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 deleted file mode 100644 index 4b4b678e13b..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt +++ /dev/null @@ -1,11 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class DeleteExtensionRepo( - private val repository: ExtensionRepoRepository, -) { - suspend fun await(baseUrl: String) { - repository.deleteRepo(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 deleted file mode 100644 index 25b919607c7..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepo.kt +++ /dev/null @@ -1,13 +0,0 @@ -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 repository: ExtensionRepoRepository, -) { - fun subscribeAll(): Flow> = repository.subscribeAll() - - suspend fun getAll(): List = repository.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 deleted file mode 100644 index a7c4e7c6d09..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/GetExtensionRepoCount.kt +++ /dev/null @@ -1,9 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class GetExtensionRepoCount( - private val repository: ExtensionRepoRepository, -) { - fun subscribe() = repository.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 deleted file mode 100644 index 112ea701c66..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/ReplaceExtensionRepo.kt +++ /dev/null @@ -1,12 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -import mihon.domain.extensionrepo.model.ExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository - -class ReplaceExtensionRepo( - private val repository: ExtensionRepoRepository, -) { - suspend fun await(repo: ExtensionRepo) { - repository.replaceRepo(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 deleted file mode 100644 index a393e69d5ad..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/UpdateExtensionRepo.kt +++ /dev/null @@ -1,30 +0,0 @@ -package mihon.domain.extensionrepo.interactor - -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 repository: ExtensionRepoRepository, - private val service: ExtensionRepoService, -) { - - suspend fun awaitAll() = coroutineScope { - repository.getAll() - .map { async { await(it) } } - .awaitAll() - } - - suspend fun await(repo: ExtensionRepo) { - val newRepo = service.fetchRepoDetails(repo.baseUrl) ?: return - if ( - repo.signingKeyFingerprint.startsWith("NOFINGERPRINT") || - repo.signingKeyFingerprint == newRepo.signingKeyFingerprint - ) { - repository.upsertRepo(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 deleted file mode 100644 index ec9ccca8724..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/model/ExtensionRepo.kt +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 47be56dcf30..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/repository/ExtensionRepoRepository.kt +++ /dev/null @@ -1,47 +0,0 @@ -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 getRepo(baseUrl: String): ExtensionRepo? - - suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? - - fun getCount(): Flow - - suspend fun insertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) - - suspend fun upsertRepo( - baseUrl: String, - name: String, - shortName: String?, - website: String, - signingKeyFingerprint: String, - ) - - suspend fun upsertRepo(repo: ExtensionRepo) { - upsertRepo( - baseUrl = repo.baseUrl, - name = repo.name, - shortName = repo.shortName, - website = repo.website, - signingKeyFingerprint = repo.signingKeyFingerprint, - ) - } - - suspend fun replaceRepo(newRepo: ExtensionRepo) - - suspend fun deleteRepo(baseUrl: String) -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt deleted file mode 100644 index 6a0a492dee8..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoDto.kt +++ /dev/null @@ -1,27 +0,0 @@ -package mihon.domain.extensionrepo.service - -import kotlinx.serialization.Serializable -import mihon.domain.extensionrepo.model.ExtensionRepo - -@Serializable -data class ExtensionRepoMetaDto( - val meta: ExtensionRepoDto, -) - -@Serializable -data class ExtensionRepoDto( - val name: String, - val shortName: String?, - val website: String, - val signingKeyFingerprint: String, -) - -fun ExtensionRepoMetaDto.toExtensionRepo(baseUrl: String): ExtensionRepo { - return ExtensionRepo( - baseUrl = baseUrl, - name = meta.name, - shortName = meta.shortName, - website = meta.website, - signingKeyFingerprint = meta.signingKeyFingerprint, - ) -} diff --git a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt b/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt deleted file mode 100644 index 2d86b89d110..00000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/service/ExtensionRepoService.kt +++ /dev/null @@ -1,36 +0,0 @@ -package mihon.domain.extensionrepo.service - -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.serialization.json.Json -import logcat.LogPriority -import mihon.domain.extensionrepo.model.ExtensionRepo -import tachiyomi.core.common.util.lang.withIOContext -import tachiyomi.core.common.util.system.logcat - -class ExtensionRepoService( - networkHelper: NetworkHelper, - private val json: Json, -) { - val client = networkHelper.client - - suspend fun fetchRepoDetails( - repo: String, - ): ExtensionRepo? { - return withIOContext { - try { - with(json) { - client.newCall(GET("$repo/repo.json")) - .awaitSuccess() - .parseAs() - .toExtensionRepo(baseUrl = repo) - } - } catch (e: Exception) { - logcat(LogPriority.ERROR, e) { "Failed to fetch repo details" } - null - } - } - } -} diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index d0b7edd3605..67beb8d3344 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -374,9 +374,6 @@ Do you wish to delete the repo \"%s\"? Do you wish to add 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