Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import eu.kanade.tachiyomi.animeextension.AnimeExtensionManager
import eu.kanade.tachiyomi.animeextension.model.AnimeExtension
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import mihon.feature.extension.sync.SyncExtensionCandidate
import mihon.feature.extension.sync.SyncExtensionIdentity
import mihon.feature.extension.sync.matchRememberedExtensions

class GetAnimeExtensionsByType(
private val preferences: SourcePreferences,
Expand All @@ -20,7 +23,10 @@ class GetAnimeExtensionsByType(
animeExtensionManager.installedExtensionsFlow,
animeExtensionManager.untrustedExtensionsFlow,
animeExtensionManager.availableExtensionsFlow,
) { enabledLanguages, _installed, _untrusted, _available ->
// Chimahon -->
preferences.rememberedAnimeExtensions().changes(),
// Chimahon <--
) { enabledLanguages, _installed, _untrusted, _available, _remembered ->
val (updates, installed) = _installed
.filter { (showNsfwSources || !it.isNsfw) }
.sortedWith(
Expand All @@ -32,9 +38,34 @@ class GetAnimeExtensionsByType(
val untrusted = _untrusted
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })

// Chimahon -->
// Packages remembered from sync and installable from this device's catalog.
val fromSyncPkgNames = matchRememberedExtensions(
remembered = _remembered.mapNotNullTo(mutableSetOf(), SyncExtensionIdentity::decode),
candidates = _available.map { extension ->
SyncExtensionCandidate(
pkgName = extension.pkgName,
signatureHash = extension.signatureHash,
isNsfw = extension.isNsfw,
languages = extension.sources.map { it.lang }.toSet()
.ifEmpty { setOf(extension.lang) },
)
},
installedPkgNames = _installed.mapTo(mutableSetOf()) { it.pkgName },
showNsfw = showNsfwSources,
enabledLanguages = enabledLanguages,
)
val fromSync = _available
.filter { it.pkgName in fromSyncPkgNames }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
// Chimahon <--

val available = _available
.filter { extension ->
_installed.none { it.pkgName == extension.pkgName } &&
// Chimahon --> matched entries are shown in the "Extensions from Sync" section instead
extension.pkgName !in fromSyncPkgNames &&
// Chimahon <--
_installed.none { it.pkgName == extension.pkgName } &&
_untrusted.none { it.pkgName == extension.pkgName } &&
(showNsfwSources || !extension.isNsfw)
}
Expand All @@ -54,7 +85,7 @@ class GetAnimeExtensionsByType(
}
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })

