diff --git a/CHANGELOG.md b/CHANGELOG.md index a6ab13892..8c9186091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,24 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - `Other` - for technical stuff. ## [Unreleased] +### Added +- Add support for `tachiyomix` extension index format ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349)) + +### Changed +- Change all reference of extension repo to extension store ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349)) + +### Fixed +- Add missing `outlineVariant` color to Nord theme ([@CompileConnected](https://github.com/CompileConnected)) ([#3184](https://github.com/mihonapp/mihon/pull/3184)) + +## [v0.19.9] - 2026-04-11 +### Fixed +- Regression with installing/updating extension ([@AntsyLich](https://github.com/AntsyLich)) + +## [v0.19.8] - 2026-04-11 +### Added +- Add dedicated "Support Us" screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3200](https://github.com/mihonapp/mihon/pull/3200)) +- Add a one time popup asking for donation from long time users ([@AntsyLich](https://github.com/AntsyLich)) ([#3203](https://github.com/mihonapp/mihon/pull/3203)) + ### Improved - Show informative error when trying to add unapproved titles to list on MAL ([@MajorTanya](https://github.com/MajorTanya)) ([#3155](https://github.com/mihonapp/mihon/pull/3155)) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b72704c99..3b303755c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -73,8 +73,8 @@ - - + + @@ -84,6 +84,17 @@ + + + + + + + + + + + diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 3c1c9e6bf..438e036c7 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 @@ -199,14 +198,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 dc7d2c304..6b25dc37b 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 @@ -28,11 +28,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 c7825a26a..b78b9af41 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 @@ -50,9 +50,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 f70984952..7e8c12564 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 @@ -4,21 +4,21 @@ import android.content.pm.PackageInfo import androidx.core.content.pm.PackageInfoCompat import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.util.system.isDebugBuildType -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo -import mihon.domain.extensionrepo.repository.ExtensionRepoRepository +import mihon.domain.extension.model.KOMIKKU_SIGNATURE +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 { // KMK --> if (isDebugBuildType) return true - if (fingerprints.contains(CreateExtensionRepo.KOMIKKU_SIGNATURE)) return true + if (fingerprints.contains(KOMIKKU_SIGNATURE)) return true // KMK <-- - 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/domain/sync/SyncPreferences.kt b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt index 463dff7d3..2537563a5 100644 --- a/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/sync/SyncPreferences.kt @@ -61,7 +61,7 @@ class SyncPreferences( tracking = preferenceStore.getBoolean("tracking", true).get(), history = preferenceStore.getBoolean("history", true).get(), appSettings = preferenceStore.getBoolean("appSettings", true).get(), - extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(), + extensionStores = preferenceStore.getBoolean("extensionStores", true).get(), sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(), privateSettings = preferenceStore.getBoolean("privateSettings", true).get(), @@ -80,7 +80,7 @@ class SyncPreferences( preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking) preferenceStore.getBoolean("history", true).set(syncSettings.history) preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings) - preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings) + preferenceStore.getBoolean("extensionStores", true).set(syncSettings.extensionStores) preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings) preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings) diff --git a/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt b/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt index 2563bcbc9..55c69a0e6 100644 --- a/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt +++ b/app/src/main/java/eu/kanade/domain/sync/models/SyncSettings.kt @@ -7,7 +7,7 @@ data class SyncSettings( val tracking: Boolean = true, val history: Boolean = true, val appSettings: Boolean = true, - val extensionRepoSettings: Boolean = true, + val extensionStores: Boolean = true, val sourceSettings: Boolean = true, val privateSettings: Boolean = false, 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 2f6bc8865..f4fce3aad 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt @@ -86,12 +86,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( @@ -270,14 +270,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/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt index d287d5ac7..7e6ac8a10 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt @@ -48,7 +48,7 @@ import eu.kanade.presentation.browse.components.BaseBrowseItem import eu.kanade.presentation.browse.components.ExtensionIcon import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.presentation.util.animateItemFastScroll import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState import eu.kanade.tachiyomi.extension.model.Extension @@ -58,6 +58,8 @@ import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission import kotlinx.collections.immutable.persistentListOf +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.model.KOMIKKU_SIGNATURE import tachiyomi.i18n.MR import tachiyomi.i18n.kmk.KMR import tachiyomi.i18n.sy.SYMR @@ -110,9 +112,9 @@ fun ExtensionScreen( modifier = Modifier.padding(contentPadding), actions = persistentListOf( EmptyScreenAction( - stringRes = MR.strings.label_extension_repos, + stringRes = MR.strings.extensionStores, icon = Icons.Outlined.Settings, - onClick = { navigator.push(ExtensionReposScreen()) }, + onClick = { navigator.push(ExtensionStoresScreen()) }, ), ), ) @@ -195,9 +197,9 @@ private fun ExtensionContent( // KMK --> KMR.strings.extensions_page_more -> { { - Button(onClick = { navigator?.push(ExtensionReposScreen()) }) { + Button(onClick = { navigator?.push(ExtensionStoresScreen()) }) { Text( - text = stringResource(MR.strings.action_add_repo), + text = stringResource(MR.strings.action_addExtensionStore), style = LocalTextStyle.current.copy( color = MaterialTheme.colorScheme.onPrimary, ), @@ -401,7 +403,7 @@ private fun ExtensionItemContent( } // KMK --> - Text(text = extension.repoName?.let { "@$it" } ?: "(?)") + Text(text = extension.storeName?.let { "@$it" } ?: "(?)") // KMK <-- val warning = when { @@ -604,11 +606,11 @@ private fun ExtensionItemContentPreview() { libVersion = 1.0, isNsfw = true, signatureHash = "900000", - repoName = "Repository", + storeName = "Repository", sources = emptyList(), - apkName = "Test", + apkUrl = "Test", iconUrl = "", - repoUrl = "", + store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), ) val extInstalled = Extension.Installed( name = "Tachiyomi", @@ -619,9 +621,9 @@ private fun ExtensionItemContentPreview() { libVersion = 1.0, isNsfw = true, signatureHash = "900000", - repoName = "Repository", + storeName = "Repository", sources = emptyList(), - repoUrl = "", + store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), pkgFactory = null, icon = null, hasUpdate = false, @@ -638,12 +640,12 @@ private fun ExtensionItemContentPreview() { libVersion = 1.0, isNsfw = true, signatureHash = "900000", - repoName = "Repository", + storeName = "Repository", ) Column { ExtensionItemContent( extension = extAvail.copy( - repoName = "Repository extensions minion multiple languages various sources", + storeName = "Repository extensions minion multiple languages various sources", ), installStep = InstallStep.Idle, ) 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 e96eb1e72..40d04db6c 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,11 +14,11 @@ import eu.kanade.core.preference.asState import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.more.settings.Preference -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen 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.i18n.kmk.KMR @@ -43,9 +43,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) // SY --> val scope = rememberCoroutineScope() @@ -142,10 +142,10 @@ object SettingsBrowseScreen : SearchableSettings { title = stringResource(MR.strings.pref_hide_in_library_items), ), Preference.PreferenceItem.TextPreference( - title = stringResource(MR.strings.label_extension_repos), - subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount), + title = stringResource(MR.strings.extensionStores), + subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.toInt(), reposCount), onClick = { - navigator.push(ExtensionReposScreen()) + navigator.push(ExtensionStoresScreen()) }, ), ), 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 deleted file mode 100644 index 3cd14aa2b..000000000 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt +++ /dev/null @@ -1,205 +0,0 @@ -package eu.kanade.presentation.more.settings.screen.browse - -import androidx.compose.runtime.Immutable -import cafe.adriel.voyager.core.model.StateScreenModel -import cafe.adriel.voyager.core.model.screenModelScope -import dev.icerock.moko.resources.StringResource -import eu.kanade.domain.source.service.SourcePreferences -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.launchIn -import kotlinx.coroutines.flow.onEach -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 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 extensionManager: ExtensionManager = Injekt.get(), - // KMK --> - private val sourcePreferences: SourcePreferences = Injekt.get(), - // KMK <-- -) : StateScreenModel(RepoScreenState.Loading) { - - private val _events: Channel = Channel(Int.MAX_VALUE) - val events = _events.receiveAsFlow() - - init { - screenModelScope.launchIO { - getExtensionRepo.subscribeAll() - .collectLatest { repos -> - mutableState.update { - RepoScreenState.Success( - repos = repos.toImmutableSet(), - // KMK --> - disabledRepos = sourcePreferences.disabledRepos().get(), - // KMK <-- - ) - } - } - } - - // KMK --> - sourcePreferences.disabledRepos().changes() - .onEach { disabledRepos -> - mutableState.update { - when (it) { - is RepoScreenState.Success -> it.copy(disabledRepos = disabledRepos) - else -> it - } - } - } - .launchIn(screenModelScope) - // KMK <-- - } - - /** - * Creates and adds a new repo to the database. - * - * @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) - } - } - - /** - * 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) { - // KMK --> - // Remove repo from disabled list - enableRepo(baseUrl) - // KMK <-- - screenModelScope.launchIO { - deleteExtensionRepo.await(baseUrl) - extensionManager.findAvailableExtensions() - } - } - - // KMK --> - fun enableRepo(baseUrl: String) { - val disabledRepos = sourcePreferences.disabledRepos().get() - if (baseUrl in disabledRepos) { - sourcePreferences.disabledRepos().set( - disabledRepos.filterNot { it == baseUrl }.toSet(), - ) - } - } - - fun disableRepo(baseUrl: String) { - val disabledRepos = sourcePreferences.disabledRepos().get() - if (baseUrl !in disabledRepos) { - sourcePreferences.disabledRepos().set( - disabledRepos + baseUrl, - ) - } - } - - fun refreshExtensionList() { - screenModelScope.launchIO { - extensionManager.findAvailableExtensions() - } - } - // KMK <-- - - fun showDialog(dialog: RepoDialog) { - mutableState.update { - when (it) { - RepoScreenState.Loading -> it - is RepoScreenState.Success -> it.copy(dialog = dialog) - } - } - } - - fun dismissDialog() { - mutableState.update { - when (it) { - RepoScreenState.Loading -> it - is RepoScreenState.Success -> it.copy(dialog = null) - } - } - } -} - -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() - data class Confirm(val url: String) : RepoDialog() -} - -sealed class RepoScreenState { - - @Immutable - data object Loading : RepoScreenState() - - @Immutable - data class Success( - val repos: ImmutableSet, - val oldRepos: ImmutableSet? = null, - val dialog: RepoDialog? = null, - // KMK --> - val disabledRepos: Set = emptySet(), - // KMK <-- - ) : RepoScreenState() { - - val isEmpty: Boolean - get() = repos.isEmpty() - } -} 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/ExtensionStoresScreen.kt similarity index 53% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt index aa0217fec..af1ce92bd 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/ExtensionStoresScreen.kt @@ -8,20 +8,18 @@ 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.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.more.settings.screen.browse.components.ExtensionStoreConfirmDialog +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreCreateDialog +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreDeleteDialog +import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoresScreen 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 -import kotlinx.coroutines.flow.collectLatest import tachiyomi.i18n.kmk.KMR import tachiyomi.presentation.core.screens.LoadingScreen -class ExtensionReposScreen( +class ExtensionStoresScreen( private val url: String? = null, ) : Screen() { @@ -30,80 +28,72 @@ class ExtensionReposScreen( val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow - val screenModel = rememberScreenModel { ExtensionReposScreenModel() } + val screenModel = rememberScreenModel { ExtensionStoresScreenModel() } val state by screenModel.state.collectAsState() LaunchedEffect(url) { - url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) } + url?.let { screenModel.addFromDeeplink(url) } } - if (state is RepoScreenState.Loading) { + if (state is ExtensionStoreScreenState.Loading) { LoadingScreen() return } - val successState = state as RepoScreenState.Success + val successState = state as ExtensionStoreScreenState.Success - ExtensionReposScreen( + ExtensionStoresScreen( state = successState, - onClickCreate = { screenModel.showDialog(RepoDialog.Create) }, - onOpenWebsite = { context.openInBrowser(it.website) }, - onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) }, + onClickCreate = { screenModel.showDialog(ExtensionStoreDialog.Create()) }, + onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) }, + onOpenWebsite = { it.contact.website.let(context::openInBrowser) }, + onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) }, // KMK --> onClickEnable = { - screenModel.enableRepo(it) + screenModel.enableStore(it.indexUrl) screenModel.refreshExtensionList() context.toast(KMR.strings.extensions_page_need_refresh) }, onClickDisable = { - screenModel.disableRepo(it) + screenModel.disableStore(it.indexUrl) screenModel.refreshExtensionList() context.toast(KMR.strings.extensions_page_need_refresh) }, // KMK <-- + onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) }, onClickRefresh = { screenModel.refreshRepos() }, navigateUp = navigator::pop, ) when (val dialog = successState.dialog) { null -> {} - is RepoDialog.Create -> { - ExtensionRepoCreateDialog( + is ExtensionStoreDialog.Create -> { + ExtensionStoreCreateDialog( onDismissRequest = screenModel::dismissDialog, onCreate = { screenModel.createRepo(it) }, - repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(), + storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(), + processing = dialog.processing, + errorMessage = dialog.errorMessage, ) } - is RepoDialog.Delete -> { - ExtensionRepoDeleteDialog( + is ExtensionStoreDialog.Delete -> { + ExtensionStoreDeleteDialog( onDismissRequest = screenModel::dismissDialog, - onDelete = { screenModel.deleteRepo(dialog.repo) }, - repo = dialog.repo, + onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) }, + storeName = dialog.store.name, + storeIndexUrl = dialog.store.indexUrl, ) } - is RepoDialog.Conflict -> { - ExtensionRepoConflictDialog( - onDismissRequest = screenModel::dismissDialog, - onMigrate = { screenModel.replaceRepo(dialog.newRepo) }, - oldRepo = dialog.oldRepo, - newRepo = dialog.newRepo, - ) - } - is RepoDialog.Confirm -> { - ExtensionRepoConfirmDialog( + is ExtensionStoreDialog.Confirm -> { + ExtensionStoreConfirmDialog( onDismissRequest = screenModel::dismissDialog, onCreate = { screenModel.createRepo(dialog.url) }, - repo = dialog.url, + storeIndexUrl = dialog.url, + storeAlreadyExists = dialog.alreadyExists, + processing = dialog.processing, + errorMessage = dialog.errorMessage, ) } } - - LaunchedEffect(Unit) { - screenModel.events.collectLatest { event -> - if (event is RepoEvent.LocalizedMessage) { - context.toast(event.stringRes) - } - } - } } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt new file mode 100644 index 000000000..f794576d3 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt @@ -0,0 +1,223 @@ +package eu.kanade.presentation.more.settings.screen.browse + +import androidx.compose.runtime.Immutable +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import eu.kanade.domain.source.service.SourcePreferences +import eu.kanade.tachiyomi.extension.ExtensionManager +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +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 uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionStoresScreenModel( + 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(), + // KMK --> + private val sourcePreferences: SourcePreferences = Injekt.get(), + // KMK <-- +) : StateScreenModel(ExtensionStoreScreenState.Loading) { + + private inline fun updateSuccessState( + func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success, + ) { + mutableState.update { + when (it) { + ExtensionStoreScreenState.Loading -> it + is ExtensionStoreScreenState.Success -> func(it) + } + } + } + + init { + screenModelScope.launchIO { + getExtensionStores.subscribe() + .collectLatest { stores -> + mutableState.update { + when (it) { + ExtensionStoreScreenState.Loading -> ExtensionStoreScreenState.Success( + stores = stores, + // KMK --> + disabledRepos = sourcePreferences.disabledRepos().get(), + // KMK <-- + ) + is ExtensionStoreScreenState.Success -> it.copy(stores = stores) + } + } + } + } + + // KMK --> + sourcePreferences.disabledRepos().changes() + .onEach { disabledRepos -> + mutableState.update { + when (it) { + is ExtensionStoreScreenState.Success -> it.copy(disabledRepos = disabledRepos) + else -> it + } + } + } + .launchIn(screenModelScope) + // KMK <-- + } + + /** + * Creates and adds a new repo to the database. + * + * @param indexUrl The baseUrl of the repo to create. + */ + fun createRepo(indexUrl: String) { + // KMK --> + screenModelScope.launchIO { + // KMK <-- + updateSuccessState { + it.copy( + dialog = when (it.dialog) { + is ExtensionStoreDialog.Create -> it.dialog.copy(processing = true) + is ExtensionStoreDialog.Confirm -> it.dialog.copy(processing = true) + else -> it.dialog + }, + ) + } + addExtensionStore(indexUrl) + .onSuccess { + extensionManager.findAvailableExtensions() + dismissDialog() + } + .onFailure { throwable -> + updateSuccessState { + it.copy( + dialog = when (it.dialog) { + is ExtensionStoreDialog.Create -> it.dialog.copy( + processing = false, + errorMessage = throwable.message ?: "unknown error", + ) + is ExtensionStoreDialog.Confirm -> it.dialog.copy( + processing = false, + errorMessage = throwable.message ?: "unknown error", + ) + else -> it.dialog + }, + ) + } + } + } + } + + /** + * Refreshes information for each repository. + */ + fun refreshRepos() { + val status = state.value + + if (status is ExtensionStoreScreenState.Success) { + screenModelScope.launchIO { + updateExtensionStores() + } + } + } + + /** + * Deletes the given repo from the database + */ + fun deleteRepo(indexUrl: String) { + // KMK --> + // Remove repo from disabled list + enableStore(indexUrl) + // KMK <-- + screenModelScope.launchIO { + removeExtensionStore(indexUrl) + extensionManager.findAvailableExtensions() + } + } + + // KMK --> + fun enableStore(indexUrl: String) { + val disabledRepos = sourcePreferences.disabledRepos().get() + if (indexUrl in disabledRepos) { + sourcePreferences.disabledRepos().set( + disabledRepos.filterNot { it == indexUrl }.toSet(), + ) + } + } + + fun disableStore(indexUrl: String) { + val disabledRepos = sourcePreferences.disabledRepos().get() + if (indexUrl !in disabledRepos) { + sourcePreferences.disabledRepos().set( + disabledRepos + indexUrl, + ) + } + } + + fun refreshExtensionList() { + screenModelScope.launchIO { + extensionManager.findAvailableExtensions() + } + } + // KMK <-- + + fun addFromDeeplink(storeIndexUrl: String) { + updateSuccessState { state -> + state.copy( + dialog = ExtensionStoreDialog.Confirm( + url = storeIndexUrl, + alreadyExists = state.stores.any { it.indexUrl == storeIndexUrl }, + ), + ) + } + } + + fun showDialog(dialog: ExtensionStoreDialog) { + updateSuccessState { state -> + state.copy(dialog = dialog) + } + } + + fun dismissDialog() { + updateSuccessState { + it.copy(dialog = null) + } + } +} + +sealed class ExtensionStoreDialog { + data class Create(val processing: Boolean = false, val errorMessage: String? = null) : ExtensionStoreDialog() + data class Delete(val store: ExtensionStore) : ExtensionStoreDialog() + data class Confirm( + val url: String, + val alreadyExists: Boolean = false, + val processing: Boolean = false, + val errorMessage: String? = null, + ) : ExtensionStoreDialog() +} + +sealed class ExtensionStoreScreenState { + + @Immutable + data object Loading : ExtensionStoreScreenState() + + @Immutable + data class Success( + val stores: List, + val dialog: ExtensionStoreDialog? = null, + // KMK --> + val disabledRepos: Set = emptySet(), + // KMK <-- + ) : ExtensionStoreScreenState() { + + val isEmpty: Boolean + get() = stores.isEmpty() + } +} 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 deleted file mode 100644 index bd7c1cedd..000000000 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposDialogs.kt +++ /dev/null @@ -1,188 +0,0 @@ -package eu.kanade.presentation.more.settings.screen.browse.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import 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 - -@Composable -fun ExtensionRepoCreateDialog( - onDismissRequest: () -> Unit, - onCreate: (String) -> Unit, - repoUrls: ImmutableSet, -) { - var name by remember { mutableStateOf("") } - - val focusRequester = remember { FocusRequester() } - val nameAlreadyExists = remember(name) { repoUrls.contains(name) } - - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - enabled = name.isNotEmpty() && !nameAlreadyExists, - onClick = { - onCreate(name) - onDismissRequest() - }, - ) { - Text(text = stringResource(MR.strings.action_add)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - title = { - Text(text = stringResource(MR.strings.action_add_repo)) - }, - text = { - Column { - Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name))) - - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - value = name, - onValueChange = { name = it }, - label = { - Text(text = stringResource(MR.strings.label_add_repo_input)) - }, - supportingText = { - val msgRes = if (name.isNotEmpty() && nameAlreadyExists) { - MR.strings.error_repo_exists - } else { - MR.strings.information_required_plain - } - Text(text = stringResource(msgRes)) - }, - isError = name.isNotEmpty() && nameAlreadyExists, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), - singleLine = true, - ) - } - }, - ) - - LaunchedEffect(focusRequester) { - // TODO: https://issuetracker.google.com/issues/204502668 - delay(0.1.seconds) - focusRequester.requestFocus() - } -} - -@Composable -fun ExtensionRepoDeleteDialog( - onDismissRequest: () -> Unit, - onDelete: () -> Unit, - repo: String, -) { - AlertDialog( - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton(onClick = { - onDelete() - onDismissRequest() - }) { - Text(text = stringResource(MR.strings.action_ok)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - title = { - Text(text = stringResource(MR.strings.action_delete_repo)) - }, - text = { - Text(text = stringResource(MR.strings.delete_repo_confirmation, repo)) - }, - ) -} - -@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, - onCreate: () -> Unit, - repo: String, -) { - AlertDialog( - onDismissRequest = onDismissRequest, - title = { - Text(text = stringResource(MR.strings.action_add_repo)) - }, - text = { - Text(text = stringResource(MR.strings.add_repo_confirmation, repo)) - }, - confirmButton = { - TextButton( - onClick = { - onCreate() - onDismissRequest() - }, - ) { - Text(text = stringResource(MR.strings.action_add)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - ) -} 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/ExtensionStoresContent.kt similarity index 71% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposContent.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresContent.kt index 346e38c43..ed7ad079b 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/ExtensionStoresContent.kt @@ -11,9 +11,9 @@ import androidx.compose.foundation.layout.size 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.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.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material3.ElevatedCard @@ -26,33 +26,33 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.imageResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.copyToClipboard -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.persistentSetOf -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE -import mihon.domain.extensionrepo.model.ExtensionRepo +import kotlinx.collections.immutable.persistentListOf +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.model.KOMIKKU_SIGNATURE +import mihon.domain.extension.model.REPO_SIGNATURE 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, +fun ExtensionStoresContent( + repos: List, lazyListState: LazyListState, paddingValues: PaddingValues, - onOpenWebsite: (ExtensionRepo) -> Unit, - onClickDelete: (String) -> Unit, + onCopy: (ExtensionStore) -> Unit, + onOpenWebsite: (ExtensionStore) -> Unit, + onOpenDiscord: (ExtensionStore) -> Unit, + onClickDelete: (ExtensionStore) -> Unit, // KMK --> - onClickEnable: (String) -> Unit, - onClickDisable: (String) -> Unit, + onClickEnable: (ExtensionStore) -> Unit, + onClickDisable: (ExtensionStore) -> Unit, disabledRepos: Set, // KMK <-- modifier: Modifier = Modifier, @@ -65,15 +65,17 @@ fun ExtensionReposContent( ) { repos.forEach { item { - ExtensionRepoListItem( + ExtensionStoresListItem( modifier = Modifier.animateItem(), - repo = it, + store = it, onOpenWebsite = { onOpenWebsite(it) }, - onDelete = { onClickDelete(it.baseUrl) }, + onOpenDiscord = { onOpenDiscord(it) }, + onCopy = { onCopy(it) }, + onDelete = { onClickDelete(it) }, // KMK --> - onEnable = { onClickEnable(it.baseUrl) }, - onDisable = { onClickDisable(it.baseUrl) }, - isDisabled = it.baseUrl in disabledRepos, + onEnable = { onClickEnable(it) }, + onDisable = { onClickDisable(it) }, + isDisabled = it.indexUrl in disabledRepos, // KMK <-- ) } @@ -82,9 +84,11 @@ fun ExtensionReposContent( } @Composable -private fun ExtensionRepoListItem( - repo: ExtensionRepo, +private fun ExtensionStoresListItem( + store: ExtensionStore, onOpenWebsite: () -> Unit, + onOpenDiscord: () -> Unit, + onCopy: () -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier, // KMK --> @@ -93,8 +97,6 @@ private fun ExtensionRepoListItem( onDisable: () -> Unit, // KMK <-- ) { - val context = LocalContext.current - ElevatedCard( modifier = modifier, ) { @@ -103,9 +105,9 @@ private fun ExtensionRepoListItem( modifier = Modifier .padding(start = MaterialTheme.padding.medium), ) { - val resId = repoResId(repo.signingKeyFingerprint) + val resId = repoResId(store.signingKey) Image( - bitmap = ImageBitmap.imageResource(id = resId), + painter = painterResource(id = resId), contentDescription = null, alpha = if (isDisabled) 0.4f else 1f, modifier = Modifier @@ -126,7 +128,7 @@ private fun ExtensionRepoListItem( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = repo.name, + text = store.name, // KMK: modifier = Modifier.padding(start = MaterialTheme.padding.medium), style = MaterialTheme.typography.titleMedium, // KMK --> @@ -142,17 +144,21 @@ private fun ExtensionRepoListItem( ) { IconButton(onClick = onOpenWebsite) { Icon( - imageVector = Icons.AutoMirrored.Outlined.OpenInNew, + imageVector = Icons.Outlined.Public, contentDescription = stringResource(MR.strings.action_open_in_browser), ) } - IconButton( - onClick = { - val url = "${repo.baseUrl}/index.min.json" - context.copyToClipboard(url, url) - }, - ) { + if (store.contact.discord != null) { + IconButton(onClick = onOpenDiscord) { + Icon( + imageVector = CustomIcons.Discord, + contentDescription = null, + ) + } + } + + IconButton(onClick = onCopy) { Icon( imageVector = Icons.Outlined.ContentCopy, contentDescription = stringResource(MR.strings.action_copy_to_clipboard), @@ -190,16 +196,18 @@ fun repoResId(signKey: String) = when (signKey) { @Preview @Composable fun ExtensionReposContentPreview() { - val repos = persistentSetOf( - ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE), - ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE), - ExtensionRepo("https://repo", "Other", "", "", "key2"), + val repos = persistentListOf( + ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true), ) - ExtensionReposContent( + ExtensionStoresContent( repos = repos, lazyListState = LazyListState(), paddingValues = PaddingValues(), + onCopy = {}, onOpenWebsite = {}, + onOpenDiscord = {}, onClickDelete = {}, onClickEnable = {}, onClickDisable = {}, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt new file mode 100644 index 000000000..19436f17a --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresDialogs.kt @@ -0,0 +1,192 @@ +package eu.kanade.presentation.more.settings.screen.browse.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource +import kotlin.time.Duration.Companion.seconds + +@Composable +fun ExtensionStoreCreateDialog( + onDismissRequest: () -> Unit, + onCreate: (String) -> Unit, + storeIndexUrls: Set, + processing: Boolean, + errorMessage: String?, +) { + val state = rememberTextFieldState() + val storeAlreadyExists by remember(storeIndexUrls) { + derivedStateOf { + val indexUrl = state.text.toString() + storeIndexUrls.contains(indexUrl) + } + } + + val focusRequester = remember { FocusRequester() } + + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_title)) + }, + text = { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + state = state, + label = { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStoreInput_inputLabel)) + }, + supportingText = { + val msgRes = if (storeAlreadyExists) { + MR.strings.extensionStoresScreen_addStore_alreadyExists + } else { + MR.strings.information_required_plain + } + Text(text = errorMessage ?: stringResource(msgRes)) + }, + isError = errorMessage != null || storeAlreadyExists, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri), + lineLimits = TextFieldLineLimits.SingleLine, + ) + }, + confirmButton = { + TextButton( + onClick = { onCreate(state.text.toString()) }, + enabled = !processing && state.text.isNotEmpty() && !storeAlreadyExists, + ) { + Text( + text = stringResource( + resource = if (processing) { + MR.strings.extensionStoresScreen_addStore_processing + } else { + MR.strings.action_add + }, + ), + ) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) + + LaunchedEffect(focusRequester) { + // TODO: https://issuetracker.google.com/issues/204502668 + delay(0.1.seconds) + focusRequester.requestFocus() + } +} + +@Composable +fun ExtensionStoreDeleteDialog( + onDismissRequest: () -> Unit, + onDelete: () -> Unit, + storeName: String, + storeIndexUrl: String, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.extensionStoresScreen_deleteStore_title)) + }, + text = { + Text(text = stringResource(MR.strings.extensionStoresScreen_deleteStore_body, storeName, storeIndexUrl)) + }, + confirmButton = { + TextButton(onClick = { + onDelete() + onDismissRequest() + }) { + Text(text = stringResource(MR.strings.action_ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) +} + +@Composable +fun ExtensionStoreConfirmDialog( + onDismissRequest: () -> Unit, + onCreate: () -> Unit, + storeIndexUrl: String, + storeAlreadyExists: Boolean, + processing: Boolean, + errorMessage: String?, +) { + val state = rememberTextFieldState(initialText = storeIndexUrl) + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_title)) + }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStoreDeeplink_bodyText)) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + state = state, + readOnly = true, + supportingText = when { + storeAlreadyExists -> { + { + Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_alreadyExists)) + } + } + errorMessage != null -> { + { + Text(text = errorMessage) + } + } + else -> null + }, + isError = errorMessage != null || storeAlreadyExists, + ) + } + }, + confirmButton = { + TextButton(onClick = onCreate, enabled = !storeAlreadyExists && !processing) { + Text( + text = stringResource( + resource = if (processing) { + MR.strings.extensionStoresScreen_addStore_processing + } else { + MR.strings.action_add + }, + ), + ) + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) +} 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/ExtensionStoresScreen.kt similarity index 71% rename from app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionReposScreen.kt rename to app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/components/ExtensionStoresScreen.kt index 651b45f51..89e8a0138 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/ExtensionStoresScreen.kt @@ -1,5 +1,3 @@ -@file:JvmName("ExtensionReposScreenKt") - package eu.kanade.presentation.more.settings.screen.browse.components import androidx.compose.foundation.layout.PaddingValues @@ -21,13 +19,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import eu.kanade.presentation.category.components.CategoryFloatingActionButton import eu.kanade.presentation.components.AppBar -import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoreScreenState import eu.kanade.tachiyomi.util.system.openInBrowser -import kotlinx.collections.immutable.persistentSetOf -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_HELP -import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE -import mihon.domain.extensionrepo.model.ExtensionRepo +import kotlinx.collections.immutable.persistentListOf +import mihon.domain.extension.model.ExtensionStore +import mihon.domain.extension.model.KOMIKKU_SIGNATURE +import mihon.domain.extension.model.REPO_HELP +import mihon.domain.extension.model.REPO_SIGNATURE import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding @@ -37,14 +35,16 @@ import tachiyomi.presentation.core.screens.EmptyScreen import tachiyomi.presentation.core.util.plus @Composable -fun ExtensionReposScreen( - state: RepoScreenState.Success, +fun ExtensionStoresScreen( + state: ExtensionStoreScreenState.Success, onClickCreate: () -> Unit, - onOpenWebsite: (ExtensionRepo) -> Unit, - onClickDelete: (String) -> Unit, + onCopy: (ExtensionStore) -> Unit, + onOpenWebsite: (ExtensionStore) -> Unit, + onOpenDiscord: (ExtensionStore) -> Unit, + onClickDelete: (ExtensionStore) -> Unit, // KMK --> - onClickEnable: (String) -> Unit, - onClickDisable: (String) -> Unit, + onClickEnable: (ExtensionStore) -> Unit, + onClickDisable: (ExtensionStore) -> Unit, // KMK <-- onClickRefresh: () -> Unit, navigateUp: () -> Unit, @@ -54,7 +54,7 @@ fun ExtensionReposScreen( topBar = { scrollBehavior -> AppBar( navigateUp = navigateUp, - title = stringResource(MR.strings.label_extension_repos), + title = stringResource(MR.strings.extensionStores), scrollBehavior = scrollBehavior, actions = { IconButton(onClick = onClickRefresh) { @@ -76,7 +76,7 @@ fun ExtensionReposScreen( if (state.isEmpty) { val context = LocalContext.current EmptyScreen( - MR.strings.information_empty_repos, + MR.strings.extensionStoresScreen_emptyLabel, modifier = Modifier.padding(paddingValues), // KMK --> help = { @@ -94,12 +94,14 @@ fun ExtensionReposScreen( return@Scaffold } - ExtensionReposContent( - repos = state.repos, + ExtensionStoresContent( + repos = state.stores, lazyListState = lazyListState, paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium), + onCopy = onCopy, onOpenWebsite = onOpenWebsite, + onOpenDiscord = onOpenDiscord, onClickDelete = onClickDelete, // KMK --> onClickEnable = onClickEnable, @@ -113,19 +115,21 @@ fun ExtensionReposScreen( // KMK --> @Preview @Composable -private fun ExtensionReposScreenPreview() { - val state = RepoScreenState.Success( - repos = persistentSetOf( - ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE), - ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE), - ExtensionRepo("https://repo", "Other", "", "", "key2"), +private fun ExtensionStoresScreenPreview() { + val state = ExtensionStoreScreenState.Success( + stores = persistentListOf( + ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false), + ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true), ), disabledRepos = setOf("https://repo"), ) - ExtensionReposScreen( + ExtensionStoresScreen( state = state, onClickCreate = { }, + onCopy = { }, onOpenWebsite = { }, + onOpenDiscord = { }, onClickDelete = { }, onClickEnable = { }, onClickDisable = { }, @@ -136,12 +140,14 @@ private fun ExtensionReposScreenPreview() { @Preview @Composable -private fun ExtensionReposScreenEmptyPreview() { - val state = RepoScreenState.Success(repos = persistentSetOf()) - ExtensionReposScreen( +private fun ExtensionStoresScreenEmptyPreview() { + val state = ExtensionStoreScreenState.Success(stores = persistentListOf()) + ExtensionStoresScreen( state = state, onClickCreate = { }, + onCopy = { }, onOpenWebsite = { }, + onOpenDiscord = { }, onClickDelete = { }, onClickEnable = { }, onClickDisable = { }, diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt index 83e4b6c08..f198455fc 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/SyncSettingsSelector.kt @@ -114,7 +114,7 @@ private class SyncSettingsSelectorModel( tracking = syncSettings.tracking, history = syncSettings.history, appSettings = syncSettings.appSettings, - extensionRepoSettings = syncSettings.extensionRepoSettings, + extensionStores = syncSettings.extensionStores, sourceSettings = syncSettings.sourceSettings, privateSettings = syncSettings.privateSettings, @@ -134,7 +134,7 @@ private class SyncSettingsSelectorModel( tracking = backupOptions.tracking, history = backupOptions.history, appSettings = backupOptions.appSettings, - extensionRepoSettings = backupOptions.extensionRepoSettings, + extensionStores = backupOptions.extensionStores, sourceSettings = backupOptions.sourceSettings, privateSettings = backupOptions.privateSettings, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt index 731f7bad4..5014f6ac5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt @@ -6,7 +6,7 @@ import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator -import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionRepoBackupCreator +import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionStoresBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.FeedBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator @@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.backup.create.creators.SavedSearchBackupCreator import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupCategory -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore import eu.kanade.tachiyomi.data.backup.models.BackupFeed import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference @@ -54,7 +54,7 @@ class BackupCreator( private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(), private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(), private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(), - private val extensionRepoBackupCreator: ExtensionRepoBackupCreator = ExtensionRepoBackupCreator(), + private val extensionStoresBackupCreator: ExtensionStoresBackupCreator = ExtensionStoresBackupCreator(), private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(), // KMK --> private val feedBackupCreator: FeedBackupCreator = FeedBackupCreator(), @@ -101,7 +101,7 @@ class BackupCreator( backupCategories = backupCategories(options), backupSources = backupSources(backupManga), backupPreferences = backupAppPreferences(options), - backupExtensionRepo = backupExtensionRepos(options), + backupExtensionStores = backupExtensionStores(options), backupSourcePreferences = backupSourcePreferences(options), // SY --> @@ -159,16 +159,16 @@ class BackupCreator( return sourcesBackupCreator(mangas) } - /* KMK --> */ suspend /* KMK <-- */ fun backupAppPreferences(options: BackupOptions): List { + internal fun backupAppPreferences(options: BackupOptions): List { if (!options.appSettings) return emptyList() return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings) } - suspend fun backupExtensionRepos(options: BackupOptions): List { - if (!options.extensionRepoSettings) return emptyList() + internal suspend fun backupExtensionStores(options: BackupOptions): List { + if (!options.extensionStores) return emptyList() - return extensionRepoBackupCreator() + return extensionStoresBackupCreator() } fun backupSourcePreferences(options: BackupOptions): List { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt index 469137c35..666ba9d3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt @@ -14,7 +14,7 @@ data class BackupOptions( val history: Boolean = true, val readEntries: Boolean = true, val appSettings: Boolean = true, - val extensionRepoSettings: Boolean = true, + val extensionStores: Boolean = true, val sourceSettings: Boolean = true, val privateSettings: Boolean = false, // SY --> @@ -31,7 +31,7 @@ data class BackupOptions( history, readEntries, appSettings, - extensionRepoSettings, + extensionStores, sourceSettings, privateSettings, // SY --> @@ -41,7 +41,7 @@ data class BackupOptions( ) fun canCreate() = - libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearchesFeeds + libraryEntries || categories || appSettings || extensionStores || sourceSettings || savedSearchesFeeds companion object { val libraryOptions = persistentListOf( @@ -103,9 +103,9 @@ data class BackupOptions( setter = { options, enabled -> options.copy(appSettings = enabled) }, ), Entry( - label = MR.strings.extensionRepo_settings, - getter = BackupOptions::extensionRepoSettings, - setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) }, + label = MR.strings.extensionStores, + getter = BackupOptions::extensionStores, + setter = { options, enabled -> options.copy(extensionStores = enabled) }, ), Entry( label = MR.strings.source_settings, @@ -128,7 +128,7 @@ data class BackupOptions( history = array[4], readEntries = array[5], appSettings = array[6], - extensionRepoSettings = array[7], + extensionStores = array[7], sourceSettings = array[8], privateSettings = array[9], // SY --> 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 deleted file mode 100644 index 2db6a6f06..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionRepoBackupCreator.kt +++ /dev/null @@ -1,17 +0,0 @@ -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 uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionRepoBackupCreator( - private val getExtensionRepos: GetExtensionRepo = Injekt.get(), -) { - - suspend operator fun invoke(): List { - return getExtensionRepos.getAll() - .map(backupExtensionReposMapper) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt new file mode 100644 index 000000000..0ca4aa59e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/creators/ExtensionStoresBackupCreator.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.backup.create.creators + +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore +import eu.kanade.tachiyomi.data.backup.models.backupExtensionStoreMapper +import mihon.domain.extension.interactor.GetExtensionStores +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionStoresBackupCreator( + private val getExtensionStores: GetExtensionStores = Injekt.get(), +) { + + suspend operator fun invoke(): List { + return getExtensionStores.get() + .map(backupExtensionStoreMapper) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt index cfb6198c2..7c37ef1d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/Backup.kt @@ -11,7 +11,7 @@ data class Backup( @ProtoNumber(101) var backupSources: List = emptyList(), @ProtoNumber(104) var backupPreferences: List = emptyList(), @ProtoNumber(105) var backupSourcePreferences: List = emptyList(), - @ProtoNumber(106) var backupExtensionRepo: List = emptyList(), + @ProtoNumber(106) var backupExtensionStores: List = emptyList(), // SY specific values @ProtoNumber(600) var backupSavedSearches: List = emptyList(), // KMK --> 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 deleted file mode 100644 index 256def7b5..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionRepos.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.kanade.tachiyomi.data.backup.models - -import kotlinx.serialization.Serializable -import kotlinx.serialization.protobuf.ProtoNumber -import mihon.domain.extensionrepo.model.ExtensionRepo - -@Serializable -class BackupExtensionRepos( - @ProtoNumber(1) var baseUrl: String, - @ProtoNumber(2) var name: String, - @ProtoNumber(3) var shortName: String?, - @ProtoNumber(4) var website: String, - @ProtoNumber(5) var signingKeyFingerprint: String, -) - -val backupExtensionReposMapper = { repo: ExtensionRepo -> - BackupExtensionRepos( - baseUrl = repo.baseUrl, - name = repo.name, - shortName = repo.shortName, - website = repo.website, - signingKeyFingerprint = repo.signingKeyFingerprint, - ) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt new file mode 100644 index 000000000..2304f260c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupExtensionStore.kt @@ -0,0 +1,28 @@ +package eu.kanade.tachiyomi.data.backup.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import mihon.domain.extension.model.ExtensionStore + +@Serializable +class BackupExtensionStore( + @ProtoNumber(1) var indexUrl: String, + @ProtoNumber(2) var name: 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 backupExtensionStoreMapper = { store: ExtensionStore -> + BackupExtensionStore( + indexUrl = store.indexUrl, + name = store.name, + badgeLabel = store.badgeLabel, + signingKey = store.signingKey, + contactWebsite = store.contact.website, + contactDiscord = store.contact.discord, + isLegacy = store.isLegacy, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt index 3f8d6ef22..9f578bbd3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt @@ -5,14 +5,14 @@ import android.net.Uri import eu.kanade.tachiyomi.data.backup.BackupDecoder import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.models.BackupCategory -import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore import eu.kanade.tachiyomi.data.backup.models.BackupFeed import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupPreference import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer -import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionRepoRestorer +import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionStoreRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.FeedRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer @@ -38,7 +38,7 @@ class BackupRestorer( private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(), private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context), - private val extensionRepoRestorer: ExtensionRepoRestorer = ExtensionRepoRestorer(), + private val extensionStoreRestorer: ExtensionStoreRestorer = ExtensionStoreRestorer(), private val mangaRestorer: MangaRestorer = MangaRestorer(isSync), // SY --> private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(), @@ -96,8 +96,8 @@ class BackupRestorer( if (options.appSettings) { restoreAmount += 1 } - if (options.extensionRepoSettings) { - restoreAmount += backup.backupExtensionRepo.size + if (options.extensionStores) { + restoreAmount += backup.backupExtensionStores.size } if (options.sourceSettings) { restoreAmount += 1 @@ -126,8 +126,8 @@ class BackupRestorer( if (options.libraryEntries) { restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList()) } - if (options.extensionRepoSettings) { - restoreExtensionRepos(backup.backupExtensionRepo) + if (options.extensionStores) { + restoreExtensionStores(backup.backupExtensionStores) } // TODO: optionally trigger online library + tracker update @@ -248,23 +248,25 @@ class BackupRestorer( } } - private fun CoroutineScope.restoreExtensionRepos( - backupExtensionRepo: List, + private fun CoroutineScope.restoreExtensionStores( + backupExtensionStores: List, ) = launch { - backupExtensionRepo + backupExtensionStores .forEach { ensureActive() try { - extensionRepoRestorer(it) + extensionStoreRestorer(it) } catch (e: Exception) { - errors.add(Date() to "Error Adding Repo: ${it.name} : ${e.message}") + errors.add(Date() to "Error adding extension store: ${it.name} : ${e.message}") } restoreProgress += 1 + // KMK --> with(notifier) { + // KMK <-- showRestoreProgress( - context.stringResource(MR.strings.extensionRepo_settings), + context.stringResource(MR.strings.extensionStores), restoreProgress, restoreAmount, isSync, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt index 182b679d0..df62c7412 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt @@ -9,7 +9,7 @@ data class RestoreOptions( val libraryEntries: Boolean = true, val categories: Boolean = true, val appSettings: Boolean = true, - val extensionRepoSettings: Boolean = true, + val extensionStores: Boolean = true, val sourceSettings: Boolean = true, // SY --> val savedSearchesFeeds: Boolean = true, @@ -20,7 +20,7 @@ data class RestoreOptions( libraryEntries, categories, appSettings, - extensionRepoSettings, + extensionStores, sourceSettings, // SY --> savedSearchesFeeds, @@ -31,7 +31,7 @@ data class RestoreOptions( libraryEntries || categories || appSettings || - extensionRepoSettings || + extensionStores || sourceSettings /* SY --> */ || savedSearchesFeeds /* SY <-- */ @@ -53,9 +53,9 @@ data class RestoreOptions( setter = { options, enabled -> options.copy(appSettings = enabled) }, ), Entry( - label = MR.strings.extensionRepo_settings, - getter = RestoreOptions::extensionRepoSettings, - setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) }, + label = MR.strings.extensionStores, + getter = RestoreOptions::extensionStores, + setter = { options, enabled -> options.copy(extensionStores = enabled) }, ), Entry( label = MR.strings.source_settings, @@ -77,7 +77,7 @@ data class RestoreOptions( libraryEntries = array[0], categories = array[1], appSettings = array[2], - extensionRepoSettings = array[3], + extensionStores = array[3], sourceSettings = array[4], // SY --> savedSearchesFeeds = array[5], 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 deleted file mode 100644 index 006e48df1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt +++ /dev/null @@ -1,40 +0,0 @@ -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.DatabaseHandler -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -class ExtensionRepoRestorer( - private val handler: DatabaseHandler = 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 { - handler.await { - extension_reposQueries.insert( - backupRepo.baseUrl, - backupRepo.name, - backupRepo.shortName, - backupRepo.website, - backupRepo.signingKeyFingerprint, - ) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt new file mode 100644 index 000000000..70702546d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionStoreRestorer.kt @@ -0,0 +1,25 @@ +package eu.kanade.tachiyomi.data.backup.restore.restorers + +import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore +import tachiyomi.data.Database +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class ExtensionStoreRestorer( + private val database: Database = Injekt.get(), +) { + + suspend operator fun invoke( + backupStore: BackupExtensionStore, + ) { + database.extension_storeQueries.upsert( + indexUrl = backupStore.indexUrl, + name = backupStore.name, + badgeLabel = backupStore.badgeLabel ?: backupStore.name, + signingKey = backupStore.signingKey, + contactWebsite = backupStore.contactWebsite, + contactDiscord = backupStore.contactDiscord, + isLegacy = backupStore.isLegacy ?: true, + ) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt index ec289d2e7..1fa3c2929 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt @@ -88,7 +88,7 @@ class SyncManager( chapters = syncOptions.chapters, tracking = syncOptions.tracking, history = syncOptions.history, - extensionRepoSettings = syncOptions.extensionRepoSettings, + extensionStores = syncOptions.extensionStores, appSettings = syncOptions.appSettings, sourceSettings = syncOptions.sourceSettings, privateSettings = syncOptions.privateSettings, @@ -107,8 +107,8 @@ class SyncManager( backupCategories = backupCreator.backupCategories(backupOptions), backupSources = backupCreator.backupSources(backupManga), backupPreferences = backupCreator.backupAppPreferences(backupOptions), - backupExtensionRepo = backupCreator.backupExtensionRepos(backupOptions), backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions), + backupExtensionStores = backupCreator.backupExtensionStores(backupOptions), // SY --> backupSavedSearches = backupCreator.backupSavedSearches(backupOptions), @@ -192,7 +192,7 @@ class SyncManager( backupSources = remoteBackup.backupSources, backupPreferences = remoteBackup.backupPreferences, backupSourcePreferences = remoteBackup.backupSourcePreferences, - backupExtensionRepo = remoteBackup.backupExtensionRepo, + backupExtensionStores = remoteBackup.backupExtensionStores, // SY --> backupSavedSearches = remoteBackup.backupSavedSearches, @@ -222,7 +222,7 @@ class SyncManager( appSettings = true, sourceSettings = true, libraryEntries = true, - extensionRepoSettings = true, + extensionStores = true, ), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 477b61be1..1391c9263 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -273,14 +273,14 @@ class ExtensionManager( // Ext found: Update installed extensions with new information from repo // Also clear isObsolete and set new repo Name if needed val hasUpdate = extension.updateExists(availableExt) + // KMK --> installedExtensionsMap[pkgName] = extension.copy( hasUpdate = hasUpdate, - repoUrl = availableExt.repoUrl, - // KMK --> + store = availableExt.store, isObsolete = false, - repoName = extension.repoName ?: availableExt.repoName, - // KMK <-- + storeName = extension.storeName ?: availableExt.storeName, ) + // KMK <-- changed = true } } @@ -298,7 +298,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 1cefa8afd..5c7f0f18a 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 @@ -6,24 +6,13 @@ 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 exh.source.BlacklistedSources import exh.source.ExhPreferences -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.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -32,19 +21,16 @@ 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() // SY --> private val sourcePreferences: SourcePreferences by injectLazy() - // SY <-- - private val json: Json by injectLazy() - private val lastExtCheck: Preference by lazy { preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0) } @@ -54,37 +40,11 @@ internal class ExtensionApi { val disabledRepos = sourcePreferences.disabledRepos().get() // KMK <-- return withIOContext { - getExtensionRepo.getAll() + repository.fetchExtensions( // KMK --> - .filterNot { it.baseUrl in disabledRepos } + disabledRepos, // KMK <-- - .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, - // KMK --> - signature = extRepo.signingKeyFingerprint, - repoName = extRepo.shortName ?: extRepo.name, - // KMK <-- - ) - } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" } - emptyList() + ) } } @@ -99,8 +59,7 @@ internal class ExtensionApi { return null } - // Update extension repo details - updateExtensionRepo.awaitAll() + updateExtensionStores() val extensions = if (fromAvailableExtensionList) { extensionManager.availableExtensionsFlow.value @@ -138,47 +97,6 @@ internal class ExtensionApi { return extensionsWithUpdate } - private fun List.toExtensions( - repoUrl: String, - // KMK --> - signature: String, - repoName: String, - // KMK <-- - ): 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, - // KMK --> - signatureHash = signature, - repoName = repoName, - // KMK <-- - ) - } - } - - fun getApkUrl(extension: Extension.Available): String { - return "${extension.repoUrl}/apk/${extension.apkName}" - } - - private fun ExtensionJsonObject.extractLibVersion(): Double { - return version.substringBeforeLast('.').toDouble() - } - // SY --> private fun Extension.isBlacklisted( blacklistEnabled: Boolean = sourcePreferences.enableSourceBlacklist().get(), @@ -194,32 +112,3 @@ internal class ExtensionApi { } // SY <-- } - -@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/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 78866756d..b72b6c9c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -20,8 +20,8 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import logcat.LogPriority -import mihon.domain.extensionrepo.interactor.GetExtensionRepo -import mihon.domain.extensionrepo.model.ExtensionRepo +import mihon.domain.extension.interactor.GetExtensionStores +import mihon.domain.extension.model.ExtensionStore import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.injectLazy import java.io.File @@ -46,7 +46,7 @@ internal object ExtensionLoader { private val trustExtension: TrustExtension by injectLazy() // KMK --> - private val getExtensionRepo: GetExtensionRepo by injectLazy() + private val getExtensionStores: GetExtensionStores by injectLazy() // KMK <-- private val loadNsfwSource by lazy { @@ -169,7 +169,7 @@ internal object ExtensionLoader { // Load each extension concurrently and wait for completion return runBlocking { // KMK --> - val extRepos = getExtensionRepo.getAll() + val extStores = getExtensionStores.get() // KMK <-- val deferred = extPkgs.map { async { @@ -177,7 +177,7 @@ internal object ExtensionLoader { context, it, // KMK --> - extRepos, + extStores, // KMK <-- ) } @@ -228,7 +228,7 @@ internal object ExtensionLoader { isShared = true, ) } - } catch (error: PackageManager.NameNotFoundException) { + } catch (_: PackageManager.NameNotFoundException) { null } @@ -245,11 +245,11 @@ internal object ExtensionLoader { context: Context, extensionInfo: ExtensionInfo, // KMK --> - extRepos: List? = null, + extStores: List? = null, // KMK <-- ): LoadResult { // KMK --> - val repos = extRepos ?: getExtensionRepo.getAll() + val stores = extStores ?: getExtensionStores.get() // KMK <-- val pkgManager = context.packageManager val pkgInfo = extensionInfo.packageInfo @@ -288,10 +288,10 @@ internal object ExtensionLoader { libVersion, signatures.last(), // KMK --> - repoName = repos.firstOrNull { repo -> - signatures.all { it == repo.signingKeyFingerprint } - }?.let { repo -> - repo.shortName.takeIf { !it.isNullOrBlank() } ?: repo.name + storeName = stores.firstOrNull { store -> + signatures.all { it == store.signingKey } + }?.let { store -> + store.badgeLabel.takeIf(String::isNotBlank) ?: store.name }, // KMK <-- ) @@ -358,10 +358,10 @@ internal object ExtensionLoader { isShared = extensionInfo.isShared, // KMK --> signatureHash = signatures.last(), - repoName = repos.firstOrNull { repo -> - signatures.all { it == repo.signingKeyFingerprint } - }?.let { repo -> - repo.shortName.takeIf { !it.isNullOrBlank() } ?: repo.name + storeName = stores.firstOrNull { store -> + signatures.all { it == store.signingKey } + }?.let { store -> + store.badgeLabel.takeIf(String::isNotBlank) ?: store.name }, // KMK <-- ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt index d1669f879..659cb7cd3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreen.kt @@ -59,7 +59,7 @@ class ExtensionsScreen(private val searchSource: String? = null) : Screen() { /* * This will redo the searching for [searchSource] every times the screen is launched, for example when - * back from the [ExtensionFilterScreen] or from the [ExtensionReposScreen]. + * back from the [ExtensionFilterScreen] or from the [ExtensionStoresScreen]. * Not really desired but let's accept it. */ if (!searchSource.isNullOrBlank()) onChangeSearchQuery(searchSource) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt index ac9d8e920..1f4b1a244 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt @@ -20,7 +20,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.browse.ExtensionScreen import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.TabContent -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen import eu.kanade.tachiyomi.ui.webview.WebViewScreen @@ -62,8 +62,8 @@ fun extensionsTab( onClick = { navigator.push(ExtensionFilterScreen()) }, ), AppBar.OverflowAction( - title = stringResource(MR.strings.label_extension_repos), - onClick = { navigator.push(ExtensionReposScreen()) }, + title = stringResource(MR.strings.extensionStores), + onClick = { navigator.push(ExtensionStoresScreen()) }, ), ), content = { contentPadding, _ -> 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 0a0f721f0..43bcfc49d 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 @@ -66,7 +66,7 @@ import eu.kanade.presentation.components.UpdatingBannerBackgroundColor import eu.kanade.presentation.more.settings.screen.ConfigureExhDialog import eu.kanade.presentation.more.settings.screen.about.AboutScreen.Companion.getReleaseNotes import eu.kanade.presentation.more.settings.screen.about.WhatsNewDialog -import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen +import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.util.AssistContentScreen import eu.kanade.presentation.util.DefaultNavigatorScreenTransition @@ -693,11 +693,11 @@ class MainActivity : BaseActivity() { navigator.popUntilRoot() navigator.push(RestoreBackupScreen(intent.data.toString())) } - // Deep link to add extension repo - else if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") { + // Deep link to add extension store + else if (intent.isAddExtensionStoreIntent()) { intent.data?.getQueryParameter("url")?.let { repoUrl -> navigator.popUntilRoot() - navigator.push(ExtensionReposScreen(repoUrl)) + navigator.push(ExtensionStoresScreen(repoUrl)) } } null @@ -713,6 +713,11 @@ class MainActivity : BaseActivity() { return true } + private fun Intent.isAddExtensionStoreIntent(): Boolean { + return (scheme == "tachiyomi" && data?.host == "add-repo") || + (scheme == "mihon" && data?.host == "extension-store") + } + companion object { const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH" const val INTENT_SEARCH_QUERY = "query" diff --git a/app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt b/app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt new file mode 100644 index 000000000..210db5d4b --- /dev/null +++ b/app/src/main/java/mihon/core/migration/migrations/DisabledRepoMigration.kt @@ -0,0 +1,30 @@ +package mihon.core.migration.migrations + +import android.app.Application +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import eu.kanade.domain.source.service.SourcePreferences +import mihon.core.migration.Migration +import mihon.core.migration.MigrationContext +import tachiyomi.core.common.util.lang.withIOContext + +class DisabledRepoMigration : Migration { + override val version: Float = 80f + + override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext { + val context = migrationContext.get() ?: return@withIOContext false + val sourcePreferences = migrationContext.get() ?: return@withIOContext false + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + prefs.edit { + val disabledRepos = prefs.getStringSet(sourcePreferences.disabledRepos().key(), emptySet()) ?: return@edit + disabledRepos + .map { + it.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json" + }.toSet() + .let { + putStringSet(sourcePreferences.disabledRepos().key(), it) + } + } + return@withIOContext true + } +} diff --git a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt index c2829db19..0499e441b 100644 --- a/app/src/main/java/mihon/core/migration/migrations/Migrations.kt +++ b/app/src/main/java/mihon/core/migration/migrations/Migrations.kt @@ -52,4 +52,7 @@ val migrations: List TrustExtensionRepositoryMigration(), CategoryPreferencesCleanupMigration(), RemoveDuplicateReaderPreferenceMigration(), + // KMK --> + DisabledRepoMigration(), + // KMK <-- ) 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 b5dd80ff5..1ef23929a 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 552bae9b3..9d44c54bf 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -25,7 +25,7 @@ android { kotlin { compilerOptions { - freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi") + optIn.add("kotlinx.serialization.ExperimentalSerializationApi") } } @@ -34,5 +34,9 @@ dependencies { implementation(projects.domain) implementation(projects.core.common) + implementation(kotlinx.serialization.json) + implementation(kotlinx.serialization.json.okio) + implementation(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 000000000..fa04cfc75 --- /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 000000000..ce051ee2d --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt @@ -0,0 +1,114 @@ +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: String, + @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 = emptyList(), + @ProtoNumber(6) val contentRating: ContentRating = ContentRating.SAFE, + @ProtoNumber(7) val message: String? = null, + ) + + @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.toDouble(), + versionCode = extension.versionCode, + versionName = extension.versionName, + lang = if (lang.size == 1) lang.first() else "all", + isNsfw = extension.sources.maxOfOrNull { it.contentRating } == ContentRating.PORNOGRAPHIC, + // KMK --> + signatureHash = store.signingKey, + storeName = store.name, + // KMK <-- + 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 000000000..85d18b21a --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt @@ -0,0 +1,65 @@ +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, + // KMK --> + signatureHash = store.signingKey, + storeName = store.name, + // KMK <-- + 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 000000000..09a563c30 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt @@ -0,0 +1,33 @@ +package mihon.data.extension.model + +import android.annotation.SuppressLint +import kotlinx.serialization.Serializable +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 000000000..9be97b8f5 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt @@ -0,0 +1,138 @@ +package mihon.data.extension.repository + +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.DatabaseHandler + +class ExtensionStoreRepositoryImpl( + private val service: ExtensionStoreService, + private val handler: DatabaseHandler, +) : ExtensionStoreRepository { + override suspend fun insert(indexUrl: String): Result { + return service.fetch(indexUrl).mapCatching { upsert(it) } + } + + override suspend fun insertFromPreference(indexUrl: String, name: String) { + handler.await { + extension_storeQueries.upsert( + indexUrl = indexUrl, + name = name, + badgeLabel = name, + signingKey = "NO_SIGNING_KEY", + contactWebsite = indexUrl, + contactDiscord = null, + isLegacy = false, + ) + } + } + + override suspend fun refreshAll() { + try { + handler.awaitList { + extension_storeQueries.getAll() + }.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) { + handler.await { + 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( + // KMK --> + disabledRepos: Set, + // KMK <-- + ): List { + return try { + supervisorScope { + handler.awaitList { + extension_storeQueries.getAll(::extensionStoreMapper) + } + // KMK --> + .filterNot { it.indexUrl in disabledRepos } + // KMK <-- + .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 handler.awaitList { extension_storeQueries.getAll(::extensionStoreMapper) } + } + + override fun getAllAsFlow(): Flow> { + return handler.subscribeToList { extension_storeQueries.getAll(::extensionStoreMapper) } + } + + override fun getCountAsFlow(): Flow { + return handler.subscribeToOne { + extension_storeQueries + .getCount() + } + } + + override suspend fun remove(indexUrl: String) { + handler.await { 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 000000000..64696fee4 --- /dev/null +++ b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt @@ -0,0 +1,106 @@ +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 logcat.LogPriority +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 tachiyomi.core.common.util.system.logcat +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 { + var updatedIndexUrl: String = indexUrl + return try { + val store = network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source -> + try { + protoBuf.decodeFromByteArray(source.peek().readByteArray()) + // KMK --> + } catch (e: Exception) { + if (e is CancellationException) throw e + // KMK <-- + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + try { + json.decodeFromBufferedSource(source.peek()) + // KMK --> + } catch (e: Exception) { + if (e is CancellationException) throw e + // KMK <-- + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + try { + json.decodeFromBufferedSource(source.peek()) + } catch (e: IllegalArgumentException) { + if (!indexUrl.endsWith("/index.min.json")) { + throw e + } + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json") + network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().use { + json.decodeFromBufferedSource(it.body.source()) + } + } + } + } + .toExtensionStore(updatedIndexUrl) + } + Result.success(store) + } catch (e: CancellationException) { + throw e + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) { + "Failed to add extension store '$updatedIndexUrl'" + } + Result.failure(e) + } + } + + suspend fun getExtensions(store: ExtensionStore): Result> { + return try { + val extensions = if (!store.isLegacy) { + network.client.newCall(GET(store.indexUrl)).awaitSuccess().use { response -> + val source = response.body.source() + try { + protoBuf.decodeFromByteArray(source.peek().readByteArray()) + .toAvailableExtensions(store) + // KMK --> + } catch (e: Exception) { + if (e is CancellationException) throw e + // KMK <-- + json.decodeFromBufferedSource(source.peek()) + .toAvailableExtensions(store) + } + } + } else { + val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json") + network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess().use { response -> + val source = response.body.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 24910d968..000000000 --- a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt +++ /dev/null @@ -1,93 +0,0 @@ -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 getRepo(baseUrl: String): ExtensionRepo? { - return handler.awaitOneOrNull { extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) } - } - - override suspend fun getRepoBySigningKeyFingerprint(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 insertRepo( - 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 upsertRepo( - 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 replaceRepo(newRepo: ExtensionRepo) { - handler.await { - extension_reposQueries.replace( - newRepo.baseUrl, - newRepo.name, - newRepo.shortName, - newRepo.website, - newRepo.signingKeyFingerprint, - ) - } - } - - override suspend fun deleteRepo(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 deleted file mode 100644 index 6db69132a..000000000 --- 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 000000000..e6cd2c471 --- /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/46.sqm b/data/src/main/sqldelight/tachiyomi/migrations/46.sqm new file mode 100644 index 000000000..abe80fe3e --- /dev/null +++ b/data/src/main/sqldelight/tachiyomi/migrations/46.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, coalesce(short_name, 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 86% 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 3f63534ee..fbf3298d2 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 { @@ -16,7 +17,7 @@ sealed class Extension { // KMK --> abstract val signatureHash: String - abstract val repoName: String? + abstract val storeName: String? // KMK <-- data class Installed( @@ -29,8 +30,8 @@ sealed class Extension { override val isNsfw: Boolean, // KMK --> override val signatureHash: String, - /** Guessing repo name from built-in signatures preset */ - override val repoName: String? = null, + /** Guessing store name from built-in signatures preset */ + override val storeName: String? = null, // KMK <-- val pkgFactory: String?, val sources: List, @@ -38,7 +39,7 @@ sealed class Extension { val hasUpdate: Boolean = false, val isObsolete: Boolean = false, val isShared: Boolean, - val repoUrl: String? = null, + val store: ExtensionStore? = null, // SY --> val isRedundant: Boolean = false, // SY <-- @@ -54,12 +55,12 @@ sealed class Extension { override val isNsfw: Boolean, // KMK --> override val signatureHash: String, - override val repoName: String, + override val storeName: String, // KMK <-- val sources: List, - val apkName: String, + val apkUrl: String, val iconUrl: String, - val repoUrl: String, + val store: ExtensionStore, ) : Extension() { data class Source( @@ -87,7 +88,7 @@ sealed class Extension { /* KMK --> */ override /* KMK <-- */ val signatureHash: String, // KMK --> - override val repoName: String? = null, + override val storeName: String? = null, // KMK <-- override val lang: String? = null, override val isNsfw: Boolean = false, 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 000000000..7c10703e5 --- /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 000000000..2871e7f67 --- /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 000000000..e463af8d6 --- /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 000000000..42a8c07e2 --- /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 000000000..7bbd2812e --- /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 000000000..1ec6a37f0 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt @@ -0,0 +1,21 @@ +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?, + ) +} + +const val REPO_HELP = "https://komikku-app.github.io/docs/guides/getting-started#adding-sources" + +// cuong-tran's key +const val KOMIKKU_SIGNATURE = "cbec121aa82ebb02aaa73806992e0368a97d47b5451ed6524816d03084c45905" +const val REPO_SIGNATURE = "9add655a78e96c4ec7a53ef89dccb557cb5d767489fac5e785d671a5a75d4da2" 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 000000000..69a59db06 --- /dev/null +++ b/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt @@ -0,0 +1,27 @@ +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( + // KMK --> + disabledRepos: Set, + // KMK <-- + ): 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 4c6990be0..000000000 --- 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 befffb4dd..000000000 --- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt +++ /dev/null @@ -1,80 +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 - } - - companion object { - const val REPO_HELP = "https://komikku-app.github.io/docs/guides/getting-started#adding-sources" - - // cuong-tran's key - const val KOMIKKU_SIGNATURE = "cbec121aa82ebb02aaa73806992e0368a97d47b5451ed6524816d03084c45905" - const val REPO_SIGNATURE = "9add655a78e96c4ec7a53ef89dccb557cb5d767489fac5e785d671a5a75d4da2" - } -} 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 4b4b678e1..000000000 --- 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 25b919607..000000000 --- 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 a7c4e7c6d..000000000 --- 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 112ea701c..000000000 --- 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 a393e69d5..000000000 --- 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 ec9ccca87..000000000 --- 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 47be56dcf..000000000 --- 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 6a0a492de..000000000 --- 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 2d86b89d1..000000000 --- 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 a1466b4f4..aba5e9ac5 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -364,20 +364,19 @@ Revoke trusted unknown extensions - Extension repos - You have no repos set. - Add repo - Repo URL - Add additional repos to %s. This should be a URL that ends with \"index.min.json\". - This repo already exists! - Delete repo - Invalid repo URL - 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. + + + Extension stores + Add extension store + You haven\'t added an extension store yet + Add extension store + This extension store already exists + Processing… + Store index URL + Do you wish to add the extension store below? + Remove extension store + Do you wish to remove the extension store \"%1$s\" (%2$s)? Fullscreen @@ -567,7 +566,6 @@ What do you want to backup? App settings Source settings - Extension repos Include sensitive settings (e.g., tracker login tokens) All read entries Creating backup