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