AnimeExtensions(updates, installed, available, untrusted)
AnimeExtensions(updates, installed, available, untrusted, /* Chimahon */ fromSync)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ data class AnimeExtensions(
val installed: List<AnimeExtension.Installed>,
val available: List<AnimeExtension.Available>,
val untrusted: List<AnimeExtension.Untrusted>,
// Chimahon -->
/** Remembered-from-sync anime extensions available in this device's catalog, one per package. */
val fromSync: List<AnimeExtension.Available> = emptyList(),
// Chimahon <--
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import mihon.feature.extension.sync.SyncExtensionCandidate
import mihon.feature.extension.sync.SyncExtensionIdentity
import mihon.feature.extension.sync.matchRememberedExtensions

class GetExtensionsByType(
private val preferences: SourcePreferences,
Expand All @@ -20,7 +23,10 @@ class GetExtensionsByType(
extensionManager.installedExtensionsFlow,
extensionManager.untrustedExtensionsFlow,
extensionManager.availableExtensionsFlow,
) { enabledLanguages, _installed, _untrusted, _available ->
// Chimahon -->
preferences.rememberedMangaExtensions().changes(),
// Chimahon <--
) { enabledLanguages, _installed, _untrusted, _available, _remembered ->
val (updates, installed) = _installed
.filter { (showNsfwSources || !it.isNsfw) }
.sortedWith(
Expand All @@ -33,14 +39,38 @@ class GetExtensionsByType(
val untrusted = _untrusted
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })

// Chimahon -->
// Packages remembered from sync and installable from this device's catalog.
val fromSyncPkgNames = matchRememberedExtensions(
remembered = _remembered.mapNotNullTo(mutableSetOf(), SyncExtensionIdentity::decode),
candidates = _available.map { extension ->
SyncExtensionCandidate(
pkgName = extension.pkgName,
signatureHash = extension.signatureHash,
isNsfw = extension.isNsfw,
languages = extension.sources.mapTo(mutableSetOf()) { it.lang },
)
},
installedPkgNames = _installed.mapTo(mutableSetOf()) { it.pkgName },
showNsfw = showNsfwSources,
enabledLanguages = enabledLanguages,
)
val fromSync = _available
.filter { it.pkgName in fromSyncPkgNames }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
// Chimahon <--

val available = _available
.filter { extension ->
_installed.none {
// KMK -->
it.signatureHash == extension.signatureHash &&
// KMK <--
it.pkgName == extension.pkgName
} &&
// Chimahon --> matched entries are shown in the "Extensions from Sync" section instead
extension.pkgName !in fromSyncPkgNames &&
// Chimahon <--
_installed.none {
// KMK -->
it.signatureHash == extension.signatureHash &&
// KMK <--
it.pkgName == extension.pkgName
} &&
_untrusted.none {
// KMK -->
it.signatureHash == extension.signatureHash &&
Expand All @@ -62,7 +92,7 @@ class GetExtensionsByType(
}
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })

Extensions(updates, installed, available, untrusted)
Extensions(updates, installed, available, untrusted, /* Chimahon */ fromSync)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ data class Extensions(
val installed: List<Extension.Installed>,
val available: List<Extension.Available>,
val untrusted: List<Extension.Untrusted>,
// Chimahon -->
/** Remembered-from-sync extensions available in this device's catalog, one entry per package. */
val fromSync: List<Extension.Available> = emptyList(),
// Chimahon <--
)
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ class SourcePreferences(
emptySet(),
)

// Chimahon -->
/**
* Cumulative history of manga extension identities (encoded as "pkgName|signatureHash") that
* this device has installed or seen via sync. Powers the "Extensions from Sync" section.
* Stored as app-state so it is never included in preference backups/restores.
*/
fun rememberedMangaExtensions() = preferenceStore.getStringSet(
Preference.appStateKey("remembered_manga_extensions"),
emptySet(),
)

/**
* Cumulative history of anime extension identities. See [rememberedMangaExtensions].
*/
fun rememberedAnimeExtensions() = preferenceStore.getStringSet(
Preference.appStateKey("remembered_anime_extensions"),
emptySet(),
)
// Chimahon <--

fun globalSearchFilterState() = preferenceStore.getBoolean(
Preference.appStateKey("has_filters_toggle_state"),
false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,8 @@ data class Backup(
@ProtoNumber(701) var backupNovelCategories: List<BackupNovelCategory> = emptyList(),
@ProtoNumber(710) var backupMangaStats: List<com.canopus.chimareader.data.MangaStats> = emptyList(),
@ProtoNumber(711) var backupAnkiStats: List<com.canopus.chimareader.data.AnkiStats> = emptyList(),
// Remembered extension identities for the "Extensions from Sync" feature (package id + signing hash only)
@ProtoNumber(720) var backupMangaExtensions: List<BackupExtension> = emptyList(),
@ProtoNumber(721) var backupAnimeExtensions: List<BackupExtension> = emptyList(),
// Chimahon <--
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.data.backup.models

import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber

// Chimahon -->
/**
* A lightweight, backward-compatible identity for a manga or anime extension used by the
* "Extensions from Sync" feature.
*
* Only the package id and signing hash are remembered so devices can recognise an extension
* they previously had without syncing APKs, download URLs, or repositories. The display name is
* intentionally omitted: matches are rendered using the current device's catalog name.
*/
@Serializable
data class BackupExtension(
@ProtoNumber(1) val pkgName: String = "",
@ProtoNumber(2) val signatureHash: String = "",
)
// Chimahon <--
50 changes: 50 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/data/sync/SyncManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.data.sync

import android.content.Context
import android.net.Uri
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.tachiyomi.animeextension.AnimeExtensionManager
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.data.backup.models.Backup
Expand All @@ -15,10 +17,14 @@ import eu.kanade.tachiyomi.data.sync.service.GoogleDriveSyncService
import eu.kanade.tachiyomi.data.sync.service.SyncData
import eu.kanade.tachiyomi.data.sync.service.SyncYomiSyncService
import eu.kanade.tachiyomi.data.sync.service.WebDavSyncService
import eu.kanade.tachiyomi.extension.ExtensionManager
import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority
import logcat.logcat
import mihon.feature.extension.sync.SyncExtensionIdentity
import mihon.feature.extension.sync.combineRememberedWithInstalled
import mihon.feature.extension.sync.cumulativeExtensionHistory
import tachiyomi.core.common.util.system.logcat
import tachiyomi.data.Chapters
import tachiyomi.data.DatabaseHandler
Expand Down Expand Up @@ -47,6 +53,11 @@ class SyncManager(
ignoreUnknownKeys = true
},
private val getCategories: GetCategories = Injekt.get(),
// Chimahon -->
private val sourcePreferences: SourcePreferences = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(),
private val animeExtensionManager: AnimeExtensionManager = Injekt.get(),
// Chimahon <--
) {
private val backupCreator: BackupCreator = BackupCreator(context, false)
private val notifier: SyncNotifier = SyncNotifier(context)
Expand Down Expand Up @@ -107,6 +118,32 @@ class SyncManager(
logcat(LogPriority.DEBUG) { "Begin create backup" }
val backupManga = backupCreator.backupMangas(databaseManga, backupOptions)
val backupAnime = backupCreator.backupAnimes(backupOptions)

// Chimahon -->
// Combine remembered extension identities with currently installed extensions before syncing.
val rememberedMangaExtensionsPref = sourcePreferences.rememberedMangaExtensions()
val rememberedAnimeExtensionsPref = sourcePreferences.rememberedAnimeExtensions()
val backupMangaExtensions = combineRememberedWithInstalled(
remembered = rememberedMangaExtensionsPref.get(),
installed = extensionManager.installedExtensionsFlow.value.map {
SyncExtensionIdentity(pkgName = it.pkgName, signatureHash = it.signatureHash)
},
)
val backupAnimeExtensions = combineRememberedWithInstalled(
remembered = rememberedAnimeExtensionsPref.get(),
installed = animeExtensionManager.installedExtensionsFlow.value.map {
SyncExtensionIdentity(pkgName = it.pkgName, signatureHash = it.signatureHash)
},
)
// Keep the local history cumulative even when nothing is received from remote.
rememberedMangaExtensionsPref.set(
cumulativeExtensionHistory(rememberedMangaExtensionsPref.get(), backupMangaExtensions),
)
rememberedAnimeExtensionsPref.set(
cumulativeExtensionHistory(rememberedAnimeExtensionsPref.get(), backupAnimeExtensions),
)
// Chimahon <--

val backup = Backup(
backupManga = backupManga,
backupCategories = backupCreator.backupCategories(backupOptions),
Expand All @@ -129,6 +166,8 @@ class SyncManager(
// Chimahon -->
backupNovels = backupCreator.backupNovels(backupOptions),
backupNovelCategories = backupCreator.backupNovelCategories(backupOptions),
backupMangaExtensions = backupMangaExtensions,
backupAnimeExtensions = backupAnimeExtensions,
// Chimahon <--
)
logcat(LogPriority.DEBUG) { "End create backup" }
Expand Down Expand Up @@ -174,6 +213,17 @@ class SyncManager(
return
}

// Chimahon -->
// Persist the union of local and remote extension identities before any early return so the
// "Extensions from Sync" history keeps accumulating even when there is nothing else to restore.
rememberedMangaExtensionsPref.set(
cumulativeExtensionHistory(rememberedMangaExtensionsPref.get(), remoteBackup.backupMangaExtensions),
)
rememberedAnimeExtensionsPref.set(
cumulativeExtensionHistory(rememberedAnimeExtensionsPref.get(), remoteBackup.backupAnimeExtensions),
)
// Chimahon <--

if (remoteBackup == syncData.backup) {
// nothing changed
logcat(LogPriority.DEBUG) { "Skip restore due to remote was overwrite from local" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import logcat.LogPriority
import logcat.logcat
import mihon.feature.extension.sync.mergeExtensionIdentities

@Serializable
data class SyncData(
Expand Down Expand Up @@ -107,6 +108,14 @@ abstract class SyncService(
remoteSyncData.backup?.backupNovelCategories ?: emptyList(),
mergedNovelCategoriesList,
)
val mergedMangaExtensions = mergeExtensionIdentities(
localSyncData.backup?.backupMangaExtensions,
remoteSyncData.backup?.backupMangaExtensions,
)
val mergedAnimeExtensions = mergeExtensionIdentities(
localSyncData.backup?.backupAnimeExtensions,
remoteSyncData.backup?.backupAnimeExtensions,
)
// Chimahon <--

// Create the merged Backup object
Expand All @@ -129,6 +138,8 @@ abstract class SyncService(
// Chimahon -->
backupNovels = mergedNovelsList,
backupNovelCategories = mergedNovelCategoriesList,
backupMangaExtensions = mergedMangaExtensions,
backupAnimeExtensions = mergedAnimeExtensions,
// Chimahon <--
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.time.Duration.Companion.seconds
Expand Down Expand Up @@ -62,7 +63,7 @@ class AnimeExtensionsScreenModel(
.map { searchQueryPredicate(it ?: "") },
currentDownloads,
getAnimeExtensions.subscribe(),
) { predicate, downloads, (_updates, _installed, _available, _untrusted) ->
) { predicate, downloads, (_updates, _installed, _available, _untrusted, _fromSync) ->
buildMap {
val updates = _updates.filter(predicate).map(extensionMapper(downloads))
if (updates.isNotEmpty()) {
Expand All @@ -77,6 +78,13 @@ class AnimeExtensionsScreenModel(
put(AnimeExtensionUiModel.Header.Resource(MR.strings.ext_installed), installed + untrusted)
}

// Chimahon -->
val fromSync = _fromSync.filter(predicate).map(extensionMapper(downloads))
if (fromSync.isNotEmpty()) {
put(AnimeExtensionUiModel.Header.Resource(KMR.strings.extensions_from_sync), fromSync)
}
// Chimahon <--

val languagesWithExtensions = _available
.filter(predicate)
.groupBy { it.lang }
Expand Down
Loading