diff --git a/CHANGELOG.md b/CHANGELOG.md
index 047e890ad..513b4fb42 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,12 @@ 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))
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d11579ed8..78fef36ab 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -60,8 +60,8 @@
-
-
+
+
@@ -71,6 +71,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt
index 6d3ddc35b..b69d60186 100644
--- a/app/src/main/java/eu/kanade/domain/DomainModule.kt
+++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt
@@ -25,16 +25,15 @@ import eu.kanade.domain.track.interactor.AddTracks
import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
import eu.kanade.domain.track.interactor.TrackChapter
-import mihon.data.repository.ExtensionRepoRepositoryImpl
+import mihon.data.extension.repository.ExtensionStoreRepositoryImpl
+import mihon.data.extension.service.ExtensionStoreService
import mihon.domain.chapter.interactor.FilterChaptersForDownload
-import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
-import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
-import mihon.domain.extensionrepo.interactor.GetExtensionRepo
-import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
-import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
-import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
-import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
-import mihon.domain.extensionrepo.service.ExtensionRepoService
+import mihon.domain.extension.interactor.AddExtensionStore
+import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
+import mihon.domain.extension.interactor.GetExtensionStores
+import mihon.domain.extension.interactor.RemoveExtensionStore
+import mihon.domain.extension.interactor.UpdateExtensionStores
+import mihon.domain.extension.repository.ExtensionStoreRepository
import mihon.domain.migration.usecases.MigrateMangaUseCase
import mihon.domain.upcoming.interactor.GetUpcomingManga
import tachiyomi.data.category.CategoryRepositoryImpl
@@ -195,14 +194,14 @@ class DomainModule : InjektModule {
addFactory { ToggleSourcePin(get()) }
addFactory { TrustExtension(get(), get()) }
- addSingletonFactory { ExtensionRepoRepositoryImpl(get()) }
- addFactory { ExtensionRepoService(get(), get()) }
- addFactory { GetExtensionRepo(get()) }
- addFactory { GetExtensionRepoCount(get()) }
- addFactory { CreateExtensionRepo(get(), get()) }
- addFactory { DeleteExtensionRepo(get()) }
- addFactory { ReplaceExtensionRepo(get()) }
- addFactory { UpdateExtensionRepo(get(), get()) }
+ addSingletonFactory { ExtensionStoreService(get(), get(), get()) }
+ addSingletonFactory { ExtensionStoreRepositoryImpl(get(), get()) }
+ addFactory { AddExtensionStore(get()) }
+ addFactory { GetExtensionStoreCountAsFlow(get()) }
+ addFactory { GetExtensionStores(get()) }
+ addFactory { RemoveExtensionStore(get()) }
+ addFactory { UpdateExtensionStores(get()) }
+
addFactory { ToggleIncognito(get()) }
addFactory { GetIncognitoState(get(), get(), get()) }
}
diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt
index c155af248..4e5c7c926 100644
--- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt
+++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionLanguages.kt
@@ -17,11 +17,7 @@ class GetExtensionLanguages(
) { enabledLanguage, availableExtensions ->
availableExtensions
.flatMap { ext ->
- if (ext.sources.isEmpty()) {
- listOf(ext.lang)
- } else {
- ext.sources.map { it.lang }
- }
+ ext.sources.map { it.lang }
}
.distinct()
.sortedWith(
diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt
index c2bfd49f8..ce689a7f4 100644
--- a/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt
+++ b/app/src/main/java/eu/kanade/domain/extension/interactor/GetExtensionsByType.kt
@@ -39,9 +39,6 @@ class GetExtensionsByType(
(showNsfwSources || !extension.isNsfw)
}
.flatMap { ext ->
- if (ext.sources.isEmpty()) {
- return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
- }
ext.sources.filter { it.lang in enabledLanguages }
.map {
ext.copy(
diff --git a/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt b/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt
index 40871be05..3b4351be5 100644
--- a/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt
+++ b/app/src/main/java/eu/kanade/domain/extension/interactor/TrustExtension.kt
@@ -3,16 +3,16 @@ package eu.kanade.domain.extension.interactor
import android.content.pm.PackageInfo
import androidx.core.content.pm.PackageInfoCompat
import eu.kanade.domain.source.service.SourcePreferences
-import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
+import mihon.domain.extension.repository.ExtensionStoreRepository
import tachiyomi.core.common.preference.getAndSet
class TrustExtension(
- private val extensionRepoRepository: ExtensionRepoRepository,
+ private val repository: ExtensionStoreRepository,
private val preferences: SourcePreferences,
) {
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List): Boolean {
- val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
+ val trustedFingerprints = repository.getAll().map { it.signingKey }.toHashSet()
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions.get()
}
diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt
index c2219c395..2848bde48 100644
--- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionDetailsScreen.kt
@@ -80,12 +80,12 @@ fun ExtensionDetailsScreen(
val uriHandler = LocalUriHandler.current
val url = remember(state.extension) {
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
- regex.find(state.extension?.repoUrl.orEmpty())
+ regex.find(state.extension?.store?.indexUrl.orEmpty())
?.let {
val (user, repo) = it.destructured
"https://github.com/$user/$repo"
}
- ?: state.extension?.repoUrl
+ ?: state.extension?.store?.indexUrl
}
Scaffold(
@@ -248,14 +248,17 @@ private fun DetailsHeader(
if (extension is Extension.Installed) {
append("\n\n")
- append(
+ appendLine(
"""
Update available: ${extension.hasUpdate}
Obsolete: ${extension.isObsolete}
Shared: ${extension.isShared}
- Repository: ${extension.repoUrl}
""".trimIndent(),
)
+ val store = extension.store
+ if (store != null) {
+ append("Repository: ${store.indexUrl}")
+ }
}
}
context.copyToClipboard("Extension Debug information", extDebugInfo)
diff --git a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt
index e46046786..04e72dfd4 100644
--- a/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt
@@ -47,7 +47,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
@@ -106,9 +106,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()) },
),
),
)
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 25e2b384a..6211761cc 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
@@ -11,10 +11,10 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.source.service.SourcePreferences
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.util.system.AuthenticatorUtil.authenticate
import kotlinx.collections.immutable.persistentListOf
-import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
+import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.pluralStringResource
@@ -34,9 +34,9 @@ object SettingsBrowseScreen : SearchableSettings {
val navigator = LocalNavigator.currentOrThrow
val sourcePreferences = remember { Injekt.get() }
- val getExtensionRepoCount = remember { Injekt.get() }
+ val getExtensionStoreCountAsFlow = remember { Injekt.get() }
- val reposCount by getExtensionRepoCount.subscribe().collectAsState(0)
+ val reposCount by getExtensionStoreCountAsFlow().collectAsState(0)
return listOf(
Preference.PreferenceGroup(
@@ -47,10 +47,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/ExtensionReposScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt
deleted file mode 100644
index 38f4e72c8..000000000
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreen.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-package eu.kanade.presentation.more.settings.screen.browse
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-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.util.Screen
-import eu.kanade.tachiyomi.util.system.openInBrowser
-import eu.kanade.tachiyomi.util.system.toast
-import kotlinx.collections.immutable.toImmutableSet
-import kotlinx.coroutines.flow.collectLatest
-import tachiyomi.presentation.core.screens.LoadingScreen
-
-class ExtensionReposScreen(
- private val url: String? = null,
-) : Screen() {
-
- @Composable
- override fun Content() {
- val context = LocalContext.current
- val navigator = LocalNavigator.currentOrThrow
-
- val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
- val state by screenModel.state.collectAsState()
-
- LaunchedEffect(url) {
- url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) }
- }
-
- if (state is RepoScreenState.Loading) {
- LoadingScreen()
- return
- }
-
- val successState = state as RepoScreenState.Success
-
- ExtensionReposScreen(
- state = successState,
- onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
- onOpenWebsite = { context.openInBrowser(it.website) },
- onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
- onClickRefresh = { screenModel.refreshRepos() },
- navigateUp = navigator::pop,
- )
-
- when (val dialog = successState.dialog) {
- null -> {}
- is RepoDialog.Create -> {
- ExtensionRepoCreateDialog(
- onDismissRequest = screenModel::dismissDialog,
- onCreate = { screenModel.createRepo(it) },
- repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(),
- )
- }
- is RepoDialog.Delete -> {
- ExtensionRepoDeleteDialog(
- onDismissRequest = screenModel::dismissDialog,
- onDelete = { screenModel.deleteRepo(dialog.repo) },
- repo = dialog.repo,
- )
- }
- is RepoDialog.Conflict -> {
- ExtensionRepoConflictDialog(
- onDismissRequest = screenModel::dismissDialog,
- onMigrate = { screenModel.replaceRepo(dialog.newRepo) },
- oldRepo = dialog.oldRepo,
- newRepo = dialog.newRepo,
- )
- }
- is RepoDialog.Confirm -> {
- ExtensionRepoConfirmDialog(
- onDismissRequest = screenModel::dismissDialog,
- onCreate = { screenModel.createRepo(dialog.url) },
- repo = dialog.url,
- )
- }
- }
-
- 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/ExtensionReposScreenModel.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt
deleted file mode 100644
index 3c5212473..000000000
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionReposScreenModel.kt
+++ /dev/null
@@ -1,150 +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.tachiyomi.extension.ExtensionManager
-import kotlinx.collections.immutable.ImmutableSet
-import kotlinx.collections.immutable.toImmutableSet
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.receiveAsFlow
-import kotlinx.coroutines.flow.update
-import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
-import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
-import mihon.domain.extensionrepo.interactor.GetExtensionRepo
-import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
-import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
-import mihon.domain.extensionrepo.model.ExtensionRepo
-import 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(),
-) : 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(),
- )
- }
- }
- }
- }
-
- /**
- * 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) {
- screenModelScope.launchIO {
- deleteExtensionRepo.await(baseUrl)
- extensionManager.findAvailableExtensions()
- }
- }
-
- 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,
- ) : RepoScreenState() {
-
- val isEmpty: Boolean
- get() = repos.isEmpty()
- }
-}
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt
new file mode 100644
index 000000000..cf065270d
--- /dev/null
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreen.kt
@@ -0,0 +1,85 @@
+package eu.kanade.presentation.more.settings.screen.browse
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+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.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 tachiyomi.presentation.core.screens.LoadingScreen
+
+class ExtensionStoresScreen(
+ private val url: String? = null,
+) : Screen() {
+
+ @Composable
+ override fun Content() {
+ val context = LocalContext.current
+ val navigator = LocalNavigator.currentOrThrow
+
+ val screenModel = rememberScreenModel { ExtensionStoresScreenModel() }
+ val state by screenModel.state.collectAsState()
+
+ LaunchedEffect(url) {
+ url?.let { screenModel.addFromDeeplink(url) }
+ }
+
+ if (state is ExtensionStoreScreenState.Loading) {
+ LoadingScreen()
+ return
+ }
+
+ val successState = state as ExtensionStoreScreenState.Success
+
+ ExtensionStoresScreen(
+ state = successState,
+ 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) },
+ onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) },
+ onClickRefresh = { screenModel.refreshRepos() },
+ navigateUp = navigator::pop,
+ )
+
+ when (val dialog = successState.dialog) {
+ null -> {}
+ is ExtensionStoreDialog.Create -> {
+ ExtensionStoreCreateDialog(
+ onDismissRequest = screenModel::dismissDialog,
+ onCreate = { screenModel.createRepo(it) },
+ storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(),
+ processing = dialog.processing,
+ errorMessage = dialog.errorMessage,
+ )
+ }
+ is ExtensionStoreDialog.Delete -> {
+ ExtensionStoreDeleteDialog(
+ onDismissRequest = screenModel::dismissDialog,
+ onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) },
+ storeName = dialog.store.name,
+ storeIndexUrl = dialog.store.indexUrl,
+ )
+ }
+ is ExtensionStoreDialog.Confirm -> {
+ ExtensionStoreConfirmDialog(
+ onDismissRequest = screenModel::dismissDialog,
+ onCreate = { screenModel.createRepo(dialog.url) },
+ storeIndexUrl = dialog.url,
+ storeAlreadyExists = dialog.alreadyExists,
+ processing = dialog.processing,
+ errorMessage = dialog.errorMessage,
+ )
+ }
+ }
+ }
+}
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..357e20425
--- /dev/null
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/browse/ExtensionStoresScreenModel.kt
@@ -0,0 +1,165 @@
+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.tachiyomi.extension.ExtensionManager
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import mihon.domain.extension.interactor.AddExtensionStore
+import mihon.domain.extension.interactor.GetExtensionStores
+import mihon.domain.extension.interactor.RemoveExtensionStore
+import mihon.domain.extension.interactor.UpdateExtensionStores
+import mihon.domain.extension.model.ExtensionStore
+import tachiyomi.core.common.util.lang.launchIO
+import 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(),
+) : 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)
+ is ExtensionStoreScreenState.Success -> it.copy(stores = stores)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates and adds a new repo to the database.
+ *
+ * @param baseUrl The baseUrl of the repo to create.
+ */
+ fun createRepo(baseUrl: String) {
+ screenModelScope.launch {
+ 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(baseUrl)
+ .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(baseUrl: String) {
+ screenModelScope.launchIO {
+ removeExtensionStore(baseUrl)
+ extensionManager.findAvailableExtensions()
+ }
+ }
+
+ 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,
+ ) : 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 72%
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 e6b2a227f..3d7c0a1b9 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
@@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Label
-import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -20,21 +20,22 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import eu.kanade.tachiyomi.util.system.copyToClipboard
-import kotlinx.collections.immutable.ImmutableSet
-import mihon.domain.extensionrepo.model.ExtensionRepo
+import mihon.domain.extension.model.ExtensionStore
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
+import tachiyomi.presentation.core.icons.CustomIcons
+import tachiyomi.presentation.core.icons.Discord
@Composable
-fun ExtensionReposContent(
- repos: ImmutableSet,
+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,
modifier: Modifier = Modifier,
) {
LazyColumn(
@@ -45,11 +46,13 @@ 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) },
)
}
}
@@ -57,14 +60,14 @@ fun ExtensionReposContent(
}
@Composable
-private fun ExtensionRepoListItem(
- repo: ExtensionRepo,
+private fun ExtensionStoresListItem(
+ store: ExtensionStore,
onOpenWebsite: () -> Unit,
+ onOpenDiscord: () -> Unit,
+ onCopy: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier,
) {
- val context = LocalContext.current
-
ElevatedCard(
modifier = modifier,
) {
@@ -80,7 +83,7 @@ private fun ExtensionRepoListItem(
) {
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
Text(
- text = repo.name,
+ text = store.name,
modifier = Modifier.padding(start = MaterialTheme.padding.medium),
style = MaterialTheme.typography.titleMedium,
)
@@ -92,17 +95,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),
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 78%
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 b07ba4101..e6595782a 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
@@ -14,8 +12,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
import eu.kanade.presentation.components.AppBar
-import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
-import mihon.domain.extensionrepo.model.ExtensionRepo
+import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoreScreenState
+import mihon.domain.extension.model.ExtensionStore
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
@@ -25,11 +23,13 @@ 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,
onClickRefresh: () -> Unit,
navigateUp: () -> Unit,
) {
@@ -38,7 +38,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) {
@@ -59,18 +59,20 @@ fun ExtensionReposScreen(
) { paddingValues ->
if (state.isEmpty) {
EmptyScreen(
- MR.strings.information_empty_repos,
+ MR.strings.extensionStoresScreen_emptyLabel,
modifier = Modifier.padding(paddingValues),
)
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,
)
}
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 0c1856b45..3f6a0d9ce 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,13 +6,13 @@ 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.MangaBackupCreator
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
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.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSource
@@ -49,7 +49,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(),
) {
@@ -85,7 +85,7 @@ class BackupCreator(
backupCategories = backupCategories(options),
backupSources = backupSources(backupManga),
backupPreferences = backupAppPreferences(options),
- backupExtensionRepo = backupExtensionRepos(options),
+ backupExtensionStores = backupExtensionStores(options),
backupSourcePreferences = backupSourcePreferences(options),
)
@@ -141,10 +141,10 @@ class BackupCreator(
return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings)
}
- private suspend fun backupExtensionRepos(options: BackupOptions): List {
- if (!options.extensionRepoSettings) return emptyList()
+ private suspend fun backupExtensionStores(options: BackupOptions): List {
+ if (!options.extensionStores) return emptyList()
- return extensionRepoBackupCreator()
+ return extensionStoresBackupCreator()
}
private 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 efb20f6bf..27397c6e1 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
@@ -12,7 +12,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,
) {
@@ -25,12 +25,12 @@ data class BackupOptions(
history,
readEntries,
appSettings,
- extensionRepoSettings,
+ extensionStores,
sourceSettings,
privateSettings,
)
- fun canCreate() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings
+ fun canCreate() = libraryEntries || categories || appSettings || extensionStores || sourceSettings
companion object {
val libraryOptions = persistentListOf(
@@ -77,9 +77,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,
@@ -102,7 +102,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],
)
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 e7f35ddf2..0c021d34a 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,5 +11,5 @@ 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(),
)
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 8f83247fc..76b1cc069 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,12 +5,12 @@ 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.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
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.MangaRestorer
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
@@ -32,7 +32,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(),
) {
@@ -79,8 +79,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
@@ -99,8 +99,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
@@ -172,22 +172,22 @@ 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}")
}
restoreProgress += 1
notifier.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 c2c88c16b..4bdd44916 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
@@ -8,7 +8,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,
) {
@@ -16,11 +16,11 @@ data class RestoreOptions(
libraryEntries,
categories,
appSettings,
- extensionRepoSettings,
+ extensionStores,
sourceSettings,
)
- fun canRestore() = libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings
+ fun canRestore() = libraryEntries || categories || appSettings || extensionStores || sourceSettings
companion object {
val options = persistentListOf(
@@ -40,9 +40,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,
@@ -55,7 +55,7 @@ data class RestoreOptions(
libraryEntries = array[0],
categories = array[1],
appSettings = array[2],
- extensionRepoSettings = array[3],
+ extensionStores = array[3],
sourceSettings = array[4],
)
}
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 7dbc0631f..000000000
--- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/ExtensionRepoRestorer.kt
+++ /dev/null
@@ -1,38 +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.Database
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-
-class ExtensionRepoRestorer(
- private val database: Database = Injekt.get(),
- private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
-) {
-
- suspend operator fun invoke(
- backupRepo: BackupExtensionRepos,
- ) {
- val dbRepos = getExtensionRepos.getAll()
- val existingReposBySHA = dbRepos.associateBy { it.signingKeyFingerprint }
- val existingReposByUrl = dbRepos.associateBy { it.baseUrl }
-
- val urlExists = existingReposByUrl[backupRepo.baseUrl]
- val shaExists = existingReposBySHA[backupRepo.signingKeyFingerprint]
-
- if (urlExists != null && urlExists.signingKeyFingerprint != backupRepo.signingKeyFingerprint) {
- error("Already Exists with different signing key fingerprint")
- } else if (shaExists != null) {
- error("${shaExists.name} has the same signing key fingerprint")
- } else {
- database.extension_reposQueries.insert(
- backupRepo.baseUrl,
- backupRepo.name,
- backupRepo.shortName,
- backupRepo.website,
- backupRepo.signingKeyFingerprint,
- )
- }
- }
-}
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/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
index a63105623..4f9856ccd 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
@@ -204,11 +204,11 @@ class ExtensionManager(
if (extension.hasUpdate != hasUpdate) {
installedExtensionsMap[pkgName] = extension.copy(
hasUpdate = hasUpdate,
- repoUrl = availableExt.repoUrl,
+ store = availableExt.store,
)
} else {
installedExtensionsMap[pkgName] = extension.copy(
- repoUrl = availableExt.repoUrl,
+ store = availableExt.store,
)
}
changed = true
@@ -228,7 +228,7 @@ class ExtensionManager(
* @param extension The extension to be installed.
*/
fun installExtension(extension: Extension.Available): Flow {
- return installer.downloadAndInstall(api.getApkUrl(extension), extension)
+ return installer.downloadAndInstall(extension.apkUrl, extension)
}
/**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt
index 71e6251db..51face7b6 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionApi.kt
@@ -5,64 +5,29 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
-import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.network.NetworkHelper
-import eu.kanade.tachiyomi.network.awaitSuccess
-import eu.kanade.tachiyomi.network.parseAs
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.Json
-import logcat.LogPriority
-import mihon.domain.extensionrepo.interactor.GetExtensionRepo
-import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
-import mihon.domain.extensionrepo.model.ExtensionRepo
+import mihon.domain.extension.interactor.UpdateExtensionStores
+import mihon.domain.extension.repository.ExtensionStoreRepository
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.core.common.util.lang.withIOContext
-import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.injectLazy
import java.time.Instant
import kotlin.time.Duration.Companion.days
internal class ExtensionApi {
- private val networkService: NetworkHelper by injectLazy()
+ private val repository: ExtensionStoreRepository by injectLazy()
+
private val preferenceStore: PreferenceStore by injectLazy()
- private val getExtensionRepo: GetExtensionRepo by injectLazy()
- private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
+ private val updateExtensionStores: UpdateExtensionStores by injectLazy()
private val extensionManager: ExtensionManager by injectLazy()
- private val json: Json by injectLazy()
private val lastExtCheck: Preference by lazy {
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
}
suspend fun findExtensions(): List {
- return withIOContext {
- getExtensionRepo.getAll()
- .map { async { getExtensions(it) } }
- .awaitAll()
- .flatten()
- }
- }
-
- private suspend fun getExtensions(extRepo: ExtensionRepo): List {
- val repoBaseUrl = extRepo.baseUrl
- return try {
- val response = networkService.client
- .newCall(GET("$repoBaseUrl/index.min.json"))
- .awaitSuccess()
-
- with(json) {
- response
- .parseAs>()
- .toExtensions(repoBaseUrl)
- }
- } catch (e: Throwable) {
- logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" }
- emptyList()
- }
+ return withIOContext { repository.fetchExtensions() }
}
suspend fun checkForUpdates(
@@ -76,8 +41,7 @@ internal class ExtensionApi {
return null
}
- // Update extension repo details
- updateExtensionRepo.awaitAll()
+ updateExtensionStores()
val extensions = if (fromAvailableExtensionList) {
extensionManager.availableExtensionsFlow.value
@@ -107,64 +71,4 @@ internal class ExtensionApi {
return extensionsWithUpdate
}
-
- private fun List.toExtensions(repoUrl: String): List {
- return this
- .filter {
- val libVersion = it.extractLibVersion()
- libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
- }
- .map {
- Extension.Available(
- name = it.name.substringAfter("Tachiyomi: "),
- pkgName = it.pkg,
- versionName = it.version,
- versionCode = it.code,
- libVersion = it.extractLibVersion(),
- lang = it.lang,
- isNsfw = it.nsfw == 1,
- sources = it.sources?.map(extensionSourceMapper).orEmpty(),
- apkName = it.apk,
- iconUrl = "$repoUrl/icon/${it.pkg}.png",
- repoUrl = repoUrl,
- )
- }
- }
-
- fun getApkUrl(extension: Extension.Available): String {
- return "${extension.repoUrl}/apk/${extension.apkName}"
- }
-
- private fun ExtensionJsonObject.extractLibVersion(): Double {
- return version.substringBeforeLast('.').toDouble()
- }
-}
-
-@Serializable
-private data class ExtensionJsonObject(
- val name: String,
- val pkg: String,
- val apk: String,
- val lang: String,
- val code: Long,
- val version: String,
- val nsfw: Int,
- val sources: List?,
-)
-
-@Serializable
-private data class ExtensionSourceJsonObject(
- val id: Long,
- val lang: String,
- val name: String,
- val baseUrl: String,
-)
-
-private val extensionSourceMapper: (ExtensionSourceJsonObject) -> Extension.Available.Source = {
- Extension.Available.Source(
- id = it.id,
- lang = it.lang,
- name = it.name,
- baseUrl = it.baseUrl,
- )
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt
index 30561baed..b92965f7a 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
@@ -16,7 +16,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
@@ -45,8 +45,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 e80e5c947..9355a4035 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
@@ -74,7 +74,7 @@ import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
import eu.kanade.presentation.components.IndexingBannerBackgroundColor
-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
@@ -562,11 +562,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
@@ -582,6 +582,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/TrustExtensionRepositoryMigration.kt b/app/src/main/java/mihon/core/migration/migrations/TrustExtensionRepositoryMigration.kt
index 35b800a4b..e4335d595 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 002c1a0b3..88afea425 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -23,7 +23,7 @@ android {
kotlin {
compilerOptions {
- freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi")
+ optIn.add("kotlinx.serialization.ExperimentalSerializationApi")
}
}
@@ -32,5 +32,9 @@ dependencies {
implementation(projects.domain)
implementation(projects.core.common)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.kotlinx.serialization.jsonOkio)
+ implementation(libs.kotlinx.serialization.protobuf)
+
api(libs.bundles.sqldelight)
}
diff --git a/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt b/data/src/main/java/mihon/data/extension/model/BaseNetworkExtensionStore.kt
new file mode 100644
index 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..d15e00977
--- /dev/null
+++ b/data/src/main/java/mihon/data/extension/model/NetworkExtensionStore.kt
@@ -0,0 +1,110 @@
+package mihon.data.extension.model
+
+import android.annotation.SuppressLint
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonNames
+import kotlinx.serialization.protobuf.ProtoNumber
+import mihon.domain.extension.model.ExtensionStore
+
+@SuppressLint("UnsafeOptInUsageError")
+@Serializable
+data class NetworkExtensionStore(
+ @ProtoNumber(1) val name: String,
+ @ProtoNumber(2) val badgeLabel: String,
+ @ProtoNumber(3) val signingKey: String,
+ @ProtoNumber(4) val contact: Contact,
+ @ProtoNumber(5) val extensions: List,
+) : BaseNetworkExtensionStore {
+ @Serializable
+ data class Contact(
+ @ProtoNumber(1) val website: String,
+ @ProtoNumber(2) val discord: String?,
+ )
+
+ @Serializable
+ data class Extension(
+ @ProtoNumber(1) val name: String,
+ @ProtoNumber(2) val packageName: String,
+ @ProtoNumber(3) val resources: Resources,
+ @ProtoNumber(4) val extensionLib: 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,
+ 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..9a47907ce
--- /dev/null
+++ b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtension.kt
@@ -0,0 +1,61 @@
+package mihon.data.extension.model
+
+import android.annotation.SuppressLint
+import eu.kanade.tachiyomi.extension.model.Extension
+import kotlinx.serialization.Serializable
+import mihon.domain.extension.model.ExtensionStore
+
+@SuppressLint("UnsafeOptInUsageError")
+@Serializable
+data class NetworkLegacyExtension(
+ val name: String,
+ val pkg: String,
+ val apk: String,
+ val lang: String,
+ val code: Long,
+ val version: String,
+ val nsfw: Int,
+ val sources: List?,
+) {
+ @Serializable
+ data class Source(
+ val id: Long,
+ val lang: String,
+ val name: String,
+ val baseUrl: String,
+ )
+
+ fun toAvailableExtension(store: ExtensionStore, storeBaseUrl: String): Extension.Available {
+ return Extension.Available(
+ name = name.substringAfter("Tachiyomi: "),
+ pkgName = pkg,
+ apkUrl = "$storeBaseUrl/apk/$apk",
+ iconUrl = "$storeBaseUrl/icon/$pkg.png",
+ libVersion = version.substringBeforeLast('.').toDouble(),
+ versionCode = code,
+ versionName = version,
+ lang = lang,
+ isNsfw = nsfw == 1,
+ sources = if (sources.isNullOrEmpty()) {
+ listOf(
+ Extension.Available.Source(
+ id = 0,
+ name = name,
+ lang = lang,
+ baseUrl = "",
+ ),
+ )
+ } else {
+ sources.map { source ->
+ Extension.Available.Source(
+ id = source.id,
+ name = source.name,
+ lang = source.lang,
+ baseUrl = source.baseUrl,
+ )
+ }
+ },
+ store = store,
+ )
+ }
+}
diff --git a/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt b/data/src/main/java/mihon/data/extension/model/NetworkLegacyExtensionRepo.kt
new file mode 100644
index 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..140f353aa
--- /dev/null
+++ b/data/src/main/java/mihon/data/extension/repository/ExtensionStoreRepositoryImpl.kt
@@ -0,0 +1,124 @@
+package mihon.data.extension.repository
+
+import app.cash.sqldelight.async.coroutines.awaitAsList
+import eu.kanade.tachiyomi.extension.model.Extension
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.supervisorScope
+import logcat.LogPriority
+import mihon.data.extension.service.ExtensionStoreService
+import mihon.domain.extension.model.ExtensionStore
+import mihon.domain.extension.repository.ExtensionStoreRepository
+import tachiyomi.core.common.util.system.logcat
+import tachiyomi.data.Database
+import tachiyomi.data.subscribeToList
+import tachiyomi.data.subscribeToOne
+
+class ExtensionStoreRepositoryImpl(
+ private val service: ExtensionStoreService,
+ private val database: Database,
+) : ExtensionStoreRepository {
+ override suspend fun insert(indexUrl: String): Result {
+ return service.fetch(indexUrl).mapCatching { upsert(it) }
+ }
+
+ override suspend fun insertFromPreference(indexUrl: String, name: String) {
+ database.extension_storeQueries.upsert(
+ indexUrl = indexUrl,
+ name = name,
+ badgeLabel = name,
+ signingKey = "NO_SIGNING_KEY",
+ contactWebsite = indexUrl,
+ contactDiscord = null,
+ isLegacy = false,
+ )
+ }
+
+ override suspend fun refreshAll() {
+ try {
+ database.extension_storeQueries.getAll().awaitAsList().forEach { store ->
+ service.fetch(store.index_url)
+ .mapCatching { upsert(it) }
+ .onFailure {
+ logcat(LogPriority.ERROR, it) {
+ "Failed to refresh extension store '${store.name} (${store.index_url})'"
+ }
+ }
+ }
+ } catch (e: Exception) {
+ logcat(LogPriority.ERROR, e)
+ }
+ }
+
+ private suspend fun upsert(store: ExtensionStore) {
+ database.extension_storeQueries.upsert(
+ indexUrl = store.indexUrl,
+ name = store.name,
+ badgeLabel = store.badgeLabel,
+ signingKey = store.signingKey,
+ contactWebsite = store.contact.website,
+ contactDiscord = store.contact.discord,
+ isLegacy = store.isLegacy,
+ )
+ }
+
+ override suspend fun fetchExtensions(): List {
+ return try {
+ supervisorScope {
+ database.extension_storeQueries.getAll(::extensionStoreMapper).awaitAsList().map { store ->
+ async {
+ service.getExtensions(store).onFailure {
+ this@ExtensionStoreRepositoryImpl.logcat(LogPriority.ERROR, it) {
+ "Failed to fetch extensions for store '${store.name} (${store.indexUrl})'"
+ }
+ }
+ }
+ }
+ .awaitAll()
+ .flatMap { it.getOrDefault(emptyList()) }
+ }
+ } catch (e: Exception) {
+ logcat(LogPriority.ERROR, e)
+ emptyList()
+ }
+ }
+
+ override suspend fun getAll(): List {
+ return database.extension_storeQueries.getAll(::extensionStoreMapper).awaitAsList()
+ }
+
+ override fun getAllAsFlow(): Flow> {
+ return database.extension_storeQueries.getAll(::extensionStoreMapper).subscribeToList()
+ }
+
+ override fun getCountAsFlow(): Flow {
+ return database.extension_storeQueries
+ .getCount()
+ .subscribeToOne()
+ }
+
+ override suspend fun remove(indexUrl: String) {
+ database.extension_storeQueries.delete(indexUrl)
+ }
+
+ private fun extensionStoreMapper(
+ indexUrl: String,
+ name: String,
+ badgeLabel: String,
+ signingKey: String,
+ contactWebsite: String,
+ contactDiscord: String?,
+ isLegacy: Boolean,
+ ): ExtensionStore = ExtensionStore(
+ indexUrl = indexUrl,
+ name = name,
+ badgeLabel = badgeLabel,
+ signingKey = signingKey,
+ contact = ExtensionStore.Contact(
+ website = contactWebsite,
+ discord = contactDiscord,
+ ),
+ isLegacy = isLegacy,
+ )
+}
diff --git a/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt
new file mode 100644
index 000000000..6f55c9311
--- /dev/null
+++ b/data/src/main/java/mihon/data/extension/service/ExtensionStoreService.kt
@@ -0,0 +1,97 @@
+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())
+ } catch (e: IllegalArgumentException) {
+ logcat(LogPriority.ERROR, e) {
+ "Failed to add extension store '$updatedIndexUrl'"
+ }
+ try {
+ json.decodeFromBufferedSource(source.peek())
+ } catch (e: IllegalArgumentException) {
+ 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().body.source().use {
+ json.decodeFromBufferedSource(it)
+ }
+ }
+ }
+ }
+ .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) {
+ val response = network.client.newCall(GET(store.indexUrl)).awaitSuccess()
+ response.body.source().use { source ->
+ try {
+ protoBuf.decodeFromByteArray(source.peek().readByteArray())
+ .toAvailableExtensions(store)
+ } catch (_: IllegalArgumentException) {
+ json.decodeFromBufferedSource(source.peek())
+ .toAvailableExtensions(store)
+ }
+ }
+ } else {
+ val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
+ val response = network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess()
+ response.body.source().use { source ->
+ json.decodeFromBufferedSource>(source)
+ .map { it.toAvailableExtension(store, storeBaseUrl) }
+ }
+ }
+ Result.success(extensions)
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Exception) {
+ Result.failure(e)
+ }
+ }
+}
diff --git a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt b/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt
deleted file mode 100644
index e60f0815e..000000000
--- a/data/src/main/java/mihon/data/repository/ExtensionRepoRepositoryImpl.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package mihon.data.repository
-
-import android.database.SQLException
-import app.cash.sqldelight.async.coroutines.awaitAsList
-import app.cash.sqldelight.async.coroutines.awaitAsOneOrNull
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
-import mihon.domain.extensionrepo.model.ExtensionRepo
-import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
-import tachiyomi.data.Database
-import tachiyomi.data.subscribeToList
-import tachiyomi.data.subscribeToOne
-
-class ExtensionRepoRepositoryImpl(
- private val database: Database,
-) : ExtensionRepoRepository {
- override fun subscribeAll(): Flow> {
- return database.extension_reposQueries
- .findAll(::mapExtensionRepo)
- .subscribeToList()
- }
-
- override suspend fun getAll(): List {
- return database.extension_reposQueries
- .findAll(::mapExtensionRepo)
- .awaitAsList()
- }
-
- override suspend fun getRepo(baseUrl: String): ExtensionRepo? {
- return database.extension_reposQueries
- .findOne(baseUrl, ::mapExtensionRepo)
- .awaitAsOneOrNull()
- }
-
- override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? {
- return database.extension_reposQueries
- .findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo)
- .awaitAsOneOrNull()
- }
-
- override fun getCount(): Flow {
- return database.extension_reposQueries
- .count()
- .subscribeToOne()
- .map { it.toInt() }
- }
-
- override suspend fun insertRepo(
- baseUrl: String,
- name: String,
- shortName: String?,
- website: String,
- signingKeyFingerprint: String,
- ) {
- try {
- database.extension_reposQueries.insert(
- baseUrl,
- name,
- shortName,
- website,
- signingKeyFingerprint,
- )
- } catch (ex: SQLException) {
- throw SaveExtensionRepoException(ex)
- }
- }
-
- override suspend fun upsertRepo(
- baseUrl: String,
- name: String,
- shortName: String?,
- website: String,
- signingKeyFingerprint: String,
- ) {
- try {
- database.extension_reposQueries.upsert(
- baseUrl,
- name,
- shortName,
- website,
- signingKeyFingerprint,
- )
- } catch (ex: SQLException) {
- throw SaveExtensionRepoException(ex)
- }
- }
-
- override suspend fun replaceRepo(newRepo: ExtensionRepo) {
- database.extension_reposQueries.replace(
- newRepo.baseUrl,
- newRepo.name,
- newRepo.shortName,
- newRepo.website,
- newRepo.signingKeyFingerprint,
- )
- }
-
- override suspend fun deleteRepo(baseUrl: String) {
- database.extension_reposQueries.delete(baseUrl)
- }
-
- private fun mapExtensionRepo(
- baseUrl: String,
- name: String,
- shortName: String?,
- website: String,
- signingKeyFingerprint: String,
- ): ExtensionRepo = ExtensionRepo(
- baseUrl = baseUrl,
- name = name,
- shortName = shortName,
- website = website,
- signingKeyFingerprint = signingKeyFingerprint,
- )
-}
diff --git a/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq b/data/src/main/sqldelight/tachiyomi/data/extension_repos.sq
deleted file mode 100644
index 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..82795ddc7
--- /dev/null
+++ b/data/src/main/sqldelight/tachiyomi/data/extension_store.sq
@@ -0,0 +1,42 @@
+import kotlin.Boolean;
+
+CREATE TABLE extension_store(
+ index_url TEXT NOT NULL PRIMARY KEY,
+ name TEXT NOT NULL,
+ badge_label TEXT NOT NULL,
+ signing_key TEXT NOT NULL,
+ contact_website TEXT NOT NULL,
+ contact_discord TEXT,
+ is_legacy INTEGER AS Boolean NOT NULL
+);
+
+get:
+SELECT *
+FROM extension_store
+WHERE index_url = :indexUrl;
+
+getAll:
+SELECT *
+FROM extension_store;
+
+getCount:
+SELECT COUNT(*)
+FROM extension_store;
+
+upsert:
+INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy)
+VALUES (:indexUrl, :name, :badgeLabel, :signingKey, :contactWebsite, :contactDiscord, :isLegacy)
+ON CONFLICT(index_url)
+DO UPDATE
+SET
+ name = :name,
+ badge_label = :badgeLabel,
+ signing_key =: signingKey,
+ contact_website = :contactWebsite,
+ contact_discord = :contactDiscord,
+ is_legacy = :isLegacy
+WHERE index_url = :indexUrl;
+
+delete:
+DELETE FROM extension_store
+WHERE index_url = :indexUrl;
diff --git a/data/src/main/sqldelight/tachiyomi/migrations/11.sqm b/data/src/main/sqldelight/tachiyomi/migrations/11.sqm
new file mode 100644
index 000000000..abe80fe3e
--- /dev/null
+++ b/data/src/main/sqldelight/tachiyomi/migrations/11.sqm
@@ -0,0 +1,16 @@
+import kotlin.Boolean;
+
+CREATE TABLE extension_store(
+ index_url TEXT NOT NULL PRIMARY KEY,
+ name TEXT NOT NULL,
+ badge_label TEXT NOT NULL,
+ signing_key TEXT NOT NULL,
+ contact_website TEXT NOT NULL,
+ contact_discord TEXT,
+ is_legacy INTEGER AS Boolean NOT NULL
+);
+
+INSERT INTO extension_store(index_url, name, badge_label, signing_key, contact_website, contact_discord, is_legacy)
+SELECT base_url || '/repo.json', name, 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 93%
rename from app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt
rename to domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt
index a8e80d0a5..2d6d2041e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt
+++ b/domain/src/main/java/eu/kanade/tachiyomi/extension/model/Extension.kt
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.model
import android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.source.Source
+import mihon.domain.extension.model.ExtensionStore
import tachiyomi.domain.source.model.StubSource
sealed class Extension {
@@ -28,7 +29,7 @@ sealed class Extension {
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,
val isShared: Boolean,
- val repoUrl: String? = null,
+ val store: ExtensionStore? = null,
) : Extension()
data class Available(
@@ -40,9 +41,9 @@ sealed class Extension {
override val lang: String,
override val isNsfw: Boolean,
val sources: List,
- val apkName: String,
+ val apkUrl: String,
val iconUrl: String,
- val repoUrl: String,
+ val store: ExtensionStore,
) : Extension() {
data class Source(
diff --git a/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt b/domain/src/main/java/mihon/domain/extension/interactor/AddExtensionStore.kt
new file mode 100644
index 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..d2cadd0b6
--- /dev/null
+++ b/domain/src/main/java/mihon/domain/extension/model/ExtensionStore.kt
@@ -0,0 +1,15 @@
+package mihon.domain.extension.model
+
+data class ExtensionStore(
+ val indexUrl: String,
+ val name: String,
+ val badgeLabel: String,
+ val signingKey: String,
+ val contact: Contact,
+ val isLegacy: Boolean,
+) {
+ data class Contact(
+ val website: String,
+ val discord: String?,
+ )
+}
diff --git a/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt b/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt
new file mode 100644
index 000000000..3ab477081
--- /dev/null
+++ b/domain/src/main/java/mihon/domain/extension/repository/ExtensionStoreRepository.kt
@@ -0,0 +1,23 @@
+package mihon.domain.extension.repository
+
+import eu.kanade.tachiyomi.extension.model.Extension
+import kotlinx.coroutines.flow.Flow
+import mihon.domain.extension.model.ExtensionStore
+
+interface ExtensionStoreRepository {
+ suspend fun insert(indexUrl: String): Result
+
+ suspend fun insertFromPreference(indexUrl: String, name: String)
+
+ suspend fun refreshAll()
+
+ suspend fun fetchExtensions(): List
+
+ suspend fun getAll(): List
+
+ fun getAllAsFlow(): Flow>
+
+ fun getCountAsFlow(): Flow
+
+ suspend fun remove(indexUrl: String)
+}
diff --git a/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt b/domain/src/main/java/mihon/domain/extensionrepo/exception/SaveExtensionRepoException.kt
deleted file mode 100644
index 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 02623d8af..000000000
--- a/domain/src/main/java/mihon/domain/extensionrepo/interactor/CreateExtensionRepo.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package mihon.domain.extensionrepo.interactor
-
-import logcat.LogPriority
-import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
-import mihon.domain.extensionrepo.model.ExtensionRepo
-import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
-import mihon.domain.extensionrepo.service.ExtensionRepoService
-import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
-import tachiyomi.core.common.util.system.logcat
-
-class CreateExtensionRepo(
- private val repository: ExtensionRepoRepository,
- private val service: ExtensionRepoService,
-) {
- private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
-
- suspend fun await(indexUrl: String): Result {
- val formattedIndexUrl = indexUrl.toHttpUrlOrNull()
- ?.toString()
- ?.takeIf { it.matches(repoRegex) }
- ?: return Result.InvalidUrl
-
- val baseUrl = formattedIndexUrl.removeSuffix("/index.min.json")
- return service.fetchRepoDetails(baseUrl)?.let { insert(it) } ?: Result.InvalidUrl
- }
-
- private suspend fun insert(repo: ExtensionRepo): Result {
- return try {
- repository.insertRepo(
- repo.baseUrl,
- repo.name,
- repo.shortName,
- repo.website,
- repo.signingKeyFingerprint,
- )
- Result.Success
- } catch (e: SaveExtensionRepoException) {
- logcat(LogPriority.WARN, e) { "SQL Conflict attempting to add new repository ${repo.baseUrl}" }
- return handleInsertionError(repo)
- }
- }
-
- /**
- * Error Handler for insert when there are trying to create new repositories
- *
- * SaveExtensionRepoException doesn't provide constraint info in exceptions.
- * First check if the conflict was on primary key. if so return RepoAlreadyExists
- * Then check if the conflict was on fingerprint. if so Return DuplicateFingerprint
- * If neither are found, there was some other Error, and return Result.Error
- *
- * @param repo Extension Repo holder for passing to DB/Error Dialog
- */
- private suspend fun handleInsertionError(repo: ExtensionRepo): Result {
- val repoExists = repository.getRepo(repo.baseUrl)
- if (repoExists != null) {
- return Result.RepoAlreadyExists
- }
- val matchingFingerprintRepo = repository.getRepoBySigningKeyFingerprint(repo.signingKeyFingerprint)
- if (matchingFingerprintRepo != null) {
- return Result.DuplicateFingerprint(matchingFingerprintRepo, repo)
- }
- return Result.Error
- }
-
- sealed interface Result {
- data class DuplicateFingerprint(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : Result
- data object InvalidUrl : Result
- data object RepoAlreadyExists : Result
- data object Success : Result
- data object Error : Result
- }
-}
diff --git a/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt b/domain/src/main/java/mihon/domain/extensionrepo/interactor/DeleteExtensionRepo.kt
deleted file mode 100644
index 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 d0b7edd36..f624eac2e 100644
--- a/i18n/src/commonMain/moko-resources/base/strings.xml
+++ b/i18n/src/commonMain/moko-resources/base/strings.xml
@@ -363,20 +363,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
@@ -566,7 +565,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