Remove SavedFilterBackup class

This commit is contained in:
Koitharu
2025-10-26 17:35:29 +02:00
parent 6377de470d
commit beaf5cc0d5
3 changed files with 238 additions and 270 deletions

View File

@@ -26,7 +26,6 @@ import org.koitharu.kotatsu.backups.data.model.CategoryBackup
import org.koitharu.kotatsu.backups.data.model.FavouriteBackup
import org.koitharu.kotatsu.backups.data.model.HistoryBackup
import org.koitharu.kotatsu.backups.data.model.MangaBackup
import org.koitharu.kotatsu.backups.data.model.SavedFilterBackup
import org.koitharu.kotatsu.backups.data.model.ScrobblingBackup
import org.koitharu.kotatsu.backups.data.model.SourceBackup
import org.koitharu.kotatsu.backups.data.model.StatisticBackup
@@ -36,6 +35,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.CompositeResult
import org.koitharu.kotatsu.core.util.progress.Progress
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.filter.data.PersistableFilter
import org.koitharu.kotatsu.filter.data.SavedFiltersRepository
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.data.TapGridSettings
@@ -48,271 +48,267 @@ import javax.inject.Inject
@Reusable
class BackupRepository @Inject constructor(
private val database: MangaDatabase,
private val settings: AppSettings,
private val tapGridSettings: TapGridSettings,
private val mangaSourcesRepository: MangaSourcesRepository,
private val savedFiltersRepository: SavedFiltersRepository,
private val database: MangaDatabase,
private val settings: AppSettings,
private val tapGridSettings: TapGridSettings,
private val mangaSourcesRepository: MangaSourcesRepository,
private val savedFiltersRepository: SavedFiltersRepository,
) {
private val json = Json {
allowSpecialFloatingPointValues = true
coerceInputValues = true
encodeDefaults = true
ignoreUnknownKeys = true
useAlternativeNames = false
}
private val json = Json {
allowSpecialFloatingPointValues = true
coerceInputValues = true
encodeDefaults = true
ignoreUnknownKeys = true
useAlternativeNames = false
}
suspend fun createBackup(
output: ZipOutputStream,
progress: FlowCollector<Progress>?,
) {
progress?.emit(Progress.INDETERMINATE)
var commonProgress = Progress(0, BackupSection.entries.size)
for (section in BackupSection.entries) {
when (section) {
BackupSection.INDEX -> output.writeJsonArray(
section = BackupSection.INDEX,
data = flowOf(BackupIndex()),
serializer = serializer(),
)
suspend fun createBackup(
output: ZipOutputStream,
progress: FlowCollector<Progress>?,
) {
progress?.emit(Progress.INDETERMINATE)
var commonProgress = Progress(0, BackupSection.entries.size)
for (section in BackupSection.entries) {
when (section) {
BackupSection.INDEX -> output.writeJsonArray(
section = BackupSection.INDEX,
data = flowOf(BackupIndex()),
serializer = serializer(),
)
BackupSection.HISTORY -> output.writeJsonArray(
section = BackupSection.HISTORY,
data = database.getHistoryDao().dump().map { HistoryBackup(it) },
serializer = serializer(),
)
BackupSection.HISTORY -> output.writeJsonArray(
section = BackupSection.HISTORY,
data = database.getHistoryDao().dump().map { HistoryBackup(it) },
serializer = serializer(),
)
BackupSection.CATEGORIES -> output.writeJsonArray(
section = BackupSection.CATEGORIES,
data = database.getFavouriteCategoriesDao().findAll().asFlow().map { CategoryBackup(it) },
serializer = serializer(),
)
BackupSection.CATEGORIES -> output.writeJsonArray(
section = BackupSection.CATEGORIES,
data = database.getFavouriteCategoriesDao().findAll().asFlow().map { CategoryBackup(it) },
serializer = serializer(),
)
BackupSection.FAVOURITES -> output.writeJsonArray(
section = BackupSection.FAVOURITES,
data = database.getFavouritesDao().dump().map { FavouriteBackup(it) },
serializer = serializer(),
)
BackupSection.FAVOURITES -> output.writeJsonArray(
section = BackupSection.FAVOURITES,
data = database.getFavouritesDao().dump().map { FavouriteBackup(it) },
serializer = serializer(),
)
BackupSection.SETTINGS -> output.writeString(
section = BackupSection.SETTINGS,
data = dumpSettings(),
)
BackupSection.SETTINGS -> output.writeString(
section = BackupSection.SETTINGS,
data = dumpSettings(),
)
BackupSection.SETTINGS_READER_GRID -> output.writeString(
section = BackupSection.SETTINGS_READER_GRID,
data = dumpReaderGridSettings(),
)
BackupSection.SETTINGS_READER_GRID -> output.writeString(
section = BackupSection.SETTINGS_READER_GRID,
data = dumpReaderGridSettings(),
)
BackupSection.BOOKMARKS -> output.writeJsonArray(
section = BackupSection.BOOKMARKS,
data = database.getBookmarksDao().dump().map { BookmarkBackup(it.first, it.second) },
serializer = serializer(),
)
BackupSection.BOOKMARKS -> output.writeJsonArray(
section = BackupSection.BOOKMARKS,
data = database.getBookmarksDao().dump().map { BookmarkBackup(it.first, it.second) },
serializer = serializer(),
)
BackupSection.SOURCES -> output.writeJsonArray(
section = BackupSection.SOURCES,
data = database.getSourcesDao().dumpEnabled().map { SourceBackup(it) },
serializer = serializer(),
)
BackupSection.SOURCES -> output.writeJsonArray(
section = BackupSection.SOURCES,
data = database.getSourcesDao().dumpEnabled().map { SourceBackup(it) },
serializer = serializer(),
)
BackupSection.SCROBBLING -> output.writeJsonArray(
section = BackupSection.SCROBBLING,
data = database.getScrobblingDao().dumpEnabled().map { ScrobblingBackup(it) },
serializer = serializer(),
)
BackupSection.SCROBBLING -> output.writeJsonArray(
section = BackupSection.SCROBBLING,
data = database.getScrobblingDao().dumpEnabled().map { ScrobblingBackup(it) },
serializer = serializer(),
)
BackupSection.STATS -> output.writeJsonArray(
section = BackupSection.STATS,
data = database.getStatsDao().dumpEnabled().map { StatisticBackup(it) },
serializer = serializer(),
)
BackupSection.STATS -> output.writeJsonArray(
section = BackupSection.STATS,
data = database.getStatsDao().dumpEnabled().map { StatisticBackup(it) },
serializer = serializer(),
)
BackupSection.SAVED_FILTERS -> {
val sources = mangaSourcesRepository.getEnabledSources()
val filters = sources.flatMap { source ->
savedFiltersRepository.getAll(source)
}
output.writeJsonArray(
section = BackupSection.SAVED_FILTERS,
data = filters.asFlow().map { SavedFilterBackup(it) },
serializer = serializer(),
)
}
}
progress?.emit(commonProgress)
commonProgress++
}
progress?.emit(commonProgress)
}
BackupSection.SAVED_FILTERS -> {
val sources = mangaSourcesRepository.getEnabledSources()
val filters = sources.flatMap { source ->
savedFiltersRepository.getAll(source)
}
output.writeJsonArray(
section = BackupSection.SAVED_FILTERS,
data = filters.asFlow(),
serializer = serializer(),
)
}
}
progress?.emit(commonProgress)
commonProgress++
}
progress?.emit(commonProgress)
}
suspend fun restoreBackup(
input: ZipInputStream,
sections: Set<BackupSection>,
progress: FlowCollector<Progress>?,
): CompositeResult {
progress?.emit(Progress.INDETERMINATE)
var commonProgress = Progress(0, sections.size)
var entry = input.nextEntry
var result = CompositeResult.EMPTY
while (entry != null) {
val section = BackupSection.of(entry)
if (section in sections) {
result = result + when (section) {
BackupSection.INDEX -> CompositeResult.EMPTY // useless in our case
BackupSection.HISTORY -> input.readJsonArray<HistoryBackup>(serializer()).restoreToDb {
upsertManga(it.manga)
getHistoryDao().upsert(it.toEntity())
}
suspend fun restoreBackup(
input: ZipInputStream,
sections: Set<BackupSection>,
progress: FlowCollector<Progress>?,
): CompositeResult {
progress?.emit(Progress.INDETERMINATE)
var commonProgress = Progress(0, sections.size)
var entry = input.nextEntry
var result = CompositeResult.EMPTY
while (entry != null) {
val section = BackupSection.of(entry)
if (section in sections) {
result += when (section) {
BackupSection.INDEX -> CompositeResult.EMPTY // useless in our case
BackupSection.HISTORY -> input.readJsonArray<HistoryBackup>(serializer()).restoreToDb {
upsertManga(it.manga)
getHistoryDao().upsert(it.toEntity())
}
BackupSection.CATEGORIES -> input.readJsonArray<CategoryBackup>(serializer()).restoreToDb {
getFavouriteCategoriesDao().upsert(it.toEntity())
}
BackupSection.CATEGORIES -> input.readJsonArray<CategoryBackup>(serializer()).restoreToDb {
getFavouriteCategoriesDao().upsert(it.toEntity())
}
BackupSection.FAVOURITES -> input.readJsonArray<FavouriteBackup>(serializer()).restoreToDb {
upsertManga(it.manga)
getFavouritesDao().upsert(it.toEntity())
}
BackupSection.FAVOURITES -> input.readJsonArray<FavouriteBackup>(serializer()).restoreToDb {
upsertManga(it.manga)
getFavouritesDao().upsert(it.toEntity())
}
BackupSection.SETTINGS -> input.readMap().let {
settings.upsertAll(it)
CompositeResult.success()
}
BackupSection.SETTINGS -> input.readMap().let {
settings.upsertAll(it)
CompositeResult.success()
}
BackupSection.SETTINGS_READER_GRID -> input.readMap().let {
tapGridSettings.upsertAll(it)
CompositeResult.success()
}
BackupSection.SETTINGS_READER_GRID -> input.readMap().let {
tapGridSettings.upsertAll(it)
CompositeResult.success()
}
BackupSection.BOOKMARKS -> input.readJsonArray<BookmarkBackup>(serializer()).restoreToDb {
upsertManga(it.manga)
getBookmarksDao().upsert(it.bookmarks.map { b -> b.toEntity() })
}
BackupSection.BOOKMARKS -> input.readJsonArray<BookmarkBackup>(serializer()).restoreToDb {
upsertManga(it.manga)
getBookmarksDao().upsert(it.bookmarks.map { b -> b.toEntity() })
}
BackupSection.SOURCES -> input.readJsonArray<SourceBackup>(serializer()).restoreToDb {
getSourcesDao().upsert(it.toEntity())
}
BackupSection.SOURCES -> input.readJsonArray<SourceBackup>(serializer()).restoreToDb {
getSourcesDao().upsert(it.toEntity())
}
BackupSection.SCROBBLING -> input.readJsonArray<ScrobblingBackup>(serializer()).restoreToDb {
getScrobblingDao().upsert(it.toEntity())
}
BackupSection.SCROBBLING -> input.readJsonArray<ScrobblingBackup>(serializer()).restoreToDb {
getScrobblingDao().upsert(it.toEntity())
}
BackupSection.STATS -> input.readJsonArray<StatisticBackup>(serializer()).restoreToDb {
getStatsDao().upsert(it.toEntity())
}
BackupSection.STATS -> input.readJsonArray<StatisticBackup>(serializer()).restoreToDb {
getStatsDao().upsert(it.toEntity())
}
BackupSection.SAVED_FILTERS -> input.readJsonArray<SavedFilterBackup>(serializer())
.restoreWithoutTransaction {
savedFiltersRepository.save(
source = it.source,
name = it.name,
filter = it.filter,
)
}
BackupSection.SAVED_FILTERS -> input.readJsonArray<PersistableFilter>(serializer())
.restoreWithoutTransaction {
savedFiltersRepository.save(it)
}
null -> CompositeResult.EMPTY // skip unknown entries
}
progress?.emit(commonProgress)
commonProgress++
}
input.closeEntry()
entry = input.nextEntry
}
progress?.emit(commonProgress)
return result
}
null -> CompositeResult.EMPTY // skip unknown entries
}
progress?.emit(commonProgress)
commonProgress++
}
input.closeEntry()
entry = input.nextEntry
}
progress?.emit(commonProgress)
return result
}
private suspend fun <T> ZipOutputStream.writeJsonArray(
section: BackupSection,
data: Flow<T>,
serializer: SerializationStrategy<T>,
) {
data.onStart {
putNextEntry(ZipEntry(section.entryName))
write("[")
}.onCompletion { error ->
if (error == null) {
write("]")
}
closeEntry()
flush()
}.collectIndexed { index, value ->
if (index > 0) {
write(",")
}
json.encodeToStream(serializer, value, this)
}
}
private suspend fun <T> ZipOutputStream.writeJsonArray(
section: BackupSection,
data: Flow<T>,
serializer: SerializationStrategy<T>,
) {
data.onStart {
putNextEntry(ZipEntry(section.entryName))
write("[")
}.onCompletion { error ->
if (error == null) {
write("]")
}
closeEntry()
flush()
}.collectIndexed { index, value ->
if (index > 0) {
write(",")
}
json.encodeToStream(serializer, value, this)
}
}
private fun <T> InputStream.readJsonArray(
serializer: DeserializationStrategy<T>,
): Sequence<T> = json.decodeToSequence(this, serializer, DecodeSequenceMode.ARRAY_WRAPPED)
private fun <T> InputStream.readJsonArray(
serializer: DeserializationStrategy<T>,
): Sequence<T> = json.decodeToSequence(this, serializer, DecodeSequenceMode.ARRAY_WRAPPED)
private fun InputStream.readMap(): Map<String, Any?> {
val jo = JSONArray(readString()).getJSONObject(0)
val map = ArrayMap<String, Any?>(jo.length())
val keys = jo.keys()
while (keys.hasNext()) {
val key = keys.next()
map[key] = jo.get(key)
}
return map
}
private fun InputStream.readMap(): Map<String, Any?> {
val jo = JSONArray(readString()).getJSONObject(0)
val map = ArrayMap<String, Any?>(jo.length())
val keys = jo.keys()
while (keys.hasNext()) {
val key = keys.next()
map[key] = jo.get(key)
}
return map
}
private fun ZipOutputStream.writeString(
section: BackupSection,
data: String,
) {
putNextEntry(ZipEntry(section.entryName))
try {
write("[")
write(data)
write("]")
} finally {
closeEntry()
flush()
}
}
private fun ZipOutputStream.writeString(
section: BackupSection,
data: String,
) {
putNextEntry(ZipEntry(section.entryName))
try {
write("[")
write(data)
write("]")
} finally {
closeEntry()
flush()
}
}
private fun OutputStream.write(str: String) = write(str.toByteArray())
private fun OutputStream.write(str: String) = write(str.toByteArray())
private fun InputStream.readString(): String = readBytes().decodeToString()
private fun InputStream.readString(): String = readBytes().decodeToString()
private fun dumpSettings(): String {
val map = settings.getAllValues().toMutableMap()
map.remove(AppSettings.KEY_APP_PASSWORD)
map.remove(AppSettings.KEY_PROXY_PASSWORD)
map.remove(AppSettings.KEY_PROXY_LOGIN)
map.remove(AppSettings.KEY_INCOGNITO_MODE)
return JSONObject(map).toString()
}
private fun dumpSettings(): String {
val map = settings.getAllValues().toMutableMap()
map.remove(AppSettings.KEY_APP_PASSWORD)
map.remove(AppSettings.KEY_PROXY_PASSWORD)
map.remove(AppSettings.KEY_PROXY_LOGIN)
map.remove(AppSettings.KEY_INCOGNITO_MODE)
return JSONObject(map).toString()
}
private fun dumpReaderGridSettings(): String {
return JSONObject(tapGridSettings.getAllValues()).toString()
}
private fun dumpReaderGridSettings(): String {
return JSONObject(tapGridSettings.getAllValues()).toString()
}
private suspend fun MangaDatabase.upsertManga(manga: MangaBackup) {
val tags = manga.tags.map { it.toEntity() }
getTagsDao().upsert(tags)
getMangaDao().upsert(manga.toEntity(), tags)
}
private suspend fun MangaDatabase.upsertManga(manga: MangaBackup) {
val tags = manga.tags.map { it.toEntity() }
getTagsDao().upsert(tags)
getMangaDao().upsert(manga.toEntity(), tags)
}
private suspend inline fun <T> Sequence<T>.restoreToDb(crossinline block: suspend MangaDatabase.(T) -> Unit): CompositeResult {
return fold(CompositeResult.EMPTY) { result, item ->
result + runCatchingCancellable {
database.withTransaction {
database.block(item)
}
}
}
}
private suspend inline fun <T> Sequence<T>.restoreToDb(crossinline block: suspend MangaDatabase.(T) -> Unit): CompositeResult {
return fold(CompositeResult.EMPTY) { result, item ->
result + runCatchingCancellable {
database.withTransaction {
database.block(item)
}
}
}
}
private suspend inline fun <T> Sequence<T>.restoreWithoutTransaction(crossinline block: suspend (T) -> Unit): CompositeResult {
return fold(CompositeResult.EMPTY) { result, item ->
result + runCatchingCancellable {
block(item)
}
}
}
private suspend inline fun <T> Sequence<T>.restoreWithoutTransaction(crossinline block: suspend (T) -> Unit): CompositeResult {
return fold(CompositeResult.EMPTY) { result, item ->
result + runCatchingCancellable {
block(item)
}
}
}
}

View File

@@ -1,34 +0,0 @@
package org.koitharu.kotatsu.backups.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.model.MangaSourceSerializer
import org.koitharu.kotatsu.filter.data.MangaListFilterSerializer
import org.koitharu.kotatsu.filter.data.PersistableFilter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
@Serializable
data class SavedFilterBackup(
@SerialName("name")
val name: String,
@Serializable(with = MangaSourceSerializer::class)
@SerialName("source")
val source: MangaSource,
@Serializable(with = MangaListFilterSerializer::class)
@SerialName("filter")
val filter: MangaListFilter,
) {
constructor(persistableFilter: PersistableFilter) : this(
name = persistableFilter.name,
source = persistableFilter.source,
filter = persistableFilter.filter,
)
fun toPersistableFilter() = PersistableFilter(
name = name,
source = source,
filter = filter,
)
}

View File

@@ -57,10 +57,16 @@ class SavedFiltersRepository @Inject constructor(
source = source,
filter = filter,
)
persist(source, persistableFilter)
persist(persistableFilter)
persistableFilter
}
suspend fun save(
filter: PersistableFilter,
) = withContext(Dispatchers.Default) {
persist(filter)
}
suspend fun rename(source: MangaSource, id: Int, newName: String) = withContext(Dispatchers.Default) {
val filter = load(source, id) ?: return@withContext
val newFilter = filter.copy(name = newName)
@@ -79,8 +85,8 @@ class SavedFiltersRepository @Inject constructor(
}
}
private fun persist(source: MangaSource, persistableFilter: PersistableFilter) {
val prefs = getPrefs(source)
private fun persist(persistableFilter: PersistableFilter) {
val prefs = getPrefs(persistableFilter.source)
val json = Json.encodeToString(persistableFilter)
prefs.edit(commit = true) {
putString(key(persistableFilter.id), json)