Change extension repo to extension store and add support for newer extension index format (#1744)
PR: mihonapp/mihon#3349 Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com> Co-authored-by: Jobobby04 <jobobby04@users.noreply.github.com> (cherry picked from commit b446d2a6b1844f24b2d4912ba8be8b4648549f41) (cherry picked from commit a0ae52671fe2ddab3883b99f437d521446a8539d)
This commit is contained in:
parent
21c1b85796
commit
da7ee11bb3
71 changed files with 1463 additions and 1305 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -11,6 +11,24 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||||
- `Other` - for technical stuff.
|
- `Other` - for technical stuff.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- Add support for `tachiyomix` extension index format ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Change all reference of extension repo to extension store ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Add missing `outlineVariant` color to Nord theme ([@CompileConnected](https://github.com/CompileConnected)) ([#3184](https://github.com/mihonapp/mihon/pull/3184))
|
||||||
|
|
||||||
|
## [v0.19.9] - 2026-04-11
|
||||||
|
### Fixed
|
||||||
|
- Regression with installing/updating extension ([@AntsyLich](https://github.com/AntsyLich))
|
||||||
|
|
||||||
|
## [v0.19.8] - 2026-04-11
|
||||||
|
### Added
|
||||||
|
- Add dedicated "Support Us" screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3200](https://github.com/mihonapp/mihon/pull/3200))
|
||||||
|
- Add a one time popup asking for donation from long time users ([@AntsyLich](https://github.com/AntsyLich)) ([#3203](https://github.com/mihonapp/mihon/pull/3203))
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
- Show informative error when trying to add unapproved titles to list on MAL ([@MajorTanya](https://github.com/MajorTanya)) ([#3155](https://github.com/mihonapp/mihon/pull/3155))
|
- Show informative error when trying to add unapproved titles to list on MAL ([@MajorTanya](https://github.com/MajorTanya)) ([#3155](https://github.com/mihonapp/mihon/pull/3155))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,8 @@
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Deep link to add repos -->
|
<!-- Legacy deeplink to add extension store -->
|
||||||
<intent-filter android:label="@string/action_add_repo">
|
<intent-filter android:label="@string/action_addExtensionStore">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
@ -84,6 +84,17 @@
|
||||||
<data android:host="add-repo" />
|
<data android:host="add-repo" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Deeplink to add extension store -->
|
||||||
|
<intent-filter android:label="@string/action_addExtensionStore">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="mihon" />
|
||||||
|
<data android:host="extension-store" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Open backup files -->
|
<!-- Open backup files -->
|
||||||
<intent-filter android:label="@string/pref_restore_backup">
|
<intent-filter android:label="@string/pref_restore_backup">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,15 @@ import eu.kanade.domain.track.interactor.AddTracks
|
||||||
import eu.kanade.domain.track.interactor.RefreshTracks
|
import eu.kanade.domain.track.interactor.RefreshTracks
|
||||||
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||||
import eu.kanade.domain.track.interactor.TrackChapter
|
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.chapter.interactor.FilterChaptersForDownload
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
import mihon.domain.extension.interactor.AddExtensionStore
|
||||||
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
|
||||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
import mihon.domain.extension.interactor.GetExtensionStores
|
||||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
|
import mihon.domain.extension.interactor.RemoveExtensionStore
|
||||||
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
|
||||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
|
||||||
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||||
|
|
@ -199,14 +198,14 @@ class DomainModule : InjektModule {
|
||||||
addFactory { ToggleSourcePin(get()) }
|
addFactory { ToggleSourcePin(get()) }
|
||||||
addFactory { TrustExtension(get(), get()) }
|
addFactory { TrustExtension(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
|
addSingletonFactory { ExtensionStoreService(get(), get(), get()) }
|
||||||
addFactory { ExtensionRepoService(get(), get()) }
|
addSingletonFactory<ExtensionStoreRepository> { ExtensionStoreRepositoryImpl(get(), get()) }
|
||||||
addFactory { GetExtensionRepo(get()) }
|
addFactory { AddExtensionStore(get()) }
|
||||||
addFactory { GetExtensionRepoCount(get()) }
|
addFactory { GetExtensionStoreCountAsFlow(get()) }
|
||||||
addFactory { CreateExtensionRepo(get(), get()) }
|
addFactory { GetExtensionStores(get()) }
|
||||||
addFactory { DeleteExtensionRepo(get()) }
|
addFactory { RemoveExtensionStore(get()) }
|
||||||
addFactory { ReplaceExtensionRepo(get()) }
|
addFactory { UpdateExtensionStores(get()) }
|
||||||
addFactory { UpdateExtensionRepo(get(), get()) }
|
|
||||||
addFactory { ToggleIncognito(get()) }
|
addFactory { ToggleIncognito(get()) }
|
||||||
addFactory { GetIncognitoState(get(), get(), get()) }
|
addFactory { GetIncognitoState(get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,7 @@ class GetExtensionLanguages(
|
||||||
) { enabledLanguage, availableExtensions ->
|
) { enabledLanguage, availableExtensions ->
|
||||||
availableExtensions
|
availableExtensions
|
||||||
.flatMap { ext ->
|
.flatMap { ext ->
|
||||||
if (ext.sources.isEmpty()) {
|
ext.sources.map { it.lang }
|
||||||
listOf(ext.lang)
|
|
||||||
} else {
|
|
||||||
ext.sources.map { it.lang }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,6 @@ class GetExtensionsByType(
|
||||||
(showNsfwSources || !extension.isNsfw)
|
(showNsfwSources || !extension.isNsfw)
|
||||||
}
|
}
|
||||||
.flatMap { ext ->
|
.flatMap { ext ->
|
||||||
if (ext.sources.isEmpty()) {
|
|
||||||
return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
|
|
||||||
}
|
|
||||||
ext.sources.filter { it.lang in enabledLanguages }
|
ext.sources.filter { it.lang in enabledLanguages }
|
||||||
.map {
|
.map {
|
||||||
ext.copy(
|
ext.copy(
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,21 @@ import android.content.pm.PackageInfo
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.util.system.isDebugBuildType
|
import eu.kanade.tachiyomi.util.system.isDebugBuildType
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
|
|
||||||
class TrustExtension(
|
class TrustExtension(
|
||||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
private val repository: ExtensionStoreRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (isDebugBuildType) return true
|
if (isDebugBuildType) return true
|
||||||
if (fingerprints.contains(CreateExtensionRepo.KOMIKKU_SIGNATURE)) return true
|
if (fingerprints.contains(KOMIKKU_SIGNATURE)) return true
|
||||||
// KMK <--
|
// KMK <--
|
||||||
val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
|
val trustedFingerprints = repository.getAll().map { it.signingKey }.toHashSet()
|
||||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
||||||
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
|
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class SyncPreferences(
|
||||||
tracking = preferenceStore.getBoolean("tracking", true).get(),
|
tracking = preferenceStore.getBoolean("tracking", true).get(),
|
||||||
history = preferenceStore.getBoolean("history", true).get(),
|
history = preferenceStore.getBoolean("history", true).get(),
|
||||||
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
|
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
|
||||||
extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(),
|
extensionStores = preferenceStore.getBoolean("extensionStores", true).get(),
|
||||||
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
|
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
|
||||||
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
|
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@ class SyncPreferences(
|
||||||
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
|
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
|
||||||
preferenceStore.getBoolean("history", true).set(syncSettings.history)
|
preferenceStore.getBoolean("history", true).set(syncSettings.history)
|
||||||
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
|
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
|
||||||
preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings)
|
preferenceStore.getBoolean("extensionStores", true).set(syncSettings.extensionStores)
|
||||||
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
|
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
|
||||||
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
|
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ data class SyncSettings(
|
||||||
val tracking: Boolean = true,
|
val tracking: Boolean = true,
|
||||||
val history: Boolean = true,
|
val history: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val extensionRepoSettings: Boolean = true,
|
val extensionStores: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,12 +86,12 @@ fun ExtensionDetailsScreen(
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val url = remember(state.extension) {
|
val url = remember(state.extension) {
|
||||||
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
||||||
regex.find(state.extension?.repoUrl.orEmpty())
|
regex.find(state.extension?.store?.indexUrl.orEmpty())
|
||||||
?.let {
|
?.let {
|
||||||
val (user, repo) = it.destructured
|
val (user, repo) = it.destructured
|
||||||
"https://github.com/$user/$repo"
|
"https://github.com/$user/$repo"
|
||||||
}
|
}
|
||||||
?: state.extension?.repoUrl
|
?: state.extension?.store?.indexUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -270,14 +270,17 @@ private fun DetailsHeader(
|
||||||
|
|
||||||
if (extension is Extension.Installed) {
|
if (extension is Extension.Installed) {
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
append(
|
appendLine(
|
||||||
"""
|
"""
|
||||||
Update available: ${extension.hasUpdate}
|
Update available: ${extension.hasUpdate}
|
||||||
Obsolete: ${extension.isObsolete}
|
Obsolete: ${extension.isObsolete}
|
||||||
Shared: ${extension.isShared}
|
Shared: ${extension.isShared}
|
||||||
Repository: ${extension.repoUrl}
|
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
|
val store = extension.store
|
||||||
|
if (store != null) {
|
||||||
|
append("Repository: ${store.indexUrl}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.copyToClipboard("Extension Debug information", extDebugInfo)
|
context.copyToClipboard("Extension Debug information", extDebugInfo)
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
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.animateItemFastScroll
|
||||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
|
@ -58,6 +58,8 @@ import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
|
|
@ -110,9 +112,9 @@ fun ExtensionScreen(
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
stringRes = MR.strings.label_extension_repos,
|
stringRes = MR.strings.extensionStores,
|
||||||
icon = Icons.Outlined.Settings,
|
icon = Icons.Outlined.Settings,
|
||||||
onClick = { navigator.push(ExtensionReposScreen()) },
|
onClick = { navigator.push(ExtensionStoresScreen()) },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -195,9 +197,9 @@ private fun ExtensionContent(
|
||||||
// KMK -->
|
// KMK -->
|
||||||
KMR.strings.extensions_page_more -> {
|
KMR.strings.extensions_page_more -> {
|
||||||
{
|
{
|
||||||
Button(onClick = { navigator?.push(ExtensionReposScreen()) }) {
|
Button(onClick = { navigator?.push(ExtensionStoresScreen()) }) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(MR.strings.action_add_repo),
|
text = stringResource(MR.strings.action_addExtensionStore),
|
||||||
style = LocalTextStyle.current.copy(
|
style = LocalTextStyle.current.copy(
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
|
|
@ -401,7 +403,7 @@ private fun ExtensionItemContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
Text(text = extension.repoName?.let { "@$it" } ?: "(?)")
|
Text(text = extension.storeName?.let { "@$it" } ?: "(?)")
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
val warning = when {
|
val warning = when {
|
||||||
|
|
@ -604,11 +606,11 @@ private fun ExtensionItemContentPreview() {
|
||||||
libVersion = 1.0,
|
libVersion = 1.0,
|
||||||
isNsfw = true,
|
isNsfw = true,
|
||||||
signatureHash = "900000",
|
signatureHash = "900000",
|
||||||
repoName = "Repository",
|
storeName = "Repository",
|
||||||
sources = emptyList(),
|
sources = emptyList(),
|
||||||
apkName = "Test",
|
apkUrl = "Test",
|
||||||
iconUrl = "",
|
iconUrl = "",
|
||||||
repoUrl = "",
|
store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||||
)
|
)
|
||||||
val extInstalled = Extension.Installed(
|
val extInstalled = Extension.Installed(
|
||||||
name = "Tachiyomi",
|
name = "Tachiyomi",
|
||||||
|
|
@ -619,9 +621,9 @@ private fun ExtensionItemContentPreview() {
|
||||||
libVersion = 1.0,
|
libVersion = 1.0,
|
||||||
isNsfw = true,
|
isNsfw = true,
|
||||||
signatureHash = "900000",
|
signatureHash = "900000",
|
||||||
repoName = "Repository",
|
storeName = "Repository",
|
||||||
sources = emptyList(),
|
sources = emptyList(),
|
||||||
repoUrl = "",
|
store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||||
pkgFactory = null,
|
pkgFactory = null,
|
||||||
icon = null,
|
icon = null,
|
||||||
hasUpdate = false,
|
hasUpdate = false,
|
||||||
|
|
@ -638,12 +640,12 @@ private fun ExtensionItemContentPreview() {
|
||||||
libVersion = 1.0,
|
libVersion = 1.0,
|
||||||
isNsfw = true,
|
isNsfw = true,
|
||||||
signatureHash = "900000",
|
signatureHash = "900000",
|
||||||
repoName = "Repository",
|
storeName = "Repository",
|
||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
ExtensionItemContent(
|
ExtensionItemContent(
|
||||||
extension = extAvail.copy(
|
extension = extAvail.copy(
|
||||||
repoName = "Repository extensions minion multiple languages various sources",
|
storeName = "Repository extensions minion multiple languages various sources",
|
||||||
),
|
),
|
||||||
installStep = InstallStep.Idle,
|
installStep = InstallStep.Idle,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@ import eu.kanade.core.preference.asState
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
import eu.kanade.presentation.more.settings.Preference
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
||||||
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen
|
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
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.core.common.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
|
|
@ -43,9 +43,9 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
||||||
val getExtensionRepoCount = remember { Injekt.get<GetExtensionRepoCount>() }
|
val getExtensionStoreCountAsFlow = remember { Injekt.get<GetExtensionStoreCountAsFlow>() }
|
||||||
|
|
||||||
val reposCount by getExtensionRepoCount.subscribe().collectAsState(0)
|
val reposCount by getExtensionStoreCountAsFlow().collectAsState(0)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
@ -142,10 +142,10 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||||
title = stringResource(MR.strings.pref_hide_in_library_items),
|
title = stringResource(MR.strings.pref_hide_in_library_items),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.label_extension_repos),
|
title = stringResource(MR.strings.extensionStores),
|
||||||
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount),
|
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.toInt(), reposCount),
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.push(ExtensionReposScreen())
|
navigator.push(ExtensionStoresScreen())
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
package eu.kanade.presentation.more.settings.screen.browse
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
|
||||||
import dev.icerock.moko.resources.StringResource
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
|
||||||
import tachiyomi.core.common.util.lang.launchIO
|
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class ExtensionReposScreenModel(
|
|
||||||
private val getExtensionRepo: GetExtensionRepo = Injekt.get(),
|
|
||||||
private val createExtensionRepo: CreateExtensionRepo = Injekt.get(),
|
|
||||||
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
|
||||||
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
|
|
||||||
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
|
|
||||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
|
||||||
// KMK -->
|
|
||||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
|
||||||
// KMK <--
|
|
||||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
|
||||||
|
|
||||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
|
||||||
val events = _events.receiveAsFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
getExtensionRepo.subscribeAll()
|
|
||||||
.collectLatest { repos ->
|
|
||||||
mutableState.update {
|
|
||||||
RepoScreenState.Success(
|
|
||||||
repos = repos.toImmutableSet(),
|
|
||||||
// KMK -->
|
|
||||||
disabledRepos = sourcePreferences.disabledRepos().get(),
|
|
||||||
// KMK <--
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMK -->
|
|
||||||
sourcePreferences.disabledRepos().changes()
|
|
||||||
.onEach { disabledRepos ->
|
|
||||||
mutableState.update {
|
|
||||||
when (it) {
|
|
||||||
is RepoScreenState.Success -> it.copy(disabledRepos = disabledRepos)
|
|
||||||
else -> it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.launchIn(screenModelScope)
|
|
||||||
// KMK <--
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and adds a new repo to the database.
|
|
||||||
*
|
|
||||||
* @param baseUrl The baseUrl of the repo to create.
|
|
||||||
*/
|
|
||||||
fun createRepo(baseUrl: String) {
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
when (val result = createExtensionRepo.await(baseUrl)) {
|
|
||||||
CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions()
|
|
||||||
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
|
||||||
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
|
|
||||||
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
|
||||||
showDialog(RepoDialog.Conflict(result.oldRepo, result.newRepo))
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts a repo to the database, replace a matching repo with the same signing key fingerprint if found.
|
|
||||||
*
|
|
||||||
* @param newRepo The repo to insert
|
|
||||||
*/
|
|
||||||
fun replaceRepo(newRepo: ExtensionRepo) {
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
replaceExtensionRepo.await(newRepo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refreshes information for each repository.
|
|
||||||
*/
|
|
||||||
fun refreshRepos() {
|
|
||||||
val status = state.value
|
|
||||||
|
|
||||||
if (status is RepoScreenState.Success) {
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
updateExtensionRepo.awaitAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the given repo from the database
|
|
||||||
*/
|
|
||||||
fun deleteRepo(baseUrl: String) {
|
|
||||||
// KMK -->
|
|
||||||
// Remove repo from disabled list
|
|
||||||
enableRepo(baseUrl)
|
|
||||||
// KMK <--
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
deleteExtensionRepo.await(baseUrl)
|
|
||||||
extensionManager.findAvailableExtensions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMK -->
|
|
||||||
fun enableRepo(baseUrl: String) {
|
|
||||||
val disabledRepos = sourcePreferences.disabledRepos().get()
|
|
||||||
if (baseUrl in disabledRepos) {
|
|
||||||
sourcePreferences.disabledRepos().set(
|
|
||||||
disabledRepos.filterNot { it == baseUrl }.toSet(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disableRepo(baseUrl: String) {
|
|
||||||
val disabledRepos = sourcePreferences.disabledRepos().get()
|
|
||||||
if (baseUrl !in disabledRepos) {
|
|
||||||
sourcePreferences.disabledRepos().set(
|
|
||||||
disabledRepos + baseUrl,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshExtensionList() {
|
|
||||||
screenModelScope.launchIO {
|
|
||||||
extensionManager.findAvailableExtensions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// KMK <--
|
|
||||||
|
|
||||||
fun showDialog(dialog: RepoDialog) {
|
|
||||||
mutableState.update {
|
|
||||||
when (it) {
|
|
||||||
RepoScreenState.Loading -> it
|
|
||||||
is RepoScreenState.Success -> it.copy(dialog = dialog)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismissDialog() {
|
|
||||||
mutableState.update {
|
|
||||||
when (it) {
|
|
||||||
RepoScreenState.Loading -> it
|
|
||||||
is RepoScreenState.Success -> it.copy(dialog = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class RepoEvent {
|
|
||||||
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
|
|
||||||
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
|
|
||||||
data object RepoAlreadyExists : LocalizedMessage(MR.strings.error_repo_exists)
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class RepoDialog {
|
|
||||||
data object Create : RepoDialog()
|
|
||||||
data class Delete(val repo: String) : RepoDialog()
|
|
||||||
data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog()
|
|
||||||
data class Confirm(val url: String) : RepoDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class RepoScreenState {
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data object Loading : RepoScreenState()
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
data class Success(
|
|
||||||
val repos: ImmutableSet<ExtensionRepo>,
|
|
||||||
val oldRepos: ImmutableSet<String>? = null,
|
|
||||||
val dialog: RepoDialog? = null,
|
|
||||||
// KMK -->
|
|
||||||
val disabledRepos: Set<String> = emptySet(),
|
|
||||||
// KMK <--
|
|
||||||
) : RepoScreenState() {
|
|
||||||
|
|
||||||
val isEmpty: Boolean
|
|
||||||
get() = repos.isEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,20 +8,18 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
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.ExtensionStoreConfirmDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreCreateDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreDeleteDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoresScreen
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
|
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.toImmutableSet
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
class ExtensionReposScreen(
|
class ExtensionStoresScreen(
|
||||||
private val url: String? = null,
|
private val url: String? = null,
|
||||||
) : Screen() {
|
) : Screen() {
|
||||||
|
|
||||||
|
|
@ -30,80 +28,72 @@ class ExtensionReposScreen(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
|
val screenModel = rememberScreenModel { ExtensionStoresScreenModel() }
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(url) {
|
LaunchedEffect(url) {
|
||||||
url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) }
|
url?.let { screenModel.addFromDeeplink(url) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is RepoScreenState.Loading) {
|
if (state is ExtensionStoreScreenState.Loading) {
|
||||||
LoadingScreen()
|
LoadingScreen()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val successState = state as RepoScreenState.Success
|
val successState = state as ExtensionStoreScreenState.Success
|
||||||
|
|
||||||
ExtensionReposScreen(
|
ExtensionStoresScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
onClickCreate = { screenModel.showDialog(ExtensionStoreDialog.Create()) },
|
||||||
onOpenWebsite = { context.openInBrowser(it.website) },
|
onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) },
|
||||||
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
onOpenWebsite = { it.contact.website.let(context::openInBrowser) },
|
||||||
|
onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) },
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onClickEnable = {
|
onClickEnable = {
|
||||||
screenModel.enableRepo(it)
|
screenModel.enableStore(it.indexUrl)
|
||||||
screenModel.refreshExtensionList()
|
screenModel.refreshExtensionList()
|
||||||
context.toast(KMR.strings.extensions_page_need_refresh)
|
context.toast(KMR.strings.extensions_page_need_refresh)
|
||||||
},
|
},
|
||||||
onClickDisable = {
|
onClickDisable = {
|
||||||
screenModel.disableRepo(it)
|
screenModel.disableStore(it.indexUrl)
|
||||||
screenModel.refreshExtensionList()
|
screenModel.refreshExtensionList()
|
||||||
context.toast(KMR.strings.extensions_page_need_refresh)
|
context.toast(KMR.strings.extensions_page_need_refresh)
|
||||||
},
|
},
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) },
|
||||||
onClickRefresh = { screenModel.refreshRepos() },
|
onClickRefresh = { screenModel.refreshRepos() },
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
)
|
)
|
||||||
|
|
||||||
when (val dialog = successState.dialog) {
|
when (val dialog = successState.dialog) {
|
||||||
null -> {}
|
null -> {}
|
||||||
is RepoDialog.Create -> {
|
is ExtensionStoreDialog.Create -> {
|
||||||
ExtensionRepoCreateDialog(
|
ExtensionStoreCreateDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onCreate = { screenModel.createRepo(it) },
|
onCreate = { screenModel.createRepo(it) },
|
||||||
repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(),
|
storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(),
|
||||||
|
processing = dialog.processing,
|
||||||
|
errorMessage = dialog.errorMessage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RepoDialog.Delete -> {
|
is ExtensionStoreDialog.Delete -> {
|
||||||
ExtensionRepoDeleteDialog(
|
ExtensionStoreDeleteDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onDelete = { screenModel.deleteRepo(dialog.repo) },
|
onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) },
|
||||||
repo = dialog.repo,
|
storeName = dialog.store.name,
|
||||||
|
storeIndexUrl = dialog.store.indexUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is RepoDialog.Conflict -> {
|
is ExtensionStoreDialog.Confirm -> {
|
||||||
ExtensionRepoConflictDialog(
|
ExtensionStoreConfirmDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
|
||||||
onMigrate = { screenModel.replaceRepo(dialog.newRepo) },
|
|
||||||
oldRepo = dialog.oldRepo,
|
|
||||||
newRepo = dialog.newRepo,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is RepoDialog.Confirm -> {
|
|
||||||
ExtensionRepoConfirmDialog(
|
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onCreate = { screenModel.createRepo(dialog.url) },
|
onCreate = { screenModel.createRepo(dialog.url) },
|
||||||
repo = dialog.url,
|
storeIndexUrl = dialog.url,
|
||||||
|
storeAlreadyExists = dialog.alreadyExists,
|
||||||
|
processing = dialog.processing,
|
||||||
|
errorMessage = dialog.errorMessage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
screenModel.events.collectLatest { event ->
|
|
||||||
if (event is RepoEvent.LocalizedMessage) {
|
|
||||||
context.toast(event.stringRes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
package eu.kanade.presentation.more.settings.screen.browse
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import mihon.domain.extension.interactor.AddExtensionStore
|
||||||
|
import mihon.domain.extension.interactor.GetExtensionStores
|
||||||
|
import mihon.domain.extension.interactor.RemoveExtensionStore
|
||||||
|
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
import tachiyomi.core.common.util.lang.launchIO
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class ExtensionStoresScreenModel(
|
||||||
|
private val getExtensionStores: GetExtensionStores = Injekt.get(),
|
||||||
|
private val addExtensionStore: AddExtensionStore = Injekt.get(),
|
||||||
|
private val removeExtensionStore: RemoveExtensionStore = Injekt.get(),
|
||||||
|
private val updateExtensionStores: UpdateExtensionStores = Injekt.get(),
|
||||||
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
|
// KMK -->
|
||||||
|
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||||
|
// KMK <--
|
||||||
|
) : StateScreenModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) {
|
||||||
|
|
||||||
|
private inline fun updateSuccessState(
|
||||||
|
func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success,
|
||||||
|
) {
|
||||||
|
mutableState.update {
|
||||||
|
when (it) {
|
||||||
|
ExtensionStoreScreenState.Loading -> it
|
||||||
|
is ExtensionStoreScreenState.Success -> func(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
getExtensionStores.subscribe()
|
||||||
|
.collectLatest { stores ->
|
||||||
|
mutableState.update {
|
||||||
|
when (it) {
|
||||||
|
ExtensionStoreScreenState.Loading -> ExtensionStoreScreenState.Success(
|
||||||
|
stores = stores,
|
||||||
|
// KMK -->
|
||||||
|
disabledRepos = sourcePreferences.disabledRepos().get(),
|
||||||
|
// KMK <--
|
||||||
|
)
|
||||||
|
is ExtensionStoreScreenState.Success -> it.copy(stores = stores)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
sourcePreferences.disabledRepos().changes()
|
||||||
|
.onEach { disabledRepos ->
|
||||||
|
mutableState.update {
|
||||||
|
when (it) {
|
||||||
|
is ExtensionStoreScreenState.Success -> it.copy(disabledRepos = disabledRepos)
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(screenModelScope)
|
||||||
|
// KMK <--
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and adds a new repo to the database.
|
||||||
|
*
|
||||||
|
* @param indexUrl The baseUrl of the repo to create.
|
||||||
|
*/
|
||||||
|
fun createRepo(indexUrl: String) {
|
||||||
|
// KMK -->
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
// KMK <--
|
||||||
|
updateSuccessState {
|
||||||
|
it.copy(
|
||||||
|
dialog = when (it.dialog) {
|
||||||
|
is ExtensionStoreDialog.Create -> it.dialog.copy(processing = true)
|
||||||
|
is ExtensionStoreDialog.Confirm -> it.dialog.copy(processing = true)
|
||||||
|
else -> it.dialog
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
addExtensionStore(indexUrl)
|
||||||
|
.onSuccess {
|
||||||
|
extensionManager.findAvailableExtensions()
|
||||||
|
dismissDialog()
|
||||||
|
}
|
||||||
|
.onFailure { throwable ->
|
||||||
|
updateSuccessState {
|
||||||
|
it.copy(
|
||||||
|
dialog = when (it.dialog) {
|
||||||
|
is ExtensionStoreDialog.Create -> it.dialog.copy(
|
||||||
|
processing = false,
|
||||||
|
errorMessage = throwable.message ?: "unknown error",
|
||||||
|
)
|
||||||
|
is ExtensionStoreDialog.Confirm -> it.dialog.copy(
|
||||||
|
processing = false,
|
||||||
|
errorMessage = throwable.message ?: "unknown error",
|
||||||
|
)
|
||||||
|
else -> it.dialog
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes information for each repository.
|
||||||
|
*/
|
||||||
|
fun refreshRepos() {
|
||||||
|
val status = state.value
|
||||||
|
|
||||||
|
if (status is ExtensionStoreScreenState.Success) {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
updateExtensionStores()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given repo from the database
|
||||||
|
*/
|
||||||
|
fun deleteRepo(indexUrl: String) {
|
||||||
|
// KMK -->
|
||||||
|
// Remove repo from disabled list
|
||||||
|
enableStore(indexUrl)
|
||||||
|
// KMK <--
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
removeExtensionStore(indexUrl)
|
||||||
|
extensionManager.findAvailableExtensions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
fun enableStore(indexUrl: String) {
|
||||||
|
val disabledRepos = sourcePreferences.disabledRepos().get()
|
||||||
|
if (indexUrl in disabledRepos) {
|
||||||
|
sourcePreferences.disabledRepos().set(
|
||||||
|
disabledRepos.filterNot { it == indexUrl }.toSet(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableStore(indexUrl: String) {
|
||||||
|
val disabledRepos = sourcePreferences.disabledRepos().get()
|
||||||
|
if (indexUrl !in disabledRepos) {
|
||||||
|
sourcePreferences.disabledRepos().set(
|
||||||
|
disabledRepos + indexUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshExtensionList() {
|
||||||
|
screenModelScope.launchIO {
|
||||||
|
extensionManager.findAvailableExtensions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// KMK <--
|
||||||
|
|
||||||
|
fun addFromDeeplink(storeIndexUrl: String) {
|
||||||
|
updateSuccessState { state ->
|
||||||
|
state.copy(
|
||||||
|
dialog = ExtensionStoreDialog.Confirm(
|
||||||
|
url = storeIndexUrl,
|
||||||
|
alreadyExists = state.stores.any { it.indexUrl == storeIndexUrl },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showDialog(dialog: ExtensionStoreDialog) {
|
||||||
|
updateSuccessState { state ->
|
||||||
|
state.copy(dialog = dialog)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissDialog() {
|
||||||
|
updateSuccessState {
|
||||||
|
it.copy(dialog = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ExtensionStoreDialog {
|
||||||
|
data class Create(val processing: Boolean = false, val errorMessage: String? = null) : ExtensionStoreDialog()
|
||||||
|
data class Delete(val store: ExtensionStore) : ExtensionStoreDialog()
|
||||||
|
data class Confirm(
|
||||||
|
val url: String,
|
||||||
|
val alreadyExists: Boolean = false,
|
||||||
|
val processing: Boolean = false,
|
||||||
|
val errorMessage: String? = null,
|
||||||
|
) : ExtensionStoreDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ExtensionStoreScreenState {
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data object Loading : ExtensionStoreScreenState()
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class Success(
|
||||||
|
val stores: List<ExtensionStore>,
|
||||||
|
val dialog: ExtensionStoreDialog? = null,
|
||||||
|
// KMK -->
|
||||||
|
val disabledRepos: Set<String> = emptySet(),
|
||||||
|
// KMK <--
|
||||||
|
) : ExtensionStoreScreenState() {
|
||||||
|
|
||||||
|
val isEmpty: Boolean
|
||||||
|
get() = stores.isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String>,
|
|
||||||
) {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -11,9 +11,9 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
|
||||||
import androidx.compose.material.icons.outlined.ContentCopy
|
import androidx.compose.material.icons.outlined.ContentCopy
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.Public
|
||||||
import androidx.compose.material.icons.outlined.Visibility
|
import androidx.compose.material.icons.outlined.Visibility
|
||||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
|
@ -26,33 +26,33 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.imageResource
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE
|
import mihon.domain.extension.model.REPO_SIGNATURE
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE
|
|
||||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
import tachiyomi.presentation.core.icons.CustomIcons
|
||||||
|
import tachiyomi.presentation.core.icons.Discord
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionReposContent(
|
fun ExtensionStoresContent(
|
||||||
repos: ImmutableSet<ExtensionRepo>,
|
repos: List<ExtensionStore>,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
onOpenWebsite: (ExtensionRepo) -> Unit,
|
onCopy: (ExtensionStore) -> Unit,
|
||||||
onClickDelete: (String) -> Unit,
|
onOpenWebsite: (ExtensionStore) -> Unit,
|
||||||
|
onOpenDiscord: (ExtensionStore) -> Unit,
|
||||||
|
onClickDelete: (ExtensionStore) -> Unit,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onClickEnable: (String) -> Unit,
|
onClickEnable: (ExtensionStore) -> Unit,
|
||||||
onClickDisable: (String) -> Unit,
|
onClickDisable: (ExtensionStore) -> Unit,
|
||||||
disabledRepos: Set<String>,
|
disabledRepos: Set<String>,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -65,15 +65,17 @@ fun ExtensionReposContent(
|
||||||
) {
|
) {
|
||||||
repos.forEach {
|
repos.forEach {
|
||||||
item {
|
item {
|
||||||
ExtensionRepoListItem(
|
ExtensionStoresListItem(
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
repo = it,
|
store = it,
|
||||||
onOpenWebsite = { onOpenWebsite(it) },
|
onOpenWebsite = { onOpenWebsite(it) },
|
||||||
onDelete = { onClickDelete(it.baseUrl) },
|
onOpenDiscord = { onOpenDiscord(it) },
|
||||||
|
onCopy = { onCopy(it) },
|
||||||
|
onDelete = { onClickDelete(it) },
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onEnable = { onClickEnable(it.baseUrl) },
|
onEnable = { onClickEnable(it) },
|
||||||
onDisable = { onClickDisable(it.baseUrl) },
|
onDisable = { onClickDisable(it) },
|
||||||
isDisabled = it.baseUrl in disabledRepos,
|
isDisabled = it.indexUrl in disabledRepos,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -82,9 +84,11 @@ fun ExtensionReposContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionRepoListItem(
|
private fun ExtensionStoresListItem(
|
||||||
repo: ExtensionRepo,
|
store: ExtensionStore,
|
||||||
onOpenWebsite: () -> Unit,
|
onOpenWebsite: () -> Unit,
|
||||||
|
onOpenDiscord: () -> Unit,
|
||||||
|
onCopy: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
|
|
@ -93,8 +97,6 @@ private fun ExtensionRepoListItem(
|
||||||
onDisable: () -> Unit,
|
onDisable: () -> Unit,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -103,9 +105,9 @@ private fun ExtensionRepoListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = MaterialTheme.padding.medium),
|
.padding(start = MaterialTheme.padding.medium),
|
||||||
) {
|
) {
|
||||||
val resId = repoResId(repo.signingKeyFingerprint)
|
val resId = repoResId(store.signingKey)
|
||||||
Image(
|
Image(
|
||||||
bitmap = ImageBitmap.imageResource(id = resId),
|
painter = painterResource(id = resId),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
alpha = if (isDisabled) 0.4f else 1f,
|
alpha = if (isDisabled) 0.4f else 1f,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -126,7 +128,7 @@ private fun ExtensionRepoListItem(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = repo.name,
|
text = store.name,
|
||||||
// KMK: modifier = Modifier.padding(start = MaterialTheme.padding.medium),
|
// KMK: modifier = Modifier.padding(start = MaterialTheme.padding.medium),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
|
|
@ -142,17 +144,21 @@ private fun ExtensionRepoListItem(
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onOpenWebsite) {
|
IconButton(onClick = onOpenWebsite) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Outlined.OpenInNew,
|
imageVector = Icons.Outlined.Public,
|
||||||
contentDescription = stringResource(MR.strings.action_open_in_browser),
|
contentDescription = stringResource(MR.strings.action_open_in_browser),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(
|
if (store.contact.discord != null) {
|
||||||
onClick = {
|
IconButton(onClick = onOpenDiscord) {
|
||||||
val url = "${repo.baseUrl}/index.min.json"
|
Icon(
|
||||||
context.copyToClipboard(url, url)
|
imageVector = CustomIcons.Discord,
|
||||||
},
|
contentDescription = null,
|
||||||
) {
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onCopy) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.ContentCopy,
|
imageVector = Icons.Outlined.ContentCopy,
|
||||||
contentDescription = stringResource(MR.strings.action_copy_to_clipboard),
|
contentDescription = stringResource(MR.strings.action_copy_to_clipboard),
|
||||||
|
|
@ -190,16 +196,18 @@ fun repoResId(signKey: String) = when (signKey) {
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionReposContentPreview() {
|
fun ExtensionReposContentPreview() {
|
||||||
val repos = persistentSetOf(
|
val repos = persistentListOf(
|
||||||
ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE),
|
ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||||
ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE),
|
ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||||
ExtensionRepo("https://repo", "Other", "", "", "key2"),
|
ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true),
|
||||||
)
|
)
|
||||||
ExtensionReposContent(
|
ExtensionStoresContent(
|
||||||
repos = repos,
|
repos = repos,
|
||||||
lazyListState = LazyListState(),
|
lazyListState = LazyListState(),
|
||||||
paddingValues = PaddingValues(),
|
paddingValues = PaddingValues(),
|
||||||
|
onCopy = {},
|
||||||
onOpenWebsite = {},
|
onOpenWebsite = {},
|
||||||
|
onOpenDiscord = {},
|
||||||
onClickDelete = {},
|
onClickDelete = {},
|
||||||
onClickEnable = {},
|
onClickEnable = {},
|
||||||
onClickDisable = {},
|
onClickDisable = {},
|
||||||
|
|
@ -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<String>,
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
@file:JvmName("ExtensionReposScreenKt")
|
|
||||||
|
|
||||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
@ -21,13 +19,13 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoreScreenState
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_HELP
|
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE
|
import mihon.domain.extension.model.REPO_HELP
|
||||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
import mihon.domain.extension.model.REPO_SIGNATURE
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
@ -37,14 +35,16 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.util.plus
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionReposScreen(
|
fun ExtensionStoresScreen(
|
||||||
state: RepoScreenState.Success,
|
state: ExtensionStoreScreenState.Success,
|
||||||
onClickCreate: () -> Unit,
|
onClickCreate: () -> Unit,
|
||||||
onOpenWebsite: (ExtensionRepo) -> Unit,
|
onCopy: (ExtensionStore) -> Unit,
|
||||||
onClickDelete: (String) -> Unit,
|
onOpenWebsite: (ExtensionStore) -> Unit,
|
||||||
|
onOpenDiscord: (ExtensionStore) -> Unit,
|
||||||
|
onClickDelete: (ExtensionStore) -> Unit,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onClickEnable: (String) -> Unit,
|
onClickEnable: (ExtensionStore) -> Unit,
|
||||||
onClickDisable: (String) -> Unit,
|
onClickDisable: (ExtensionStore) -> Unit,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
onClickRefresh: () -> Unit,
|
onClickRefresh: () -> Unit,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
|
|
@ -54,7 +54,7 @@ fun ExtensionReposScreen(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
title = stringResource(MR.strings.label_extension_repos),
|
title = stringResource(MR.strings.extensionStores),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onClickRefresh) {
|
IconButton(onClick = onClickRefresh) {
|
||||||
|
|
@ -76,7 +76,7 @@ fun ExtensionReposScreen(
|
||||||
if (state.isEmpty) {
|
if (state.isEmpty) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
MR.strings.information_empty_repos,
|
MR.strings.extensionStoresScreen_emptyLabel,
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
help = {
|
help = {
|
||||||
|
|
@ -94,12 +94,14 @@ fun ExtensionReposScreen(
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtensionReposContent(
|
ExtensionStoresContent(
|
||||||
repos = state.repos,
|
repos = state.stores,
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
paddingValues = paddingValues + topSmallPaddingValues +
|
paddingValues = paddingValues + topSmallPaddingValues +
|
||||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||||
|
onCopy = onCopy,
|
||||||
onOpenWebsite = onOpenWebsite,
|
onOpenWebsite = onOpenWebsite,
|
||||||
|
onOpenDiscord = onOpenDiscord,
|
||||||
onClickDelete = onClickDelete,
|
onClickDelete = onClickDelete,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onClickEnable = onClickEnable,
|
onClickEnable = onClickEnable,
|
||||||
|
|
@ -113,19 +115,21 @@ fun ExtensionReposScreen(
|
||||||
// KMK -->
|
// KMK -->
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionReposScreenPreview() {
|
private fun ExtensionStoresScreenPreview() {
|
||||||
val state = RepoScreenState.Success(
|
val state = ExtensionStoreScreenState.Success(
|
||||||
repos = persistentSetOf(
|
stores = persistentListOf(
|
||||||
ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE),
|
ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||||
ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE),
|
ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||||
ExtensionRepo("https://repo", "Other", "", "", "key2"),
|
ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true),
|
||||||
),
|
),
|
||||||
disabledRepos = setOf("https://repo"),
|
disabledRepos = setOf("https://repo"),
|
||||||
)
|
)
|
||||||
ExtensionReposScreen(
|
ExtensionStoresScreen(
|
||||||
state = state,
|
state = state,
|
||||||
onClickCreate = { },
|
onClickCreate = { },
|
||||||
|
onCopy = { },
|
||||||
onOpenWebsite = { },
|
onOpenWebsite = { },
|
||||||
|
onOpenDiscord = { },
|
||||||
onClickDelete = { },
|
onClickDelete = { },
|
||||||
onClickEnable = { },
|
onClickEnable = { },
|
||||||
onClickDisable = { },
|
onClickDisable = { },
|
||||||
|
|
@ -136,12 +140,14 @@ private fun ExtensionReposScreenPreview() {
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionReposScreenEmptyPreview() {
|
private fun ExtensionStoresScreenEmptyPreview() {
|
||||||
val state = RepoScreenState.Success(repos = persistentSetOf())
|
val state = ExtensionStoreScreenState.Success(stores = persistentListOf())
|
||||||
ExtensionReposScreen(
|
ExtensionStoresScreen(
|
||||||
state = state,
|
state = state,
|
||||||
onClickCreate = { },
|
onClickCreate = { },
|
||||||
|
onCopy = { },
|
||||||
onOpenWebsite = { },
|
onOpenWebsite = { },
|
||||||
|
onOpenDiscord = { },
|
||||||
onClickDelete = { },
|
onClickDelete = { },
|
||||||
onClickEnable = { },
|
onClickEnable = { },
|
||||||
onClickDisable = { },
|
onClickDisable = { },
|
||||||
|
|
@ -114,7 +114,7 @@ private class SyncSettingsSelectorModel(
|
||||||
tracking = syncSettings.tracking,
|
tracking = syncSettings.tracking,
|
||||||
history = syncSettings.history,
|
history = syncSettings.history,
|
||||||
appSettings = syncSettings.appSettings,
|
appSettings = syncSettings.appSettings,
|
||||||
extensionRepoSettings = syncSettings.extensionRepoSettings,
|
extensionStores = syncSettings.extensionStores,
|
||||||
sourceSettings = syncSettings.sourceSettings,
|
sourceSettings = syncSettings.sourceSettings,
|
||||||
privateSettings = syncSettings.privateSettings,
|
privateSettings = syncSettings.privateSettings,
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ private class SyncSettingsSelectorModel(
|
||||||
tracking = backupOptions.tracking,
|
tracking = backupOptions.tracking,
|
||||||
history = backupOptions.history,
|
history = backupOptions.history,
|
||||||
appSettings = backupOptions.appSettings,
|
appSettings = backupOptions.appSettings,
|
||||||
extensionRepoSettings = backupOptions.extensionRepoSettings,
|
extensionStores = backupOptions.extensionStores,
|
||||||
sourceSettings = backupOptions.sourceSettings,
|
sourceSettings = backupOptions.sourceSettings,
|
||||||
privateSettings = backupOptions.privateSettings,
|
privateSettings = backupOptions.privateSettings,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionRepoBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionStoresBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.FeedBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.FeedBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
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.PreferenceBackupCreator
|
||||||
|
|
@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.backup.create.creators.SavedSearchBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupFeed
|
import eu.kanade.tachiyomi.data.backup.models.BackupFeed
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
|
|
@ -54,7 +54,7 @@ class BackupCreator(
|
||||||
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
||||||
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
||||||
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
|
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
|
||||||
private val extensionRepoBackupCreator: ExtensionRepoBackupCreator = ExtensionRepoBackupCreator(),
|
private val extensionStoresBackupCreator: ExtensionStoresBackupCreator = ExtensionStoresBackupCreator(),
|
||||||
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
private val feedBackupCreator: FeedBackupCreator = FeedBackupCreator(),
|
private val feedBackupCreator: FeedBackupCreator = FeedBackupCreator(),
|
||||||
|
|
@ -101,7 +101,7 @@ class BackupCreator(
|
||||||
backupCategories = backupCategories(options),
|
backupCategories = backupCategories(options),
|
||||||
backupSources = backupSources(backupManga),
|
backupSources = backupSources(backupManga),
|
||||||
backupPreferences = backupAppPreferences(options),
|
backupPreferences = backupAppPreferences(options),
|
||||||
backupExtensionRepo = backupExtensionRepos(options),
|
backupExtensionStores = backupExtensionStores(options),
|
||||||
backupSourcePreferences = backupSourcePreferences(options),
|
backupSourcePreferences = backupSourcePreferences(options),
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
@ -159,16 +159,16 @@ class BackupCreator(
|
||||||
return sourcesBackupCreator(mangas)
|
return sourcesBackupCreator(mangas)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* KMK --> */ suspend /* KMK <-- */ fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
internal fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||||
if (!options.appSettings) return emptyList()
|
if (!options.appSettings) return emptyList()
|
||||||
|
|
||||||
return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings)
|
return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun backupExtensionRepos(options: BackupOptions): List<BackupExtensionRepos> {
|
internal suspend fun backupExtensionStores(options: BackupOptions): List<BackupExtensionStore> {
|
||||||
if (!options.extensionRepoSettings) return emptyList()
|
if (!options.extensionStores) return emptyList()
|
||||||
|
|
||||||
return extensionRepoBackupCreator()
|
return extensionStoresBackupCreator()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ data class BackupOptions(
|
||||||
val history: Boolean = true,
|
val history: Boolean = true,
|
||||||
val readEntries: Boolean = true,
|
val readEntries: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val extensionRepoSettings: Boolean = true,
|
val extensionStores: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
@ -31,7 +31,7 @@ data class BackupOptions(
|
||||||
history,
|
history,
|
||||||
readEntries,
|
readEntries,
|
||||||
appSettings,
|
appSettings,
|
||||||
extensionRepoSettings,
|
extensionStores,
|
||||||
sourceSettings,
|
sourceSettings,
|
||||||
privateSettings,
|
privateSettings,
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
@ -41,7 +41,7 @@ data class BackupOptions(
|
||||||
)
|
)
|
||||||
|
|
||||||
fun canCreate() =
|
fun canCreate() =
|
||||||
libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearchesFeeds
|
libraryEntries || categories || appSettings || extensionStores || sourceSettings || savedSearchesFeeds
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val libraryOptions = persistentListOf(
|
val libraryOptions = persistentListOf(
|
||||||
|
|
@ -103,9 +103,9 @@ data class BackupOptions(
|
||||||
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.extensionRepo_settings,
|
label = MR.strings.extensionStores,
|
||||||
getter = BackupOptions::extensionRepoSettings,
|
getter = BackupOptions::extensionStores,
|
||||||
setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) },
|
setter = { options, enabled -> options.copy(extensionStores = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.source_settings,
|
label = MR.strings.source_settings,
|
||||||
|
|
@ -128,7 +128,7 @@ data class BackupOptions(
|
||||||
history = array[4],
|
history = array[4],
|
||||||
readEntries = array[5],
|
readEntries = array[5],
|
||||||
appSettings = array[6],
|
appSettings = array[6],
|
||||||
extensionRepoSettings = array[7],
|
extensionStores = array[7],
|
||||||
sourceSettings = array[8],
|
sourceSettings = array[8],
|
||||||
privateSettings = array[9],
|
privateSettings = array[9],
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
|
||||||
|
|
@ -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<BackupExtensionRepos> {
|
|
||||||
return getExtensionRepos.getAll()
|
|
||||||
.map(backupExtensionReposMapper)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<BackupExtensionStore> {
|
||||||
|
return getExtensionStores.get()
|
||||||
|
.map(backupExtensionStoreMapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ data class Backup(
|
||||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||||
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
||||||
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
||||||
@ProtoNumber(106) var backupExtensionRepo: List<BackupExtensionRepos> = emptyList(),
|
@ProtoNumber(106) var backupExtensionStores: List<BackupExtensionStore> = emptyList(),
|
||||||
// SY specific values
|
// SY specific values
|
||||||
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
|
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -5,14 +5,14 @@ import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupDecoder
|
import eu.kanade.tachiyomi.data.backup.BackupDecoder
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupFeed
|
import eu.kanade.tachiyomi.data.backup.models.BackupFeed
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
|
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
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.CategoriesRestorer
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionRepoRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionStoreRestorer
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.FeedRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.FeedRestorer
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
|
||||||
|
|
@ -38,7 +38,7 @@ class BackupRestorer(
|
||||||
|
|
||||||
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
||||||
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
||||||
private val extensionRepoRestorer: ExtensionRepoRestorer = ExtensionRepoRestorer(),
|
private val extensionStoreRestorer: ExtensionStoreRestorer = ExtensionStoreRestorer(),
|
||||||
private val mangaRestorer: MangaRestorer = MangaRestorer(isSync),
|
private val mangaRestorer: MangaRestorer = MangaRestorer(isSync),
|
||||||
// SY -->
|
// SY -->
|
||||||
private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(),
|
private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(),
|
||||||
|
|
@ -96,8 +96,8 @@ class BackupRestorer(
|
||||||
if (options.appSettings) {
|
if (options.appSettings) {
|
||||||
restoreAmount += 1
|
restoreAmount += 1
|
||||||
}
|
}
|
||||||
if (options.extensionRepoSettings) {
|
if (options.extensionStores) {
|
||||||
restoreAmount += backup.backupExtensionRepo.size
|
restoreAmount += backup.backupExtensionStores.size
|
||||||
}
|
}
|
||||||
if (options.sourceSettings) {
|
if (options.sourceSettings) {
|
||||||
restoreAmount += 1
|
restoreAmount += 1
|
||||||
|
|
@ -126,8 +126,8 @@ class BackupRestorer(
|
||||||
if (options.libraryEntries) {
|
if (options.libraryEntries) {
|
||||||
restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList())
|
restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList())
|
||||||
}
|
}
|
||||||
if (options.extensionRepoSettings) {
|
if (options.extensionStores) {
|
||||||
restoreExtensionRepos(backup.backupExtensionRepo)
|
restoreExtensionStores(backup.backupExtensionStores)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: optionally trigger online library + tracker update
|
// TODO: optionally trigger online library + tracker update
|
||||||
|
|
@ -248,23 +248,25 @@ class BackupRestorer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.restoreExtensionRepos(
|
private fun CoroutineScope.restoreExtensionStores(
|
||||||
backupExtensionRepo: List<BackupExtensionRepos>,
|
backupExtensionStores: List<BackupExtensionStore>,
|
||||||
) = launch {
|
) = launch {
|
||||||
backupExtensionRepo
|
backupExtensionStores
|
||||||
.forEach {
|
.forEach {
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
extensionRepoRestorer(it)
|
extensionStoreRestorer(it)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errors.add(Date() to "Error Adding Repo: ${it.name} : ${e.message}")
|
errors.add(Date() to "Error adding extension store: ${it.name} : ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
|
// KMK -->
|
||||||
with(notifier) {
|
with(notifier) {
|
||||||
|
// KMK <--
|
||||||
showRestoreProgress(
|
showRestoreProgress(
|
||||||
context.stringResource(MR.strings.extensionRepo_settings),
|
context.stringResource(MR.strings.extensionStores),
|
||||||
restoreProgress,
|
restoreProgress,
|
||||||
restoreAmount,
|
restoreAmount,
|
||||||
isSync,
|
isSync,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ data class RestoreOptions(
|
||||||
val libraryEntries: Boolean = true,
|
val libraryEntries: Boolean = true,
|
||||||
val categories: Boolean = true,
|
val categories: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val extensionRepoSettings: Boolean = true,
|
val extensionStores: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
// SY -->
|
// SY -->
|
||||||
val savedSearchesFeeds: Boolean = true,
|
val savedSearchesFeeds: Boolean = true,
|
||||||
|
|
@ -20,7 +20,7 @@ data class RestoreOptions(
|
||||||
libraryEntries,
|
libraryEntries,
|
||||||
categories,
|
categories,
|
||||||
appSettings,
|
appSettings,
|
||||||
extensionRepoSettings,
|
extensionStores,
|
||||||
sourceSettings,
|
sourceSettings,
|
||||||
// SY -->
|
// SY -->
|
||||||
savedSearchesFeeds,
|
savedSearchesFeeds,
|
||||||
|
|
@ -31,7 +31,7 @@ data class RestoreOptions(
|
||||||
libraryEntries ||
|
libraryEntries ||
|
||||||
categories ||
|
categories ||
|
||||||
appSettings ||
|
appSettings ||
|
||||||
extensionRepoSettings ||
|
extensionStores ||
|
||||||
sourceSettings /* SY --> */ ||
|
sourceSettings /* SY --> */ ||
|
||||||
savedSearchesFeeds /* SY <-- */
|
savedSearchesFeeds /* SY <-- */
|
||||||
|
|
||||||
|
|
@ -53,9 +53,9 @@ data class RestoreOptions(
|
||||||
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.extensionRepo_settings,
|
label = MR.strings.extensionStores,
|
||||||
getter = RestoreOptions::extensionRepoSettings,
|
getter = RestoreOptions::extensionStores,
|
||||||
setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) },
|
setter = { options, enabled -> options.copy(extensionStores = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.source_settings,
|
label = MR.strings.source_settings,
|
||||||
|
|
@ -77,7 +77,7 @@ data class RestoreOptions(
|
||||||
libraryEntries = array[0],
|
libraryEntries = array[0],
|
||||||
categories = array[1],
|
categories = array[1],
|
||||||
appSettings = array[2],
|
appSettings = array[2],
|
||||||
extensionRepoSettings = array[3],
|
extensionStores = array[3],
|
||||||
sourceSettings = array[4],
|
sourceSettings = array[4],
|
||||||
// SY -->
|
// SY -->
|
||||||
savedSearchesFeeds = array[5],
|
savedSearchesFeeds = array[5],
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
|
||||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
|
||||||
import tachiyomi.data.DatabaseHandler
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class ExtensionRepoRestorer(
|
|
||||||
private val handler: DatabaseHandler = Injekt.get(),
|
|
||||||
private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
backupRepo: BackupExtensionRepos,
|
|
||||||
) {
|
|
||||||
val dbRepos = getExtensionRepos.getAll()
|
|
||||||
val existingReposBySHA = dbRepos.associateBy { it.signingKeyFingerprint }
|
|
||||||
val existingReposByUrl = dbRepos.associateBy { it.baseUrl }
|
|
||||||
|
|
||||||
val urlExists = existingReposByUrl[backupRepo.baseUrl]
|
|
||||||
val shaExists = existingReposBySHA[backupRepo.signingKeyFingerprint]
|
|
||||||
|
|
||||||
if (urlExists != null && urlExists.signingKeyFingerprint != backupRepo.signingKeyFingerprint) {
|
|
||||||
error("Already Exists with different signing key fingerprint")
|
|
||||||
} else if (shaExists != null) {
|
|
||||||
error("${shaExists.name} has the same signing key fingerprint")
|
|
||||||
} else {
|
|
||||||
handler.await {
|
|
||||||
extension_reposQueries.insert(
|
|
||||||
backupRepo.baseUrl,
|
|
||||||
backupRepo.name,
|
|
||||||
backupRepo.shortName,
|
|
||||||
backupRepo.website,
|
|
||||||
backupRepo.signingKeyFingerprint,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -88,7 +88,7 @@ class SyncManager(
|
||||||
chapters = syncOptions.chapters,
|
chapters = syncOptions.chapters,
|
||||||
tracking = syncOptions.tracking,
|
tracking = syncOptions.tracking,
|
||||||
history = syncOptions.history,
|
history = syncOptions.history,
|
||||||
extensionRepoSettings = syncOptions.extensionRepoSettings,
|
extensionStores = syncOptions.extensionStores,
|
||||||
appSettings = syncOptions.appSettings,
|
appSettings = syncOptions.appSettings,
|
||||||
sourceSettings = syncOptions.sourceSettings,
|
sourceSettings = syncOptions.sourceSettings,
|
||||||
privateSettings = syncOptions.privateSettings,
|
privateSettings = syncOptions.privateSettings,
|
||||||
|
|
@ -107,8 +107,8 @@ class SyncManager(
|
||||||
backupCategories = backupCreator.backupCategories(backupOptions),
|
backupCategories = backupCreator.backupCategories(backupOptions),
|
||||||
backupSources = backupCreator.backupSources(backupManga),
|
backupSources = backupCreator.backupSources(backupManga),
|
||||||
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
|
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
|
||||||
backupExtensionRepo = backupCreator.backupExtensionRepos(backupOptions),
|
|
||||||
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
|
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
|
||||||
|
backupExtensionStores = backupCreator.backupExtensionStores(backupOptions),
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
backupSavedSearches = backupCreator.backupSavedSearches(backupOptions),
|
backupSavedSearches = backupCreator.backupSavedSearches(backupOptions),
|
||||||
|
|
@ -192,7 +192,7 @@ class SyncManager(
|
||||||
backupSources = remoteBackup.backupSources,
|
backupSources = remoteBackup.backupSources,
|
||||||
backupPreferences = remoteBackup.backupPreferences,
|
backupPreferences = remoteBackup.backupPreferences,
|
||||||
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
||||||
backupExtensionRepo = remoteBackup.backupExtensionRepo,
|
backupExtensionStores = remoteBackup.backupExtensionStores,
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
backupSavedSearches = remoteBackup.backupSavedSearches,
|
backupSavedSearches = remoteBackup.backupSavedSearches,
|
||||||
|
|
@ -222,7 +222,7 @@ class SyncManager(
|
||||||
appSettings = true,
|
appSettings = true,
|
||||||
sourceSettings = true,
|
sourceSettings = true,
|
||||||
libraryEntries = true,
|
libraryEntries = true,
|
||||||
extensionRepoSettings = true,
|
extensionStores = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -273,14 +273,14 @@ class ExtensionManager(
|
||||||
// Ext found: Update installed extensions with new information from repo
|
// Ext found: Update installed extensions with new information from repo
|
||||||
// Also clear isObsolete and set new repo Name if needed
|
// Also clear isObsolete and set new repo Name if needed
|
||||||
val hasUpdate = extension.updateExists(availableExt)
|
val hasUpdate = extension.updateExists(availableExt)
|
||||||
|
// KMK -->
|
||||||
installedExtensionsMap[pkgName] = extension.copy(
|
installedExtensionsMap[pkgName] = extension.copy(
|
||||||
hasUpdate = hasUpdate,
|
hasUpdate = hasUpdate,
|
||||||
repoUrl = availableExt.repoUrl,
|
store = availableExt.store,
|
||||||
// KMK -->
|
|
||||||
isObsolete = false,
|
isObsolete = false,
|
||||||
repoName = extension.repoName ?: availableExt.repoName,
|
storeName = extension.storeName ?: availableExt.storeName,
|
||||||
// KMK <--
|
|
||||||
)
|
)
|
||||||
|
// KMK <--
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -298,7 +298,7 @@ class ExtensionManager(
|
||||||
* @param extension The extension to be installed.
|
* @param extension The extension to be installed.
|
||||||
*/
|
*/
|
||||||
fun installExtension(extension: Extension.Available): Flow<InstallStep> {
|
fun installExtension(extension: Extension.Available): Flow<InstallStep> {
|
||||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
return installer.downloadAndInstall(extension.apkUrl, extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,13 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
|
||||||
import exh.source.BlacklistedSources
|
import exh.source.BlacklistedSources
|
||||||
import exh.source.ExhPreferences
|
import exh.source.ExhPreferences
|
||||||
import kotlinx.coroutines.async
|
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||||
import kotlinx.coroutines.awaitAll
|
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import logcat.LogPriority
|
|
||||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.system.logcat
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
@ -32,19 +21,16 @@ import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
internal class ExtensionApi {
|
internal class ExtensionApi {
|
||||||
|
|
||||||
private val networkService: NetworkHelper by injectLazy()
|
private val repository: ExtensionStoreRepository by injectLazy()
|
||||||
|
|
||||||
private val preferenceStore: PreferenceStore by injectLazy()
|
private val preferenceStore: PreferenceStore by injectLazy()
|
||||||
private val getExtensionRepo: GetExtensionRepo by injectLazy()
|
private val updateExtensionStores: UpdateExtensionStores by injectLazy()
|
||||||
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
|
|
||||||
private val extensionManager: ExtensionManager by injectLazy()
|
private val extensionManager: ExtensionManager by injectLazy()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private val sourcePreferences: SourcePreferences by injectLazy()
|
private val sourcePreferences: SourcePreferences by injectLazy()
|
||||||
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
private val lastExtCheck: Preference<Long> by lazy {
|
private val lastExtCheck: Preference<Long> by lazy {
|
||||||
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
|
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
|
||||||
}
|
}
|
||||||
|
|
@ -54,37 +40,11 @@ internal class ExtensionApi {
|
||||||
val disabledRepos = sourcePreferences.disabledRepos().get()
|
val disabledRepos = sourcePreferences.disabledRepos().get()
|
||||||
// KMK <--
|
// KMK <--
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
getExtensionRepo.getAll()
|
repository.fetchExtensions(
|
||||||
// KMK -->
|
// KMK -->
|
||||||
.filterNot { it.baseUrl in disabledRepos }
|
disabledRepos,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
.map { async { getExtensions(it) } }
|
)
|
||||||
.awaitAll()
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getExtensions(extRepo: ExtensionRepo): List<Extension.Available> {
|
|
||||||
val repoBaseUrl = extRepo.baseUrl
|
|
||||||
return try {
|
|
||||||
val response = networkService.client
|
|
||||||
.newCall(GET("$repoBaseUrl/index.min.json"))
|
|
||||||
.awaitSuccess()
|
|
||||||
|
|
||||||
with(json) {
|
|
||||||
response
|
|
||||||
.parseAs<List<ExtensionJsonObject>>()
|
|
||||||
.toExtensions(
|
|
||||||
repoBaseUrl,
|
|
||||||
// KMK -->
|
|
||||||
signature = extRepo.signingKeyFingerprint,
|
|
||||||
repoName = extRepo.shortName ?: extRepo.name,
|
|
||||||
// KMK <--
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" }
|
|
||||||
emptyList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,8 +59,7 @@ internal class ExtensionApi {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update extension repo details
|
updateExtensionStores()
|
||||||
updateExtensionRepo.awaitAll()
|
|
||||||
|
|
||||||
val extensions = if (fromAvailableExtensionList) {
|
val extensions = if (fromAvailableExtensionList) {
|
||||||
extensionManager.availableExtensionsFlow.value
|
extensionManager.availableExtensionsFlow.value
|
||||||
|
|
@ -138,47 +97,6 @@ internal class ExtensionApi {
|
||||||
return extensionsWithUpdate
|
return extensionsWithUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<ExtensionJsonObject>.toExtensions(
|
|
||||||
repoUrl: String,
|
|
||||||
// KMK -->
|
|
||||||
signature: String,
|
|
||||||
repoName: String,
|
|
||||||
// KMK <--
|
|
||||||
): List<Extension.Available> {
|
|
||||||
return this
|
|
||||||
.filter {
|
|
||||||
val libVersion = it.extractLibVersion()
|
|
||||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
Extension.Available(
|
|
||||||
name = it.name.substringAfter("Tachiyomi: "),
|
|
||||||
pkgName = it.pkg,
|
|
||||||
versionName = it.version,
|
|
||||||
versionCode = it.code,
|
|
||||||
libVersion = it.extractLibVersion(),
|
|
||||||
lang = it.lang,
|
|
||||||
isNsfw = it.nsfw == 1,
|
|
||||||
sources = it.sources?.map(extensionSourceMapper).orEmpty(),
|
|
||||||
apkName = it.apk,
|
|
||||||
iconUrl = "$repoUrl/icon/${it.pkg}.png",
|
|
||||||
repoUrl = repoUrl,
|
|
||||||
// KMK -->
|
|
||||||
signatureHash = signature,
|
|
||||||
repoName = repoName,
|
|
||||||
// KMK <--
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getApkUrl(extension: Extension.Available): String {
|
|
||||||
return "${extension.repoUrl}/apk/${extension.apkName}"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ExtensionJsonObject.extractLibVersion(): Double {
|
|
||||||
return version.substringBeforeLast('.').toDouble()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private fun Extension.isBlacklisted(
|
private fun Extension.isBlacklisted(
|
||||||
blacklistEnabled: Boolean = sourcePreferences.enableSourceBlacklist().get(),
|
blacklistEnabled: Boolean = sourcePreferences.enableSourceBlacklist().get(),
|
||||||
|
|
@ -194,32 +112,3 @@ internal class ExtensionApi {
|
||||||
}
|
}
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class ExtensionJsonObject(
|
|
||||||
val name: String,
|
|
||||||
val pkg: String,
|
|
||||||
val apk: String,
|
|
||||||
val lang: String,
|
|
||||||
val code: Long,
|
|
||||||
val version: String,
|
|
||||||
val nsfw: Int,
|
|
||||||
val sources: List<ExtensionSourceJsonObject>?,
|
|
||||||
)
|
|
||||||
|
|
||||||
@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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
import mihon.domain.extension.interactor.GetExtensionStores
|
||||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -46,7 +46,7 @@ internal object ExtensionLoader {
|
||||||
private val trustExtension: TrustExtension by injectLazy()
|
private val trustExtension: TrustExtension by injectLazy()
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
private val getExtensionRepo: GetExtensionRepo by injectLazy()
|
private val getExtensionStores: GetExtensionStores by injectLazy()
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
private val loadNsfwSource by lazy {
|
private val loadNsfwSource by lazy {
|
||||||
|
|
@ -169,7 +169,7 @@ internal object ExtensionLoader {
|
||||||
// Load each extension concurrently and wait for completion
|
// Load each extension concurrently and wait for completion
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
val extRepos = getExtensionRepo.getAll()
|
val extStores = getExtensionStores.get()
|
||||||
// KMK <--
|
// KMK <--
|
||||||
val deferred = extPkgs.map {
|
val deferred = extPkgs.map {
|
||||||
async {
|
async {
|
||||||
|
|
@ -177,7 +177,7 @@ internal object ExtensionLoader {
|
||||||
context,
|
context,
|
||||||
it,
|
it,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
extRepos,
|
extStores,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -228,7 +228,7 @@ internal object ExtensionLoader {
|
||||||
isShared = true,
|
isShared = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (error: PackageManager.NameNotFoundException) {
|
} catch (_: PackageManager.NameNotFoundException) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,11 +245,11 @@ internal object ExtensionLoader {
|
||||||
context: Context,
|
context: Context,
|
||||||
extensionInfo: ExtensionInfo,
|
extensionInfo: ExtensionInfo,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
extRepos: List<ExtensionRepo>? = null,
|
extStores: List<ExtensionStore>? = null,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
): LoadResult {
|
): LoadResult {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
val repos = extRepos ?: getExtensionRepo.getAll()
|
val stores = extStores ?: getExtensionStores.get()
|
||||||
// KMK <--
|
// KMK <--
|
||||||
val pkgManager = context.packageManager
|
val pkgManager = context.packageManager
|
||||||
val pkgInfo = extensionInfo.packageInfo
|
val pkgInfo = extensionInfo.packageInfo
|
||||||
|
|
@ -288,10 +288,10 @@ internal object ExtensionLoader {
|
||||||
libVersion,
|
libVersion,
|
||||||
signatures.last(),
|
signatures.last(),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
repoName = repos.firstOrNull { repo ->
|
storeName = stores.firstOrNull { store ->
|
||||||
signatures.all { it == repo.signingKeyFingerprint }
|
signatures.all { it == store.signingKey }
|
||||||
}?.let { repo ->
|
}?.let { store ->
|
||||||
repo.shortName.takeIf { !it.isNullOrBlank() } ?: repo.name
|
store.badgeLabel.takeIf(String::isNotBlank) ?: store.name
|
||||||
},
|
},
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
|
|
@ -358,10 +358,10 @@ internal object ExtensionLoader {
|
||||||
isShared = extensionInfo.isShared,
|
isShared = extensionInfo.isShared,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
signatureHash = signatures.last(),
|
signatureHash = signatures.last(),
|
||||||
repoName = repos.firstOrNull { repo ->
|
storeName = stores.firstOrNull { store ->
|
||||||
signatures.all { it == repo.signingKeyFingerprint }
|
signatures.all { it == store.signingKey }
|
||||||
}?.let { repo ->
|
}?.let { store ->
|
||||||
repo.shortName.takeIf { !it.isNullOrBlank() } ?: repo.name
|
store.badgeLabel.takeIf(String::isNotBlank) ?: store.name
|
||||||
},
|
},
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class ExtensionsScreen(private val searchSource: String? = null) : Screen() {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This will redo the searching for [searchSource] every times the screen is launched, for example when
|
* This will redo the searching for [searchSource] every times the screen is launched, for example when
|
||||||
* back from the [ExtensionFilterScreen] or from the [ExtensionReposScreen].
|
* back from the [ExtensionFilterScreen] or from the [ExtensionStoresScreen].
|
||||||
* Not really desired but let's accept it.
|
* Not really desired but let's accept it.
|
||||||
*/
|
*/
|
||||||
if (!searchSource.isNullOrBlank()) onChangeSearchQuery(searchSource)
|
if (!searchSource.isNullOrBlank()) onChangeSearchQuery(searchSource)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.browse.ExtensionScreen
|
import eu.kanade.presentation.browse.ExtensionScreen
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.TabContent
|
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.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreen
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
import eu.kanade.tachiyomi.ui.webview.WebViewScreen
|
||||||
|
|
@ -62,8 +62,8 @@ fun extensionsTab(
|
||||||
onClick = { navigator.push(ExtensionFilterScreen()) },
|
onClick = { navigator.push(ExtensionFilterScreen()) },
|
||||||
),
|
),
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(MR.strings.label_extension_repos),
|
title = stringResource(MR.strings.extensionStores),
|
||||||
onClick = { navigator.push(ExtensionReposScreen()) },
|
onClick = { navigator.push(ExtensionStoresScreen()) },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
content = { contentPadding, _ ->
|
content = { contentPadding, _ ->
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ import eu.kanade.presentation.components.UpdatingBannerBackgroundColor
|
||||||
import eu.kanade.presentation.more.settings.screen.ConfigureExhDialog
|
import eu.kanade.presentation.more.settings.screen.ConfigureExhDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.about.AboutScreen.Companion.getReleaseNotes
|
import eu.kanade.presentation.more.settings.screen.about.AboutScreen.Companion.getReleaseNotes
|
||||||
import eu.kanade.presentation.more.settings.screen.about.WhatsNewDialog
|
import eu.kanade.presentation.more.settings.screen.about.WhatsNewDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
||||||
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||||
import eu.kanade.presentation.util.AssistContentScreen
|
import eu.kanade.presentation.util.AssistContentScreen
|
||||||
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
|
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
|
||||||
|
|
@ -693,11 +693,11 @@ class MainActivity : BaseActivity() {
|
||||||
navigator.popUntilRoot()
|
navigator.popUntilRoot()
|
||||||
navigator.push(RestoreBackupScreen(intent.data.toString()))
|
navigator.push(RestoreBackupScreen(intent.data.toString()))
|
||||||
}
|
}
|
||||||
// Deep link to add extension repo
|
// Deep link to add extension store
|
||||||
else if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") {
|
else if (intent.isAddExtensionStoreIntent()) {
|
||||||
intent.data?.getQueryParameter("url")?.let { repoUrl ->
|
intent.data?.getQueryParameter("url")?.let { repoUrl ->
|
||||||
navigator.popUntilRoot()
|
navigator.popUntilRoot()
|
||||||
navigator.push(ExtensionReposScreen(repoUrl))
|
navigator.push(ExtensionStoresScreen(repoUrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
|
|
@ -713,6 +713,11 @@ class MainActivity : BaseActivity() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Intent.isAddExtensionStoreIntent(): Boolean {
|
||||||
|
return (scheme == "tachiyomi" && data?.host == "add-repo") ||
|
||||||
|
(scheme == "mihon" && data?.host == "extension-store")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
|
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
|
||||||
const val INTENT_SEARCH_QUERY = "query"
|
const val INTENT_SEARCH_QUERY = "query"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package mihon.core.migration.migrations
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import mihon.core.migration.Migration
|
||||||
|
import mihon.core.migration.MigrationContext
|
||||||
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
|
|
||||||
|
class DisabledRepoMigration : Migration {
|
||||||
|
override val version: Float = 80f
|
||||||
|
|
||||||
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
||||||
|
val context = migrationContext.get<Application>() ?: return@withIOContext false
|
||||||
|
val sourcePreferences = migrationContext.get<SourcePreferences>() ?: return@withIOContext false
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
prefs.edit {
|
||||||
|
val disabledRepos = prefs.getStringSet(sourcePreferences.disabledRepos().key(), emptySet()) ?: return@edit
|
||||||
|
disabledRepos
|
||||||
|
.map {
|
||||||
|
it.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json"
|
||||||
|
}.toSet()
|
||||||
|
.let {
|
||||||
|
putStringSet(sourcePreferences.disabledRepos().key(), it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@withIOContext true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,4 +52,7 @@ val migrations: List<Migration>
|
||||||
TrustExtensionRepositoryMigration(),
|
TrustExtensionRepositoryMigration(),
|
||||||
CategoryPreferencesCleanupMigration(),
|
CategoryPreferencesCleanupMigration(),
|
||||||
RemoveDuplicateReaderPreferenceMigration(),
|
RemoveDuplicateReaderPreferenceMigration(),
|
||||||
|
// KMK -->
|
||||||
|
DisabledRepoMigration(),
|
||||||
|
// KMK <--
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.core.migration.Migration
|
import mihon.core.migration.Migration
|
||||||
import mihon.core.migration.MigrationContext
|
import mihon.core.migration.MigrationContext
|
||||||
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
|
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
|
||||||
|
|
@ -14,18 +13,14 @@ class TrustExtensionRepositoryMigration : Migration {
|
||||||
|
|
||||||
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
override suspend fun invoke(migrationContext: MigrationContext): Boolean = withIOContext {
|
||||||
val sourcePreferences = migrationContext.get<SourcePreferences>() ?: return@withIOContext false
|
val sourcePreferences = migrationContext.get<SourcePreferences>() ?: return@withIOContext false
|
||||||
val extensionRepositoryRepository =
|
val repository = migrationContext.get<ExtensionStoreRepository>() ?: return@withIOContext false
|
||||||
migrationContext.get<ExtensionRepoRepository>() ?: return@withIOContext false
|
|
||||||
for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) {
|
for ((index, source) in sourcePreferences.extensionRepos().get().withIndex()) {
|
||||||
try {
|
try {
|
||||||
extensionRepositoryRepository.upsertRepo(
|
repository.insertFromPreference(
|
||||||
source,
|
indexUrl = source.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json",
|
||||||
"Repo #${index + 1}",
|
name = "Repo #${index + 1}",
|
||||||
null,
|
|
||||||
source,
|
|
||||||
"NOFINGERPRINT-${index + 1}",
|
|
||||||
)
|
)
|
||||||
} catch (e: SaveExtensionRepoException) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" }
|
logcat(LogPriority.ERROR, e) { "Error Migrating Extension Repo with baseUrl: $source" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ android {
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
freeCompilerArgs.add("-opt-in=kotlinx.serialization.ExperimentalSerializationApi")
|
optIn.add("kotlinx.serialization.ExperimentalSerializationApi")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,5 +34,9 @@ dependencies {
|
||||||
implementation(projects.domain)
|
implementation(projects.domain)
|
||||||
implementation(projects.core.common)
|
implementation(projects.core.common)
|
||||||
|
|
||||||
|
implementation(kotlinx.serialization.json)
|
||||||
|
implementation(kotlinx.serialization.json.okio)
|
||||||
|
implementation(kotlinx.serialization.protobuf)
|
||||||
|
|
||||||
api(libs.bundles.sqldelight)
|
api(libs.bundles.sqldelight)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package mihon.data.extension.model
|
||||||
|
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
|
||||||
|
interface BaseNetworkExtensionStore {
|
||||||
|
fun toExtensionStore(indexUrl: String): ExtensionStore
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
package mihon.data.extension.model
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonNames
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
@Serializable
|
||||||
|
data class NetworkExtensionStore(
|
||||||
|
@ProtoNumber(1) val name: String,
|
||||||
|
@ProtoNumber(2) val badgeLabel: String,
|
||||||
|
@ProtoNumber(3) val signingKey: String,
|
||||||
|
@ProtoNumber(4) val contact: Contact,
|
||||||
|
@ProtoNumber(5) val extensions: List<Extension>,
|
||||||
|
) : 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<Source>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@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<String> = 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<eu.kanade.tachiyomi.extension.model.Extension.Available> {
|
||||||
|
return extensions.map { extension ->
|
||||||
|
val lang = extension.sources.map { it.language }.toSet()
|
||||||
|
eu.kanade.tachiyomi.extension.model.Extension.Available(
|
||||||
|
name = extension.name,
|
||||||
|
pkgName = extension.packageName,
|
||||||
|
apkUrl = extension.resources.apkUrl,
|
||||||
|
iconUrl = extension.resources.iconUrl,
|
||||||
|
libVersion = extension.extensionLib.toDouble(),
|
||||||
|
versionCode = extension.versionCode,
|
||||||
|
versionName = extension.versionName,
|
||||||
|
lang = if (lang.size == 1) lang.first() else "all",
|
||||||
|
isNsfw = extension.sources.maxOfOrNull { it.contentRating } == ContentRating.PORNOGRAPHIC,
|
||||||
|
// KMK -->
|
||||||
|
signatureHash = store.signingKey,
|
||||||
|
storeName = store.name,
|
||||||
|
// KMK <--
|
||||||
|
sources = extension.sources.map { source ->
|
||||||
|
eu.kanade.tachiyomi.extension.model.Extension.Available.Source(
|
||||||
|
id = source.id,
|
||||||
|
name = source.name,
|
||||||
|
lang = source.language,
|
||||||
|
baseUrl = source.homeUrl,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
store = store,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package mihon.data.extension.model
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
@Serializable
|
||||||
|
data class NetworkLegacyExtension(
|
||||||
|
val name: String,
|
||||||
|
val pkg: String,
|
||||||
|
val apk: String,
|
||||||
|
val lang: String,
|
||||||
|
val code: Long,
|
||||||
|
val version: String,
|
||||||
|
val nsfw: Int,
|
||||||
|
val sources: List<Source>?,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class Source(
|
||||||
|
val id: Long,
|
||||||
|
val lang: String,
|
||||||
|
val name: String,
|
||||||
|
val baseUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toAvailableExtension(store: ExtensionStore, storeBaseUrl: String): Extension.Available {
|
||||||
|
return Extension.Available(
|
||||||
|
name = name.substringAfter("Tachiyomi: "),
|
||||||
|
pkgName = pkg,
|
||||||
|
apkUrl = "$storeBaseUrl/apk/$apk",
|
||||||
|
iconUrl = "$storeBaseUrl/icon/$pkg.png",
|
||||||
|
libVersion = version.substringBeforeLast('.').toDouble(),
|
||||||
|
versionCode = code,
|
||||||
|
versionName = version,
|
||||||
|
lang = lang,
|
||||||
|
isNsfw = nsfw == 1,
|
||||||
|
// KMK -->
|
||||||
|
signatureHash = store.signingKey,
|
||||||
|
storeName = store.name,
|
||||||
|
// KMK <--
|
||||||
|
sources = if (sources.isNullOrEmpty()) {
|
||||||
|
listOf(
|
||||||
|
Extension.Available.Source(
|
||||||
|
id = 0,
|
||||||
|
name = name,
|
||||||
|
lang = lang,
|
||||||
|
baseUrl = "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sources.map { source ->
|
||||||
|
Extension.Available.Source(
|
||||||
|
id = source.id,
|
||||||
|
name = source.name,
|
||||||
|
lang = source.lang,
|
||||||
|
baseUrl = source.baseUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
store = store,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
package mihon.data.extension.repository
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import logcat.LogPriority
|
||||||
|
import mihon.data.extension.service.ExtensionStoreService
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||||
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
import tachiyomi.data.DatabaseHandler
|
||||||
|
|
||||||
|
class ExtensionStoreRepositoryImpl(
|
||||||
|
private val service: ExtensionStoreService,
|
||||||
|
private val handler: DatabaseHandler,
|
||||||
|
) : ExtensionStoreRepository {
|
||||||
|
override suspend fun insert(indexUrl: String): Result<Unit> {
|
||||||
|
return service.fetch(indexUrl).mapCatching { upsert(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun insertFromPreference(indexUrl: String, name: String) {
|
||||||
|
handler.await {
|
||||||
|
extension_storeQueries.upsert(
|
||||||
|
indexUrl = indexUrl,
|
||||||
|
name = name,
|
||||||
|
badgeLabel = name,
|
||||||
|
signingKey = "NO_SIGNING_KEY",
|
||||||
|
contactWebsite = indexUrl,
|
||||||
|
contactDiscord = null,
|
||||||
|
isLegacy = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun refreshAll() {
|
||||||
|
try {
|
||||||
|
handler.awaitList {
|
||||||
|
extension_storeQueries.getAll()
|
||||||
|
}.forEach { store ->
|
||||||
|
service.fetch(store.index_url)
|
||||||
|
.mapCatching { upsert(it) }
|
||||||
|
.onFailure {
|
||||||
|
logcat(LogPriority.ERROR, it) {
|
||||||
|
"Failed to refresh extension store '${store.name} (${store.index_url})'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun upsert(store: ExtensionStore) {
|
||||||
|
handler.await {
|
||||||
|
extension_storeQueries.upsert(
|
||||||
|
indexUrl = store.indexUrl,
|
||||||
|
name = store.name,
|
||||||
|
badgeLabel = store.badgeLabel,
|
||||||
|
signingKey = store.signingKey,
|
||||||
|
contactWebsite = store.contact.website,
|
||||||
|
contactDiscord = store.contact.discord,
|
||||||
|
isLegacy = store.isLegacy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchExtensions(
|
||||||
|
// KMK -->
|
||||||
|
disabledRepos: Set<String>,
|
||||||
|
// KMK <--
|
||||||
|
): List<Extension.Available> {
|
||||||
|
return try {
|
||||||
|
supervisorScope {
|
||||||
|
handler.awaitList {
|
||||||
|
extension_storeQueries.getAll(::extensionStoreMapper)
|
||||||
|
}
|
||||||
|
// KMK -->
|
||||||
|
.filterNot { it.indexUrl in disabledRepos }
|
||||||
|
// KMK <--
|
||||||
|
.map { store ->
|
||||||
|
async {
|
||||||
|
service.getExtensions(store).onFailure {
|
||||||
|
this@ExtensionStoreRepositoryImpl.logcat(LogPriority.ERROR, it) {
|
||||||
|
"Failed to fetch extensions for store '${store.name} (${store.indexUrl})'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.flatMap { it.getOrDefault(emptyList()) }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAll(): List<ExtensionStore> {
|
||||||
|
return handler.awaitList { extension_storeQueries.getAll(::extensionStoreMapper) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllAsFlow(): Flow<List<ExtensionStore>> {
|
||||||
|
return handler.subscribeToList { extension_storeQueries.getAll(::extensionStoreMapper) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCountAsFlow(): Flow<Long> {
|
||||||
|
return handler.subscribeToOne {
|
||||||
|
extension_storeQueries
|
||||||
|
.getCount()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun remove(indexUrl: String) {
|
||||||
|
handler.await { extension_storeQueries.delete(indexUrl) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extensionStoreMapper(
|
||||||
|
indexUrl: String,
|
||||||
|
name: String,
|
||||||
|
badgeLabel: String,
|
||||||
|
signingKey: String,
|
||||||
|
contactWebsite: String,
|
||||||
|
contactDiscord: String?,
|
||||||
|
isLegacy: Boolean,
|
||||||
|
): ExtensionStore = ExtensionStore(
|
||||||
|
indexUrl = indexUrl,
|
||||||
|
name = name,
|
||||||
|
badgeLabel = badgeLabel,
|
||||||
|
signingKey = signingKey,
|
||||||
|
contact = ExtensionStore.Contact(
|
||||||
|
website = contactWebsite,
|
||||||
|
discord = contactDiscord,
|
||||||
|
),
|
||||||
|
isLegacy = isLegacy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package mihon.data.extension.service
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
|
import kotlinx.serialization.decodeFromByteArray
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import logcat.LogPriority
|
||||||
|
import mihon.data.extension.model.NetworkExtensionStore
|
||||||
|
import mihon.data.extension.model.NetworkLegacyExtension
|
||||||
|
import mihon.data.extension.model.NetworkLegacyExtensionRepo
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
class ExtensionStoreService(
|
||||||
|
private val network: NetworkHelper,
|
||||||
|
private val json: Json,
|
||||||
|
private val protoBuf: ProtoBuf,
|
||||||
|
) {
|
||||||
|
suspend fun fetch(indexUrl: String): Result<ExtensionStore> {
|
||||||
|
var updatedIndexUrl: String = indexUrl
|
||||||
|
return try {
|
||||||
|
val store = network.client.newCall(GET(indexUrl)).awaitSuccess().body.source().use { source ->
|
||||||
|
try {
|
||||||
|
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||||
|
// KMK -->
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
// KMK <--
|
||||||
|
logcat(LogPriority.ERROR, e) {
|
||||||
|
"Failed to add extension store '$updatedIndexUrl'"
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
||||||
|
// KMK -->
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
// KMK <--
|
||||||
|
logcat(LogPriority.ERROR, e) {
|
||||||
|
"Failed to add extension store '$updatedIndexUrl'"
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(source.peek())
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
if (!indexUrl.endsWith("/index.min.json")) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
logcat(LogPriority.ERROR, e) {
|
||||||
|
"Failed to add extension store '$updatedIndexUrl'"
|
||||||
|
}
|
||||||
|
updatedIndexUrl = indexUrl.replace("/index.min.json", "/repo.json")
|
||||||
|
network.client.newCall(GET(updatedIndexUrl)).awaitSuccess().use {
|
||||||
|
json.decodeFromBufferedSource<NetworkLegacyExtensionRepo>(it.body.source())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toExtensionStore(updatedIndexUrl)
|
||||||
|
}
|
||||||
|
Result.success(store)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e) {
|
||||||
|
"Failed to add extension store '$updatedIndexUrl'"
|
||||||
|
}
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getExtensions(store: ExtensionStore): Result<List<Extension.Available>> {
|
||||||
|
return try {
|
||||||
|
val extensions = if (!store.isLegacy) {
|
||||||
|
network.client.newCall(GET(store.indexUrl)).awaitSuccess().use { response ->
|
||||||
|
val source = response.body.source()
|
||||||
|
try {
|
||||||
|
protoBuf.decodeFromByteArray<NetworkExtensionStore>(source.peek().readByteArray())
|
||||||
|
.toAvailableExtensions(store)
|
||||||
|
// KMK -->
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
// KMK <--
|
||||||
|
json.decodeFromBufferedSource<NetworkExtensionStore>(source.peek())
|
||||||
|
.toAvailableExtensions(store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val storeBaseUrl = store.indexUrl.removeSuffix("/repo.json")
|
||||||
|
network.client.newCall(GET("$storeBaseUrl/index.min.json")).awaitSuccess().use { response ->
|
||||||
|
val source = response.body.source()
|
||||||
|
json.decodeFromBufferedSource<List<NetworkLegacyExtension>>(source)
|
||||||
|
.map { it.toAvailableExtension(store, storeBaseUrl) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result.success(extensions)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
package mihon.data.repository
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteException
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
|
|
||||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
|
||||||
import tachiyomi.data.DatabaseHandler
|
|
||||||
|
|
||||||
class ExtensionRepoRepositoryImpl(
|
|
||||||
private val handler: DatabaseHandler,
|
|
||||||
) : ExtensionRepoRepository {
|
|
||||||
override fun subscribeAll(): Flow<List<ExtensionRepo>> {
|
|
||||||
return handler.subscribeToList { extension_reposQueries.findAll(::mapExtensionRepo) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getAll(): List<ExtensionRepo> {
|
|
||||||
return handler.awaitList { extension_reposQueries.findAll(::mapExtensionRepo) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getRepo(baseUrl: String): ExtensionRepo? {
|
|
||||||
return handler.awaitOneOrNull { extension_reposQueries.findOne(baseUrl, ::mapExtensionRepo) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo? {
|
|
||||||
return handler.awaitOneOrNull {
|
|
||||||
extension_reposQueries.findOneBySigningKeyFingerprint(fingerprint, ::mapExtensionRepo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCount(): Flow<Int> {
|
|
||||||
return handler.subscribeToOne { extension_reposQueries.count() }.map { it.toInt() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun insertRepo(
|
|
||||||
baseUrl: String,
|
|
||||||
name: String,
|
|
||||||
shortName: String?,
|
|
||||||
website: String,
|
|
||||||
signingKeyFingerprint: String,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
handler.await { extension_reposQueries.insert(baseUrl, name, shortName, website, signingKeyFingerprint) }
|
|
||||||
} catch (ex: SQLiteException) {
|
|
||||||
throw SaveExtensionRepoException(ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun upsertRepo(
|
|
||||||
baseUrl: String,
|
|
||||||
name: String,
|
|
||||||
shortName: String?,
|
|
||||||
website: String,
|
|
||||||
signingKeyFingerprint: String,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
handler.await { extension_reposQueries.upsert(baseUrl, name, shortName, website, signingKeyFingerprint) }
|
|
||||||
} catch (ex: SQLiteException) {
|
|
||||||
throw SaveExtensionRepoException(ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun replaceRepo(newRepo: ExtensionRepo) {
|
|
||||||
handler.await {
|
|
||||||
extension_reposQueries.replace(
|
|
||||||
newRepo.baseUrl,
|
|
||||||
newRepo.name,
|
|
||||||
newRepo.shortName,
|
|
||||||
newRepo.website,
|
|
||||||
newRepo.signingKeyFingerprint,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun deleteRepo(baseUrl: String) {
|
|
||||||
return handler.await { extension_reposQueries.delete(baseUrl) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapExtensionRepo(
|
|
||||||
baseUrl: String,
|
|
||||||
name: String,
|
|
||||||
shortName: String?,
|
|
||||||
website: String,
|
|
||||||
signingKeyFingerprint: String,
|
|
||||||
): ExtensionRepo = ExtensionRepo(
|
|
||||||
baseUrl = baseUrl,
|
|
||||||
name = name,
|
|
||||||
shortName = shortName,
|
|
||||||
website = website,
|
|
||||||
signingKeyFingerprint = signingKeyFingerprint,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
42
data/src/main/sqldelight/tachiyomi/data/extension_store.sq
Normal file
42
data/src/main/sqldelight/tachiyomi/data/extension_store.sq
Normal file
|
|
@ -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;
|
||||||
16
data/src/main/sqldelight/tachiyomi/migrations/46.sqm
Normal file
16
data/src/main/sqldelight/tachiyomi/migrations/46.sqm
Normal file
|
|
@ -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;
|
||||||
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.model
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
|
|
||||||
sealed class Extension {
|
sealed class Extension {
|
||||||
|
|
@ -16,7 +17,7 @@ sealed class Extension {
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
abstract val signatureHash: String
|
abstract val signatureHash: String
|
||||||
abstract val repoName: String?
|
abstract val storeName: String?
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
data class Installed(
|
data class Installed(
|
||||||
|
|
@ -29,8 +30,8 @@ sealed class Extension {
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
override val signatureHash: String,
|
override val signatureHash: String,
|
||||||
/** Guessing repo name from built-in signatures preset */
|
/** Guessing store name from built-in signatures preset */
|
||||||
override val repoName: String? = null,
|
override val storeName: String? = null,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
val pkgFactory: String?,
|
val pkgFactory: String?,
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
|
|
@ -38,7 +39,7 @@ sealed class Extension {
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
val isShared: Boolean,
|
val isShared: Boolean,
|
||||||
val repoUrl: String? = null,
|
val store: ExtensionStore? = null,
|
||||||
// SY -->
|
// SY -->
|
||||||
val isRedundant: Boolean = false,
|
val isRedundant: Boolean = false,
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
@ -54,12 +55,12 @@ sealed class Extension {
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
override val signatureHash: String,
|
override val signatureHash: String,
|
||||||
override val repoName: String,
|
override val storeName: String,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
val apkName: String,
|
val apkUrl: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val repoUrl: String,
|
val store: ExtensionStore,
|
||||||
) : Extension() {
|
) : Extension() {
|
||||||
|
|
||||||
data class Source(
|
data class Source(
|
||||||
|
|
@ -87,7 +88,7 @@ sealed class Extension {
|
||||||
/* KMK --> */
|
/* KMK --> */
|
||||||
override /* KMK <-- */ val signatureHash: String,
|
override /* KMK <-- */ val signatureHash: String,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
override val repoName: String? = null,
|
override val storeName: String? = null,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
override val lang: String? = null,
|
override val lang: String? = null,
|
||||||
override val isNsfw: Boolean = false,
|
override val isNsfw: Boolean = false,
|
||||||
|
|
@ -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<Unit> {
|
||||||
|
return repository.insert(indexUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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<ExtensionStore> = repository.getAll()
|
||||||
|
|
||||||
|
fun subscribe(): Flow<List<ExtensionStore>> = repository.getAllAsFlow()
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package mihon.domain.extension.model
|
||||||
|
|
||||||
|
data class ExtensionStore(
|
||||||
|
val indexUrl: String,
|
||||||
|
val name: String,
|
||||||
|
val badgeLabel: String,
|
||||||
|
val signingKey: String,
|
||||||
|
val contact: Contact,
|
||||||
|
val isLegacy: Boolean,
|
||||||
|
) {
|
||||||
|
data class Contact(
|
||||||
|
val website: String,
|
||||||
|
val discord: String?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const val REPO_HELP = "https://komikku-app.github.io/docs/guides/getting-started#adding-sources"
|
||||||
|
|
||||||
|
// cuong-tran's key
|
||||||
|
const val KOMIKKU_SIGNATURE = "cbec121aa82ebb02aaa73806992e0368a97d47b5451ed6524816d03084c45905"
|
||||||
|
const val REPO_SIGNATURE = "9add655a78e96c4ec7a53ef89dccb557cb5d767489fac5e785d671a5a75d4da2"
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package mihon.domain.extension.repository
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
|
||||||
|
interface ExtensionStoreRepository {
|
||||||
|
suspend fun insert(indexUrl: String): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun insertFromPreference(indexUrl: String, name: String)
|
||||||
|
|
||||||
|
suspend fun refreshAll()
|
||||||
|
|
||||||
|
suspend fun fetchExtensions(
|
||||||
|
// KMK -->
|
||||||
|
disabledRepos: Set<String>,
|
||||||
|
// KMK <--
|
||||||
|
): List<Extension.Available>
|
||||||
|
|
||||||
|
suspend fun getAll(): List<ExtensionStore>
|
||||||
|
|
||||||
|
fun getAllAsFlow(): Flow<List<ExtensionStore>>
|
||||||
|
|
||||||
|
fun getCountAsFlow(): Flow<Long>
|
||||||
|
|
||||||
|
suspend fun remove(indexUrl: String)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
package mihon.domain.extensionrepo.interactor
|
|
||||||
|
|
||||||
import logcat.LogPriority
|
|
||||||
import mihon.domain.extensionrepo.exception.SaveExtensionRepoException
|
|
||||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
|
||||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import tachiyomi.core.common.util.system.logcat
|
|
||||||
|
|
||||||
class CreateExtensionRepo(
|
|
||||||
private val repository: ExtensionRepoRepository,
|
|
||||||
private val service: ExtensionRepoService,
|
|
||||||
) {
|
|
||||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
|
||||||
|
|
||||||
suspend fun await(indexUrl: String): Result {
|
|
||||||
val formattedIndexUrl = indexUrl.toHttpUrlOrNull()
|
|
||||||
?.toString()
|
|
||||||
?.takeIf { it.matches(repoRegex) }
|
|
||||||
?: return Result.InvalidUrl
|
|
||||||
|
|
||||||
val baseUrl = formattedIndexUrl.removeSuffix("/index.min.json")
|
|
||||||
return service.fetchRepoDetails(baseUrl)?.let { insert(it) } ?: Result.InvalidUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun insert(repo: ExtensionRepo): Result {
|
|
||||||
return try {
|
|
||||||
repository.insertRepo(
|
|
||||||
repo.baseUrl,
|
|
||||||
repo.name,
|
|
||||||
repo.shortName,
|
|
||||||
repo.website,
|
|
||||||
repo.signingKeyFingerprint,
|
|
||||||
)
|
|
||||||
Result.Success
|
|
||||||
} catch (e: SaveExtensionRepoException) {
|
|
||||||
logcat(LogPriority.WARN, e) { "SQL Conflict attempting to add new repository ${repo.baseUrl}" }
|
|
||||||
return handleInsertionError(repo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error Handler for insert when there are trying to create new repositories
|
|
||||||
*
|
|
||||||
* SaveExtensionRepoException doesn't provide constraint info in exceptions.
|
|
||||||
* First check if the conflict was on primary key. if so return RepoAlreadyExists
|
|
||||||
* Then check if the conflict was on fingerprint. if so Return DuplicateFingerprint
|
|
||||||
* If neither are found, there was some other Error, and return Result.Error
|
|
||||||
*
|
|
||||||
* @param repo Extension Repo holder for passing to DB/Error Dialog
|
|
||||||
*/
|
|
||||||
private suspend fun handleInsertionError(repo: ExtensionRepo): Result {
|
|
||||||
val repoExists = repository.getRepo(repo.baseUrl)
|
|
||||||
if (repoExists != null) {
|
|
||||||
return Result.RepoAlreadyExists
|
|
||||||
}
|
|
||||||
val matchingFingerprintRepo = repository.getRepoBySigningKeyFingerprint(repo.signingKeyFingerprint)
|
|
||||||
if (matchingFingerprintRepo != null) {
|
|
||||||
return Result.DuplicateFingerprint(matchingFingerprintRepo, repo)
|
|
||||||
}
|
|
||||||
return Result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface Result {
|
|
||||||
data class DuplicateFingerprint(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : Result
|
|
||||||
data object InvalidUrl : Result
|
|
||||||
data object RepoAlreadyExists : Result
|
|
||||||
data object Success : Result
|
|
||||||
data object Error : Result
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val REPO_HELP = "https://komikku-app.github.io/docs/guides/getting-started#adding-sources"
|
|
||||||
|
|
||||||
// cuong-tran's key
|
|
||||||
const val KOMIKKU_SIGNATURE = "cbec121aa82ebb02aaa73806992e0368a97d47b5451ed6524816d03084c45905"
|
|
||||||
const val REPO_SIGNATURE = "9add655a78e96c4ec7a53ef89dccb557cb5d767489fac5e785d671a5a75d4da2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<List<ExtensionRepo>> = repository.subscribeAll()
|
|
||||||
|
|
||||||
suspend fun getAll(): List<ExtensionRepo> = repository.getAll()
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
|
|
@ -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<List<ExtensionRepo>>
|
|
||||||
|
|
||||||
suspend fun getAll(): List<ExtensionRepo>
|
|
||||||
|
|
||||||
suspend fun getRepo(baseUrl: String): ExtensionRepo?
|
|
||||||
|
|
||||||
suspend fun getRepoBySigningKeyFingerprint(fingerprint: String): ExtensionRepo?
|
|
||||||
|
|
||||||
fun getCount(): Flow<Int>
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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<ExtensionRepoMetaDto>()
|
|
||||||
.toExtensionRepo(baseUrl = repo)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to fetch repo details" }
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -364,20 +364,19 @@
|
||||||
<string name="ext_revoke_trust">Revoke trusted unknown extensions</string>
|
<string name="ext_revoke_trust">Revoke trusted unknown extensions</string>
|
||||||
|
|
||||||
<!-- Extension repos -->
|
<!-- Extension repos -->
|
||||||
<string name="label_extension_repos">Extension repos</string>
|
|
||||||
<string name="information_empty_repos">You have no repos set.</string>
|
|
||||||
<string name="action_add_repo">Add repo</string>
|
|
||||||
<string name="label_add_repo_input">Repo URL</string>
|
|
||||||
<string name="action_add_repo_message">Add additional repos to %s. This should be a URL that ends with \"index.min.json\".</string>
|
|
||||||
<string name="error_repo_exists">This repo already exists!</string>
|
|
||||||
<string name="action_delete_repo">Delete repo</string>
|
|
||||||
<string name="invalid_repo_name">Invalid repo URL</string>
|
|
||||||
<string name="delete_repo_confirmation">Do you wish to delete the repo \"%s\"?</string>
|
|
||||||
<string name="add_repo_confirmation">Do you wish to add the repo \"%s\"?</string>
|
|
||||||
<string name="action_open_repo">Open source repo</string>
|
<string name="action_open_repo">Open source repo</string>
|
||||||
<string name="action_replace_repo">Replace</string>
|
|
||||||
<string name="action_replace_repo_title">Signing Key Fingerprint Already Exists</string>
|
<!-- Extension store -->
|
||||||
<string name="action_replace_repo_message">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.</string>
|
<string name="extensionStores">Extension stores</string>
|
||||||
|
<string name="action.addExtensionStore">Add extension store</string>
|
||||||
|
<string name="extensionStoresScreen.emptyLabel">You haven\'t added an extension store yet</string>
|
||||||
|
<string name="extensionStoresScreen.addStore.title">Add extension store</string>
|
||||||
|
<string name="extensionStoresScreen.addStore.alreadyExists">This extension store already exists</string>
|
||||||
|
<string name="extensionStoresScreen.addStore.processing">Processing…</string>
|
||||||
|
<string name="extensionStoresScreen.addStoreInput.inputLabel">Store index URL</string>
|
||||||
|
<string name="extensionStoresScreen.addStoreDeeplink.bodyText">Do you wish to add the extension store below?</string>
|
||||||
|
<string name="extensionStoresScreen.deleteStore.title">Remove extension store</string>
|
||||||
|
<string name="extensionStoresScreen.deleteStore.body">Do you wish to remove the extension store \"%1$s\" (%2$s)?</string>
|
||||||
|
|
||||||
<!-- Reader section -->
|
<!-- Reader section -->
|
||||||
<string name="pref_fullscreen">Fullscreen</string>
|
<string name="pref_fullscreen">Fullscreen</string>
|
||||||
|
|
@ -567,7 +566,6 @@
|
||||||
<string name="backup_choice">What do you want to backup?</string>
|
<string name="backup_choice">What do you want to backup?</string>
|
||||||
<string name="app_settings">App settings</string>
|
<string name="app_settings">App settings</string>
|
||||||
<string name="source_settings">Source settings</string>
|
<string name="source_settings">Source settings</string>
|
||||||
<string name="extensionRepo_settings">Extension repos</string>
|
|
||||||
<string name="private_settings">Include sensitive settings (e.g., tracker login tokens)</string>
|
<string name="private_settings">Include sensitive settings (e.g., tracker login tokens)</string>
|
||||||
<string name="non_library_settings">All read entries</string>
|
<string name="non_library_settings">All read entries</string>
|
||||||
<string name="creating_backup">Creating backup</string>
|
<string name="creating_backup">Creating backup</string>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue