From f6080cd5eb22c24009775f8ba721cbece57349b0 Mon Sep 17 00:00:00 2001 From: Ahmad Ansori Palembani Date: Wed, 19 Jun 2024 11:12:37 +0700 Subject: [PATCH] refactor: Migrated most manga queries and some chapter queries to SQLDelight --- CHANGELOG.md | 5 +- .../restorers/CategoriesBackupRestorer.kt | 9 +- .../restore/restorers/MangaBackupRestorer.kt | 44 +++-- .../tachiyomi/data/database/models/Manga.kt | 25 +++ .../data/database/queries/ChapterQueries.kt | 16 -- .../data/database/queries/MangaQueries.kt | 57 +----- .../data/library/LibraryUpdateJob.kt | 2 +- .../tachiyomi/ui/library/LibraryPresenter.kt | 8 +- .../ui/manga/MangaDetailsPresenter.kt | 67 ++++--- .../manga/process/MigrationListController.kt | 8 +- .../tachiyomi/ui/reader/ReaderViewModel.kt | 13 +- .../util/chapter/ChapterSourceSync.kt | 166 ++++++++++-------- .../kanade/tachiyomi/util/manga/MangaUtil.kt | 10 +- .../main/java/yokai/core/di/DomainModule.kt | 8 + .../data/chapter/ChapterRepositoryImpl.kt | 91 ++++++++++ .../yokai/data/manga/MangaRepositoryImpl.kt | 10 +- .../yokai/domain/chapter/ChapterRepository.kt | 9 + .../chapter/interactor/DeleteChapters.kt | 11 ++ .../chapter/interactor/InsertChapters.kt | 10 ++ .../chapter/interactor/UpdateChapters.kt | 11 ++ .../yokai/domain/manga/MangaRepository.kt | 6 +- .../yokai/domain/manga/interactor/GetManga.kt | 13 ++ .../domain/manga/interactor/UpdateManga.kt | 4 +- .../sqldelight/tachiyomi/data/chapters.sq | 31 ++++ .../sqldelight/tachiyomi/data/mangas.sq | 10 ++ .../domain/chapter/models/ChapterUpdate.kt | 17 ++ 26 files changed, 450 insertions(+), 211 deletions(-) create mode 100644 app/src/main/java/yokai/domain/chapter/interactor/DeleteChapters.kt create mode 100644 app/src/main/java/yokai/domain/chapter/interactor/InsertChapters.kt create mode 100644 app/src/main/java/yokai/domain/chapter/interactor/UpdateChapters.kt create mode 100644 app/src/main/java/yokai/domain/manga/interactor/GetManga.kt create mode 100644 domain/src/commonMain/kotlin/yokai/domain/chapter/models/ChapterUpdate.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6670688dc8..7732b44637 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ - Update dependency com.google.gms:google-services to v4.4.2 - Add crashlytics integration for Kermit - Replace ProgressBar with ProgressIndicator from Material3 to improve UI consistency -- Merge lastFetch and lastRead query into library_view VIEW +- More StorIO to SQLDelight migrations + - Merge lastFetch and lastRead query into library_view VIEW + - Migrated a few more chapter related queries + - Migrated most of manga related queries - Update Japenese translation - Update dependency com.github.tachiyomiorg:unifile to a9de196cc7 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesBackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesBackupRestorer.kt index e48f0f98b2..0412046a7c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesBackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/CategoriesBackupRestorer.kt @@ -4,16 +4,17 @@ import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.database.DatabaseHelper import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import yokai.domain.category.interactor.GetCategories class CategoriesBackupRestorer( private val db: DatabaseHelper = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), ) { - @Suppress("RedundantSuspendModifier") suspend fun restoreCategories(backupCategories: List, onComplete: () -> Unit) { + // Get categories from file and from db + // Do it outside of transaction because StorIO might hang because we're using SQLDelight + val dbCategories = getCategories.await() db.inTransaction { - // Get categories from file and from db - val dbCategories = db.getCategories().executeAsBlocking() - // Iterate over them backupCategories.map { it.getCategoryImpl() }.forEach { category -> // Used to know if the category is already in the db diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaBackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaBackupRestorer.kt index bf67bfedb7..66d41d726d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaBackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/restorers/MangaBackupRestorer.kt @@ -17,14 +17,24 @@ import eu.kanade.tachiyomi.util.manga.MangaUtil import eu.kanade.tachiyomi.util.system.launchNow import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import yokai.domain.category.interactor.GetCategories +import yokai.domain.chapter.interactor.GetChapters import yokai.domain.library.custom.model.CustomMangaInfo +import yokai.domain.manga.interactor.GetManga +import yokai.domain.manga.interactor.InsertManga +import yokai.domain.manga.interactor.UpdateManga import kotlin.math.max class MangaBackupRestorer( private val db: DatabaseHelper = Injekt.get(), private val customMangaManager: CustomMangaManager = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val getChapters: GetChapters = Injekt.get(), + private val getManga: GetManga = Injekt.get(), + private val insertManga: InsertManga = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), ) { - fun restoreManga( + suspend fun restoreManga( backupManga: BackupManga, backupCategories: List, onComplete: (Manga) -> Unit, @@ -40,19 +50,19 @@ class MangaBackupRestorer( val filteredScanlators = backupManga.excludedScanlators try { - val dbManga = db.getManga(manga.url, manga.source).executeAsBlocking() + val dbManga = getManga.awaitByUrlAndSource(manga.url, manga.source) if (dbManga == null) { // Manga not in database - restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories, filteredScanlators, customManga) + restoreNewManga(manga, chapters, categories, history, tracks, backupCategories, filteredScanlators, customManga) } else { // Manga in database // Copy information from manga already in database manga.id = dbManga.id manga.filtered_scanlators = dbManga.filtered_scanlators manga.copyFrom(dbManga) - db.insertManga(manga).executeAsBlocking() + updateManga.await(manga.toMangaUpdate()) // Fetch rest of manga information - restoreNewManga(manga, chapters, categories, history, tracks, backupCategories, filteredScanlators, customManga) + restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories, filteredScanlators, customManga) } } catch (e: Exception) { onError(manga, e) @@ -69,7 +79,7 @@ class MangaBackupRestorer( * @param chapters chapters of manga that needs updating * @param categories categories that need updating */ - private fun restoreExistingManga( + private suspend fun restoreNewManga( manga: Manga, chapters: List, categories: List, @@ -81,7 +91,7 @@ class MangaBackupRestorer( ) { val fetchedManga = manga.also { it.initialized = it.description != null - it.id = db.insertManga(it).executeAsBlocking().insertedId() + it.id = insertManga.await(it) } fetchedManga.id ?: return @@ -89,7 +99,7 @@ class MangaBackupRestorer( restoreExtras(fetchedManga, categories, history, tracks, backupCategories, filteredScanlators, customManga) } - private fun restoreNewManga( + private suspend fun restoreExistingManga( backupManga: Manga, chapters: List, categories: List, @@ -103,8 +113,8 @@ class MangaBackupRestorer( restoreExtras(backupManga, categories, history, tracks, backupCategories, filteredScanlators, customManga) } - private fun restoreChapters(manga: Manga, chapters: List) { - val dbChapters = db.getChapters(manga).executeAsBlocking() + private suspend fun restoreChapters(manga: Manga, chapters: List) { + val dbChapters = getChapters.await(manga) chapters.forEach { chapter -> val dbChapter = dbChapters.find { it.url == chapter.url } @@ -130,7 +140,7 @@ class MangaBackupRestorer( newChapters[false]?.let { db.insertChapters(it).executeAsBlocking() } } - private fun restoreExtras( + private suspend fun restoreExtras( manga: Manga, categories: List, history: List, @@ -157,8 +167,8 @@ class MangaBackupRestorer( * @param manga the manga whose categories have to be restored. * @param categories the categories to restore. */ - private fun restoreCategories(manga: Manga, categories: List, backupCategories: List) { - val dbCategories = db.getCategories().executeAsBlocking() + private suspend fun restoreCategories(manga: Manga, categories: List, backupCategories: List) { + val dbCategories = getCategories.await() val mangaCategoriesToUpdate = ArrayList(categories.size) categories.forEach { backupCategoryOrder -> backupCategories.firstOrNull { @@ -184,7 +194,7 @@ class MangaBackupRestorer( * * @param history list containing history to be restored */ - internal fun restoreHistoryForManga(history: List) { + internal suspend fun restoreHistoryForManga(history: List) { // List containing history to be updated val historyToBeUpdated = ArrayList(history.size) for ((url, lastRead, readDuration) in history) { @@ -216,7 +226,7 @@ class MangaBackupRestorer( * @param manga the manga whose sync have to be restored. * @param tracks the track list to restore. */ - private fun restoreTrackForManga(manga: Manga, tracks: List) { + private suspend fun restoreTrackForManga(manga: Manga, tracks: List) { // Fix foreign keys with the current manga id tracks.map { it.manga_id = manga.id!! } @@ -253,8 +263,8 @@ class MangaBackupRestorer( } } - private fun restoreFilteredScanlatorsForManga(manga: Manga, filteredScanlators: List) { + private suspend fun restoreFilteredScanlatorsForManga(manga: Manga, filteredScanlators: List) { val actualList = ChapterUtil.getScanlators(manga.filtered_scanlators) + filteredScanlators - MangaUtil.setScanlatorFilter(db, manga, actualList.toSet()) + MangaUtil.setScanlatorFilter(updateManga, manga, actualList.toSet()) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index ed197846f4..3de8914a36 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import yokai.data.updateStrategyAdapter +import yokai.domain.manga.models.MangaUpdate import java.util.* // TODO: Transform into data class @@ -327,6 +328,30 @@ interface Manga : SManga { MangaCoverMetadata.addCoverColor(this, value.first, value.second) } + fun toMangaUpdate(): MangaUpdate { + return MangaUpdate( + id = id!!, + source = source, + url = url, + artist = artist, + author = author, + description = description, + genres = genre?.split(", ").orEmpty(), + title = title, + status = status, + thumbnailUrl = thumbnail_url, + favorite = favorite, + lastUpdate = last_update, + initialized = initialized, + viewerFlags = viewer_flags, + hideTitle = hide_title, + chapterFlags = chapter_flags, + dateAdded = date_added, + filteredScanlators = filtered_scanlators, + updateStrategy = update_strategy, + ) + } + companion object { // Generic filter that does not filter anything diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt index e25d558884..80cfb1d659 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt @@ -6,10 +6,8 @@ import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaChapter -import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver import eu.kanade.tachiyomi.data.database.resolvers.ChapterKnownBackupPutResolver import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver -import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.util.lang.sqLite @@ -88,15 +86,6 @@ interface ChapterQueries : DbProvider { fun insertChapters(chapters: List) = db.put().objects(chapters).prepare() - fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare() - - fun deleteChapters(chapters: List) = db.delete().objects(chapters).prepare() - - fun updateChaptersBackup(chapters: List) = db.put() - .objects(chapters) - .withPutResolver(ChapterBackupPutResolver()) - .prepare() - fun updateKnownChaptersBackup(chapters: List) = db.put() .objects(chapters) .withPutResolver(ChapterKnownBackupPutResolver()) @@ -111,9 +100,4 @@ interface ChapterQueries : DbProvider { .objects(chapters) .withPutResolver(ChapterProgressPutResolver()) .prepare() - - fun fixChaptersSourceOrder(chapters: List) = db.put() - .objects(chapters) - .withPutResolver(ChapterSourceOrderPutResolver()) - .prepare() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index 16dc7449c1..e02bf99da8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -9,10 +9,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount import eu.kanade.tachiyomi.data.database.resolvers.MangaDateAddedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver -import eu.kanade.tachiyomi.data.database.resolvers.MangaFilteredScanlatorsPutResolver -import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver -import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver -import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver import eu.kanade.tachiyomi.data.database.tables.ChapterTable @@ -91,55 +87,24 @@ interface MangaQueries : DbProvider { fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() - fun updateChapterFlags(manga: Manga) = db.put() - .`object`(manga) - .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags)) - .prepare() - - fun updateChapterFlags(manga: List) = db.put() - .objects(manga) - .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true)) - .prepare() - - fun updateViewerFlags(manga: Manga) = db.put() - .`object`(manga) - .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags)) - .prepare() - - fun updateViewerFlags(manga: List) = db.put() - .objects(manga) - .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true)) - .prepare() - - fun updateLastUpdated(manga: Manga) = db.put() - .`object`(manga) - .withPutResolver(MangaLastUpdatedPutResolver()) - .prepare() - + // FIXME: Migrate to SQLDelight, on halt: used by StorIO's inTransaction fun updateMangaFavorite(manga: Manga) = db.put() .`object`(manga) .withPutResolver(MangaFavoritePutResolver()) .prepare() + // FIXME: Migrate to SQLDelight, on halt: used by StorIO's inTransaction fun updateMangaAdded(manga: Manga) = db.put() .`object`(manga) .withPutResolver(MangaDateAddedPutResolver()) .prepare() + // FIXME: Migrate to SQLDelight, on halt: used by StorIO's inTransaction fun updateMangaTitle(manga: Manga) = db.put() .`object`(manga) .withPutResolver(MangaTitlePutResolver()) .prepare() - fun updateMangaInfo(manga: Manga) = db.put() - .`object`(manga) - .withPutResolver(MangaInfoPutResolver()) - .prepare() - - fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() - - fun deleteMangas(mangas: List) = db.delete().objects(mangas).prepare() - fun deleteMangasNotInLibraryBySourceIds(sourceIds: List) = db.delete() .byQuery( DeleteQuery.builder() @@ -166,14 +131,6 @@ interface MangaQueries : DbProvider { ) .prepare() - fun deleteMangas() = db.delete() - .byQuery( - DeleteQuery.builder() - .table(MangaTable.TABLE) - .build(), - ) - .prepare() - fun getReadNotInLibraryMangas() = db.get() .listOfObjects(Manga::class.java) .withQuery( @@ -182,12 +139,4 @@ interface MangaQueries : DbProvider { .build(), ) .prepare() - - fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java) - .withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare() - - fun updateMangaFilteredScanlators(manga: Manga) = db.put() - .`object`(manga) - .withPutResolver(MangaFilteredScanlatorsPutResolver()) - .prepare() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt index c3822b108c..911637481d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateJob.kt @@ -402,7 +402,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet val fetchedChapters = source.getChapterList(manga.copy()) if (fetchedChapters.isNotEmpty()) { - val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source) + val newChapters = syncChaptersWithSource(fetchedChapters, manga, source) if (newChapters.first.isNotEmpty()) { if (shouldDownload) { downloadChapters( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 991c8f7a95..3f8e8ced6b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -1136,7 +1136,7 @@ class LibraryPresenter( val mangaToDelete = mangas.distinctBy { it.id } .mapNotNull { if (it.id != null) MangaUpdate(it.id!!, favorite = false) else null } - withIOContext { updateManga.updateAll(mangaToDelete) } + withIOContext { updateManga.awaitAll(mangaToDelete) } getLibrary() } } @@ -1173,7 +1173,7 @@ class LibraryPresenter( val mangaToAdd = mangas.distinctBy { it.id } .mapNotNull { if (it.id != null) MangaUpdate(it.id!!, favorite = true) else null } - withIOContext { updateManga.updateAll(mangaToAdd) } + withIOContext { updateManga.awaitAll(mangaToAdd) } (view as? FilteredLibraryController)?.updateStatsPage() getLibrary() } @@ -1504,8 +1504,8 @@ class LibraryPresenter( fun updateDB() { val db: DatabaseHelper = Injekt.get() val getLibraryManga: GetLibraryManga by injectLazy() + val libraryManga = runBlocking { getLibraryManga.await() } db.inTransaction { - val libraryManga = runBlocking { getLibraryManga.await() } libraryManga.forEach { manga -> if (manga.date_added == 0L) { val chapters = db.getChapters(manga).executeAsBlocking() @@ -1529,8 +1529,8 @@ class LibraryPresenter( val db: DatabaseHelper = Injekt.get() val cc: CoverCache = Injekt.get() val getLibraryManga: GetLibraryManga by injectLazy() + val libraryManga = runBlocking { getLibraryManga.await() } db.inTransaction { - val libraryManga = runBlocking { getLibraryManga.await() } libraryManga.forEach { manga -> if (manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) { val file = cc.getCoverFile(manga) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index 98fa984ca9..73c2a59d02 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -53,10 +53,10 @@ import eu.kanade.tachiyomi.util.manga.MangaUtil import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.ImageUtil -import eu.kanade.tachiyomi.util.system.executeOnIO import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.launchUI +import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.widget.TriStateCheckBox import kotlinx.coroutines.Dispatchers @@ -74,6 +74,8 @@ import uy.kohesive.injekt.injectLazy import yokai.domain.chapter.interactor.GetAvailableScanlators import yokai.domain.chapter.interactor.GetChapters import yokai.domain.library.custom.model.CustomMangaInfo +import yokai.domain.manga.interactor.UpdateManga +import yokai.domain.manga.models.MangaUpdate import yokai.domain.storage.StorageManager import java.io.File import java.io.FileOutputStream @@ -92,6 +94,7 @@ class MangaDetailsPresenter( ) : BaseCoroutinePresenter(), DownloadQueue.DownloadListener { private val getAvailableScanlators: GetAvailableScanlators by injectLazy() private val getChapters: GetChapters by injectLazy() + private val updateManga: UpdateManga by injectLazy() private val customMangaManager: CustomMangaManager by injectLazy() private val mangaShortcutManager: MangaShortcutManager by injectLazy() @@ -404,7 +407,7 @@ class MangaDetailsPresenter( } val finChapters = chapters.await() if (finChapters.isNotEmpty()) { - val newChapters = syncChaptersWithSource(db, finChapters, manga, source) + val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) } if (newChapters.first.isNotEmpty()) { if (manga.shouldDownloadNewChapters(db, preferences)) { downloadChapters( @@ -469,7 +472,7 @@ class MangaDetailsPresenter( } isLoading = false try { - syncChaptersWithSource(db, chapters, manga, source) + syncChaptersWithSource(chapters, manga, source) getChapters() withContext(Dispatchers.Main) { @@ -555,14 +558,14 @@ class MangaDetailsPresenter( if (mangaSortMatchesDefault()) { manga.setSortToGlobal() } - asyncUpdateMangaAndChapters() + presenterScope.launchIO { asyncUpdateMangaAndChapters() } } fun setGlobalChapterSort(sort: Int, descend: Boolean) { preferences.sortChapterOrder().set(sort) preferences.chaptersDescAsDefault().set(descend) manga.setSortToGlobal() - asyncUpdateMangaAndChapters() + presenterScope.launchIO { asyncUpdateMangaAndChapters() } } fun mangaSortMatchesDefault(): Boolean { @@ -583,7 +586,7 @@ class MangaDetailsPresenter( fun resetSortingToDefault() { manga.setSortToGlobal() - asyncUpdateMangaAndChapters() + presenterScope.launchIO { asyncUpdateMangaAndChapters() } } /** @@ -613,7 +616,7 @@ class MangaDetailsPresenter( if (mangaFilterMatchesDefault()) { manga.setFilterToGlobal() } - asyncUpdateMangaAndChapters() + presenterScope.launchIO { asyncUpdateMangaAndChapters() } } /** @@ -623,7 +626,7 @@ class MangaDetailsPresenter( fun hideTitle(hide: Boolean) { manga.displayMode = if (hide) Manga.CHAPTER_DISPLAY_NUMBER else Manga.CHAPTER_DISPLAY_NAME manga.setFilterToLocal() - db.updateChapterFlags(manga).executeAsBlocking() + presenterScope.launchIO { updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) } if (mangaFilterMatchesDefault()) { manga.setFilterToGlobal() } @@ -632,7 +635,7 @@ class MangaDetailsPresenter( fun resetFilterToDefault() { manga.setFilterToGlobal() - asyncUpdateMangaAndChapters() + presenterScope.launchIO { asyncUpdateMangaAndChapters() } } fun setGlobalChapterFilters( @@ -663,15 +666,13 @@ class MangaDetailsPresenter( ) preferences.hideChapterTitlesByDefault().set(manga.hideChapterTitles) manga.setFilterToGlobal() - asyncUpdateMangaAndChapters() + presenterScope.launchIO { asyncUpdateMangaAndChapters() } } - private fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { - presenterScope.launch { - if (!justChapters) db.updateChapterFlags(manga).executeOnIO() - getChapters() - withContext(Dispatchers.Main) { view?.updateChapters(chapters) } - } + private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { + if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) + getChapters() + withUIContext { view?.updateChapters(chapters) } } private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true @@ -690,13 +691,15 @@ class MangaDetailsPresenter( } fun setScanlatorFilter(filteredScanlators: Set) { - val manga = manga - MangaUtil.setScanlatorFilter( - db, - manga, - if (filteredScanlators.size == allChapterScanlators.size) emptySet() else filteredScanlators - ) - asyncUpdateMangaAndChapters() + presenterScope.launchIO { + val manga = manga + MangaUtil.setScanlatorFilter( + updateManga, + manga, + if (filteredScanlators.size == allChapterScanlators.size) emptySet() else filteredScanlators + ) + asyncUpdateMangaAndChapters() + } } fun toggleFavorite(): Boolean { @@ -802,11 +805,23 @@ class MangaDetailsPresenter( } } manga.viewer_flags = -1 - db.updateViewerFlags(manga).executeAsBlocking() + presenterScope.launchIO { updateManga.await(MangaUpdate(manga.id!!, viewerFlags = manga.viewer_flags)) } } manga.status = status ?: SManga.UNKNOWN LocalSource(downloadManager.context).updateMangaInfo(manga, lang) - db.updateMangaInfo(manga).executeAsBlocking() + presenterScope.launchIO { + updateManga.await( + MangaUpdate( + manga.id!!, + title = manga.originalTitle, + author = manga.originalAuthor, + artist = manga.originalArtist, + description = manga.originalDescription, + genres = manga.originalGenre?.split(", ").orEmpty(), + status = manga.originalStatus, + ) + ) + } } else { var genre = if (!tags.isNullOrEmpty() && tags.joinToString(", ") != manga.originalGenre) { tags.map { tag -> tag.replaceFirstChar { it.titlecase(Locale.getDefault()) } } @@ -817,7 +832,7 @@ class MangaDetailsPresenter( if (seriesType != null) { genre = setSeriesType(seriesType, genre?.joinToString()) manga.viewer_flags = -1 - db.updateViewerFlags(manga).executeAsBlocking() + presenterScope.launchIO { updateManga.await(MangaUpdate(manga.id!!, viewerFlags = manga.viewer_flags)) } } val manga = CustomMangaInfo( mangaId = manga.id!!, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt index 45bc099379..4ea51f6ad2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/manga/process/MigrationListController.kt @@ -44,6 +44,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.system.withIOContext import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.isControllerVisible @@ -61,7 +62,7 @@ import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import uy.kohesive.injekt.injectLazy -import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.* import kotlin.coroutines.CoroutineContext class MigrationListController(bundle: Bundle? = null) : @@ -190,7 +191,6 @@ class MigrationListController(bundle: Bundle? = null) : val chapters = source.getChapterList(localManga) try { syncChaptersWithSource( - db, chapters, localManga, source, @@ -232,7 +232,7 @@ class MigrationListController(bundle: Bundle? = null) : emptyList() } withContext(Dispatchers.IO) { - syncChaptersWithSource(db, chapters, localManga, source) + syncChaptersWithSource(chapters, localManga, source) } localManga } else { @@ -368,7 +368,7 @@ class MigrationListController(bundle: Bundle? = null) : val localManga = smartSearchEngine.networkToLocalManga(manga, source.id) try { val chapters = source.getChapterList(localManga) - syncChaptersWithSource(db, chapters, localManga, source) + withIOContext { syncChaptersWithSource(chapters, localManga, source) } } catch (e: Exception) { return@async null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 2a93e5bc3d..9361588feb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -75,6 +75,8 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import yokai.domain.chapter.interactor.GetChapters import yokai.domain.download.DownloadPreferences +import yokai.domain.manga.interactor.UpdateManga +import yokai.domain.manga.models.MangaUpdate import yokai.domain.storage.StorageManager import java.util.* import java.util.concurrent.* @@ -94,6 +96,7 @@ class ReaderViewModel( private val downloadPreferences: DownloadPreferences = Injekt.get(), ) : ViewModel() { private val getChapters: GetChapters by injectLazy() + private val updateManga: UpdateManga by injectLazy() private val mutableState = MutableStateFlow(State()) val state = mutableState.asStateFlow() @@ -333,7 +336,6 @@ class ReaderViewModel( val chapterId: Long if (chapters.isNotEmpty()) { val newChapters = syncChaptersWithSource( - db, chapters, manga, delegatedSource.delegate!!, @@ -678,7 +680,7 @@ class ReaderViewModel( manga.viewer_flags = 0 } manga.readingModeType = if (cantSwitchToLTR) 0 else readerType - db.updateViewerFlags(manga).asRxObservable().subscribe() + viewModelScope.launchIO { updateManga.await(MangaUpdate(manga.id!!, viewerFlags = manga.viewer_flags)) } } return if (manga.readingModeType == 0) default else manga.readingModeType } @@ -689,9 +691,9 @@ class ReaderViewModel( fun setMangaReadingMode(readingModeType: Int) { val manga = manga ?: return - runBlocking(Dispatchers.IO) { + viewModelScope.launchIO { manga.readingModeType = readingModeType - db.updateViewerFlags(manga).executeAsBlocking() + updateManga.await(MangaUpdate(manga.id!!, viewerFlags = manga.viewer_flags)) val currChapters = state.value.viewerChapters if (currChapters != null) { // Save current page @@ -726,12 +728,11 @@ class ReaderViewModel( fun setMangaOrientationType(rotationType: Int) { val manga = manga ?: return this.manga?.orientationType = rotationType - db.updateViewerFlags(manga).executeAsBlocking() Logger.i { "Manga orientation is ${manga.orientationType}" } viewModelScope.launchIO { - db.updateViewerFlags(manga).executeAsBlocking() + updateManga.await(MangaUpdate(manga.id!!, viewerFlags = manga.viewer_flags)) val currChapters = state.value.viewerChapters if (currChapters != null) { mutableState.update { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index 7deac40ecc..befbf80fd0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.util.chapter -import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadManager @@ -8,7 +7,17 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.chapter.ChapterFilter.Companion.filterChaptersByScanlators +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy +import yokai.data.DatabaseHandler +import yokai.domain.chapter.interactor.DeleteChapters +import yokai.domain.chapter.interactor.GetChapters +import yokai.domain.chapter.interactor.InsertChapters +import yokai.domain.chapter.interactor.UpdateChapters +import yokai.domain.chapter.models.ChapterUpdate +import yokai.domain.manga.interactor.UpdateManga +import yokai.domain.manga.models.MangaUpdate import java.util.* /** @@ -20,11 +29,16 @@ import java.util.* * @param source the source of the chapters. * @return a pair of new insertions and deletions. */ -fun syncChaptersWithSource( - db: DatabaseHelper, +suspend fun syncChaptersWithSource( rawSourceChapters: List, manga: Manga, source: Source, + deleteChapters: DeleteChapters = Injekt.get(), + getChapters: GetChapters = Injekt.get(), + insertChapters: InsertChapters = Injekt.get(), + updateChapters: UpdateChapters = Injekt.get(), + updateManga: UpdateManga = Injekt.get(), + handler: DatabaseHandler = Injekt.get(), ): Pair, List> { if (rawSourceChapters.isEmpty()) { throw Exception("No chapters found") @@ -32,7 +46,7 @@ fun syncChaptersWithSource( val downloadManager: DownloadManager by injectLazy() // Chapters from db. - val dbChapters = db.getChapters(manga).executeAsBlocking() + val dbChapters = getChapters.await(manga) val sourceChapters = rawSourceChapters .distinctBy { it.url } @@ -49,7 +63,7 @@ fun syncChaptersWithSource( val toAdd = mutableListOf() // Chapters whose metadata have changed. - val toChange = mutableListOf() + val toChange = mutableListOf() for (sourceChapter in sourceChapters) { val dbChapter = dbChapters.find { it.url == sourceChapter.url } @@ -71,12 +85,15 @@ fun syncChaptersWithSource( ) { downloadManager.renameChapter(source, manga, dbChapter, sourceChapter) } - dbChapter.scanlator = sourceChapter.scanlator - dbChapter.name = sourceChapter.name - dbChapter.date_upload = sourceChapter.date_upload - dbChapter.chapter_number = sourceChapter.chapter_number - dbChapter.source_order = sourceChapter.source_order - toChange.add(dbChapter) + val update = ChapterUpdate( + dbChapter.id!!, + scanlator = sourceChapter.scanlator, + name = sourceChapter.name, + dateUpload = sourceChapter.date_upload, + chapterNumber = sourceChapter.chapter_number.toDouble(), + sourceOrder = sourceChapter.source_order.toLong(), + ) + toChange.add(update) } } } @@ -101,73 +118,84 @@ fun syncChaptersWithSource( val newestDate = dbChapters.maxOfOrNull { it.date_upload } ?: 0L if (newestDate != 0L && newestDate != manga.last_update) { manga.last_update = newestDate - db.updateLastUpdated(manga).executeAsBlocking() + val update = MangaUpdate(manga.id!!, lastUpdate = manga.last_update) + updateManga.await(update) } return Pair(emptyList(), emptyList()) } val readded = mutableListOf() - db.inTransaction { - val deletedChapterNumbers = TreeSet() - val deletedReadChapterNumbers = TreeSet() - if (toDelete.isNotEmpty()) { - for (c in toDelete) { - if (c.read) { - deletedReadChapterNumbers.add(c.chapter_number) - } - deletedChapterNumbers.add(c.chapter_number) + val deletedChapterNumbers = TreeSet() + val deletedReadChapterNumbers = TreeSet() + if (toDelete.isNotEmpty()) { + for (c in toDelete) { + if (c.read) { + deletedReadChapterNumbers.add(c.chapter_number) } - db.deleteChapters(toDelete).executeAsBlocking() + deletedChapterNumbers.add(c.chapter_number) } - - if (toAdd.isNotEmpty()) { - // Set the date fetch for new items in reverse order to allow another sorting method. - // Sources MUST return the chapters from most to less recent, which is common. - var now = Date().time - - for (i in toAdd.indices.reversed()) { - val chapter = toAdd[i] - chapter.date_fetch = now++ - if (chapter.isRecognizedNumber && chapter.chapter_number in deletedChapterNumbers) { - // Try to mark already read chapters as read when the source deletes them - if (chapter.chapter_number in deletedReadChapterNumbers) { - chapter.read = true - } - // Try to to use the fetch date it originally had to not pollute 'Updates' tab - toDelete.filter { it.chapter_number == chapter.chapter_number } - .minByOrNull { it.date_fetch }?.let { - chapter.date_fetch = it.date_fetch - } - - readded.add(chapter) - } - } - val chapters = db.insertChapters(toAdd).executeAsBlocking() - toAdd.forEach { chapter -> - chapter.id = chapters.results().getValue(chapter).insertedId() - } - } - - if (toChange.isNotEmpty()) { - db.insertChapters(toChange).executeAsBlocking() - } - - // Fix order in source. - db.fixChaptersSourceOrder(sourceChapters).executeAsBlocking() - - // Set this manga as updated since chapters were changed - val newestChapterDate = db.getChapters(manga).executeAsBlocking() - .maxOfOrNull { it.date_upload } ?: 0L - if (newestChapterDate == 0L) { - if (toAdd.isNotEmpty()) { - manga.last_update = Date().time - } - } else { - manga.last_update = newestChapterDate - } - db.updateLastUpdated(manga).executeAsBlocking() + deleteChapters.awaitAll(toDelete) } + + if (toAdd.isNotEmpty()) { + // Set the date fetch for new items in reverse order to allow another sorting method. + // Sources MUST return the chapters from most to less recent, which is common. + var now = Date().time + + for (i in toAdd.indices.reversed()) { + val chapter = toAdd[i] + chapter.date_fetch = now++ + if (chapter.isRecognizedNumber && chapter.chapter_number in deletedChapterNumbers) { + // Try to mark already read chapters as read when the source deletes them + if (chapter.chapter_number in deletedReadChapterNumbers) { + chapter.read = true + } + // Try to to use the fetch date it originally had to not pollute 'Updates' tab + toDelete.filter { it.chapter_number == chapter.chapter_number } + .minByOrNull { it.date_fetch }?.let { + chapter.date_fetch = it.date_fetch + } + + readded.add(chapter) + } + } + toAdd.forEach { chapter -> + chapter.id = insertChapters.await(chapter) + } + } + + if (toChange.isNotEmpty()) { + updateChapters.awaitAll(toChange) + } + + // Fix order in source. + handler.await(inTransaction = true) { + sourceChapters.forEach { chapter -> + if (chapter.manga_id == null) return@forEach + chaptersQueries.fixSourceOrder( + url = chapter.url, + mangaId = chapter.manga_id!!, + sourceOrder = chapter.source_order.toLong(), + ) + } + } + + var mangaUpdate: MangaUpdate? = null + // Set this manga as updated since chapters were changed + val newestChapterDate = getChapters.await(manga) + .maxOfOrNull { it.date_upload } ?: 0L + if (newestChapterDate == 0L) { + if (toAdd.isNotEmpty()) { + manga.last_update = Date().time + mangaUpdate = MangaUpdate(manga.id!!, lastUpdate = manga.last_update) + } + } else { + manga.last_update = newestChapterDate + mangaUpdate = MangaUpdate(manga.id!!, lastUpdate = manga.last_update) + } + mangaUpdate?.let { updateManga.await(it) } + val reAddedSet = readded.toSet() return Pair( toAdd.subtract(reAddedSet).toList().filterChaptersByScanlators(manga), diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaUtil.kt index 972d59fbc2..7235b9fc56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/manga/MangaUtil.kt @@ -1,13 +1,17 @@ package eu.kanade.tachiyomi.util.manga -import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.util.chapter.ChapterUtil +import yokai.domain.manga.interactor.UpdateManga +import yokai.domain.manga.models.MangaUpdate object MangaUtil { - fun setScanlatorFilter(db: DatabaseHelper, manga: Manga, filteredScanlators: Set) { + suspend fun setScanlatorFilter(updateManga: UpdateManga, manga: Manga, filteredScanlators: Set) { + if (manga.id == null) return + manga.filtered_scanlators = if (filteredScanlators.isEmpty()) null else ChapterUtil.getScanlatorString(filteredScanlators) - db.updateMangaFilteredScanlators(manga).executeAsBlocking() + + updateManga.await(MangaUpdate(manga.id!!, filteredScanlators = manga.filtered_scanlators)) } } diff --git a/app/src/main/java/yokai/core/di/DomainModule.kt b/app/src/main/java/yokai/core/di/DomainModule.kt index 06117fe46f..34f36c785e 100644 --- a/app/src/main/java/yokai/core/di/DomainModule.kt +++ b/app/src/main/java/yokai/core/di/DomainModule.kt @@ -13,8 +13,11 @@ import yokai.data.manga.MangaRepositoryImpl import yokai.domain.category.CategoryRepository import yokai.domain.category.interactor.GetCategories import yokai.domain.chapter.ChapterRepository +import yokai.domain.chapter.interactor.DeleteChapters import yokai.domain.chapter.interactor.GetAvailableScanlators import yokai.domain.chapter.interactor.GetChapters +import yokai.domain.chapter.interactor.InsertChapters +import yokai.domain.chapter.interactor.UpdateChapters import yokai.domain.extension.interactor.TrustExtension import yokai.domain.extension.repo.ExtensionRepoRepository import yokai.domain.extension.repo.interactor.CreateExtensionRepo @@ -30,6 +33,7 @@ import yokai.domain.library.custom.interactor.GetCustomManga import yokai.domain.library.custom.interactor.RelinkCustomManga import yokai.domain.manga.MangaRepository import yokai.domain.manga.interactor.GetLibraryManga +import yokai.domain.manga.interactor.GetManga import yokai.domain.manga.interactor.InsertManga import yokai.domain.manga.interactor.UpdateManga @@ -52,13 +56,17 @@ class DomainModule : InjektModule { addFactory { RelinkCustomManga(get()) } addSingletonFactory { MangaRepositoryImpl(get()) } + addFactory { GetManga(get()) } addFactory { GetLibraryManga(get()) } addFactory { InsertManga(get()) } addFactory { UpdateManga(get()) } addSingletonFactory { ChapterRepositoryImpl(get()) } + addFactory { DeleteChapters(get()) } addFactory { GetAvailableScanlators(get()) } addFactory { GetChapters(get()) } + addFactory { InsertChapters(get()) } + addFactory { UpdateChapters(get()) } addSingletonFactory { CategoryRepositoryImpl(get()) } addFactory { GetCategories(get()) } diff --git a/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt b/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt index 9d471e5f10..7c7274122d 100644 --- a/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt +++ b/app/src/main/java/yokai/data/chapter/ChapterRepositoryImpl.kt @@ -1,10 +1,12 @@ package yokai.data.chapter +import co.touchlab.kermit.Logger import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.util.system.toInt import kotlinx.coroutines.flow.Flow import yokai.data.DatabaseHandler import yokai.domain.chapter.ChapterRepository +import yokai.domain.chapter.models.ChapterUpdate class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepository { override suspend fun getChapters(mangaId: Long, filterScanlators: Boolean): List = @@ -18,4 +20,93 @@ class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepos override fun getScanlatorsByChapterAsFlow(mangaId: Long): Flow> = handler.subscribeToList { chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } } + + override suspend fun delete(chapter: Chapter) = + try { + partialDelete(chapter) + true + } catch (e: Exception) { + Logger.e(e) { "Failed to delete chapter with id '${chapter.id}'" } + false + } + + override suspend fun deleteAll(chapters: List) = + try { + partialDelete(*chapters.toTypedArray()) + true + } catch (e: Exception) { + Logger.e(e) { "Failed to bulk delete chapters" } + false + } + + private suspend fun partialDelete(vararg chapters: Chapter) { + handler.await(inTransaction = true) { + chapters.forEach { chapter -> + if (chapter.id == null) return@forEach + chaptersQueries.delete(chapter.id!!) + } + } + } + + override suspend fun update(update: ChapterUpdate): Boolean = + try { + partialUpdate(update) + true + } catch (e: Exception) { + Logger.e(e) { "Failed to update chapter with id '${update.id}'" } + false + } + + override suspend fun updateAll(updates: List): Boolean = + try { + partialUpdate(*updates.toTypedArray()) + true + } catch (e: Exception) { + Logger.e(e) { "Failed to bulk update chapters" } + false + } + + private suspend fun partialUpdate(vararg updates: ChapterUpdate) { + handler.await(inTransaction = true) { + updates.forEach { update -> + chaptersQueries.update( + chapterId = update.id, + mangaId = update.mangaId, + url = update.url, + name = update.name, + scanlator = update.scanlator, + read = update.read, + bookmark = update.bookmark, + lastPageRead = update.lastPageRead, + pagesLeft = update.pagesLeft, + chapterNumber = update.chapterNumber, + sourceOrder = update.sourceOrder, + dateFetch = update.dateFetch, + dateUpload = update.dateUpload + ) + } + } + } + + override suspend fun insert(chapter: Chapter): Long? { + if (chapter.manga_id == null) return null + + return handler.awaitOneOrNullExecutable(inTransaction = true) { + chaptersQueries.insert( + mangaId = chapter.manga_id!!, + url = chapter.url, + name = chapter.name, + scanlator = chapter.scanlator, + read = chapter.read, + bookmark = chapter.bookmark, + lastPageRead = chapter.last_page_read.toLong(), + pagesLeft = chapter.pages_left.toLong(), + chapterNumber = chapter.chapter_number.toDouble(), + sourceOrder = chapter.source_order.toLong(), + dateFetch = chapter.date_fetch, + dateUpload = chapter.date_upload, + ) + chaptersQueries.selectLastInsertedRowId() + } + } } diff --git a/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt index 65c88d485b..01d64aa8d5 100644 --- a/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/yokai/data/manga/MangaRepositoryImpl.kt @@ -11,10 +11,16 @@ import yokai.domain.manga.MangaRepository import yokai.domain.manga.models.MangaUpdate class MangaRepositoryImpl(private val handler: DatabaseHandler) : MangaRepository { - override suspend fun getManga(): List = + override suspend fun getMangaList(): List = handler.awaitList { mangasQueries.findAll(Manga::mapper) } - override fun getMangaAsFlow(): Flow> = + override suspend fun getMangaByUrlAndSource(url: String, source: Long): Manga? = + handler.awaitOneOrNull { mangasQueries.findByUrlAndSource(url, source, Manga::mapper) } + + override suspend fun getMangaById(id: Long): Manga? = + handler.awaitOneOrNull { mangasQueries.findById(id, Manga::mapper) } + + override fun getMangaListAsFlow(): Flow> = handler.subscribeToList { mangasQueries.findAll(Manga::mapper) } override suspend fun getLibraryManga(): List = diff --git a/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt b/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt index c241b3f291..bdc5829a71 100644 --- a/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt +++ b/app/src/main/java/yokai/domain/chapter/ChapterRepository.kt @@ -2,6 +2,7 @@ package yokai.domain.chapter import eu.kanade.tachiyomi.data.database.models.Chapter import kotlinx.coroutines.flow.Flow +import yokai.domain.chapter.models.ChapterUpdate interface ChapterRepository { suspend fun getChapters(mangaId: Long, filterScanlators: Boolean): List @@ -9,4 +10,12 @@ interface ChapterRepository { suspend fun getScanlatorsByChapter(mangaId: Long): List fun getScanlatorsByChapterAsFlow(mangaId: Long): Flow> + + suspend fun delete(chapter: Chapter): Boolean + suspend fun deleteAll(chapters: List): Boolean + + suspend fun update(update: ChapterUpdate): Boolean + suspend fun updateAll(updates: List): Boolean + + suspend fun insert(chapter: Chapter): Long? } diff --git a/app/src/main/java/yokai/domain/chapter/interactor/DeleteChapters.kt b/app/src/main/java/yokai/domain/chapter/interactor/DeleteChapters.kt new file mode 100644 index 0000000000..3eed201380 --- /dev/null +++ b/app/src/main/java/yokai/domain/chapter/interactor/DeleteChapters.kt @@ -0,0 +1,11 @@ +package yokai.domain.chapter.interactor + +import eu.kanade.tachiyomi.data.database.models.Chapter +import yokai.domain.chapter.ChapterRepository + +class DeleteChapters( + private val chapterRepository: ChapterRepository, +) { + suspend fun await(chapter: Chapter) = chapterRepository.delete(chapter) + suspend fun awaitAll(chapters: List) = chapterRepository.deleteAll(chapters) +} diff --git a/app/src/main/java/yokai/domain/chapter/interactor/InsertChapters.kt b/app/src/main/java/yokai/domain/chapter/interactor/InsertChapters.kt new file mode 100644 index 0000000000..22cba039d7 --- /dev/null +++ b/app/src/main/java/yokai/domain/chapter/interactor/InsertChapters.kt @@ -0,0 +1,10 @@ +package yokai.domain.chapter.interactor + +import eu.kanade.tachiyomi.data.database.models.Chapter +import yokai.domain.chapter.ChapterRepository + +class InsertChapters( + private val chapterRepository: ChapterRepository, +) { + suspend fun await(chapter: Chapter) = chapterRepository.insert(chapter) +} diff --git a/app/src/main/java/yokai/domain/chapter/interactor/UpdateChapters.kt b/app/src/main/java/yokai/domain/chapter/interactor/UpdateChapters.kt new file mode 100644 index 0000000000..65f6668dbd --- /dev/null +++ b/app/src/main/java/yokai/domain/chapter/interactor/UpdateChapters.kt @@ -0,0 +1,11 @@ +package yokai.domain.chapter.interactor + +import yokai.domain.chapter.ChapterRepository +import yokai.domain.chapter.models.ChapterUpdate + +class UpdateChapters( + private val chapterRepository: ChapterRepository, +) { + suspend fun await(chapter: ChapterUpdate) = chapterRepository.update(chapter) + suspend fun awaitAll(chapters: List) = chapterRepository.updateAll(chapters) +} diff --git a/app/src/main/java/yokai/domain/manga/MangaRepository.kt b/app/src/main/java/yokai/domain/manga/MangaRepository.kt index 6388153d27..a2f102543e 100644 --- a/app/src/main/java/yokai/domain/manga/MangaRepository.kt +++ b/app/src/main/java/yokai/domain/manga/MangaRepository.kt @@ -6,8 +6,10 @@ import kotlinx.coroutines.flow.Flow import yokai.domain.manga.models.MangaUpdate interface MangaRepository { - suspend fun getManga(): List - fun getMangaAsFlow(): Flow> + suspend fun getMangaList(): List + suspend fun getMangaByUrlAndSource(url: String, source: Long): Manga? + suspend fun getMangaById(id: Long): Manga? + fun getMangaListAsFlow(): Flow> suspend fun getLibraryManga(): List fun getLibraryMangaAsFlow(): Flow> suspend fun update(update: MangaUpdate): Boolean diff --git a/app/src/main/java/yokai/domain/manga/interactor/GetManga.kt b/app/src/main/java/yokai/domain/manga/interactor/GetManga.kt new file mode 100644 index 0000000000..a5d00ad9ee --- /dev/null +++ b/app/src/main/java/yokai/domain/manga/interactor/GetManga.kt @@ -0,0 +1,13 @@ +package yokai.domain.manga.interactor + +import yokai.domain.manga.MangaRepository + +class GetManga ( + private val mangaRepository: MangaRepository, +) { + suspend fun awaitAll() = mangaRepository.getMangaList() + fun subscribeAll() = mangaRepository.getMangaListAsFlow() + + suspend fun awaitByUrlAndSource(url: String, source: Long) = mangaRepository.getMangaByUrlAndSource(url, source) + suspend fun awaitById(id: Long) = mangaRepository.getMangaById(id) +} diff --git a/app/src/main/java/yokai/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/yokai/domain/manga/interactor/UpdateManga.kt index 310206e517..a4e9a67a76 100644 --- a/app/src/main/java/yokai/domain/manga/interactor/UpdateManga.kt +++ b/app/src/main/java/yokai/domain/manga/interactor/UpdateManga.kt @@ -6,6 +6,6 @@ import yokai.domain.manga.models.MangaUpdate class UpdateManga ( private val mangaRepository: MangaRepository, ) { - suspend fun update(update: MangaUpdate) = mangaRepository.update(update) - suspend fun updateAll(updates: List) = mangaRepository.updateAll(updates) + suspend fun await(update: MangaUpdate) = mangaRepository.update(update) + suspend fun awaitAll(updates: List) = mangaRepository.updateAll(updates) } diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq b/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq index 9f003957ab..20007f8d46 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/chapters.sq @@ -36,3 +36,34 @@ getScanlatorsByMangaId: SELECT scanlator FROM chapters WHERE manga_id = :mangaId; + +delete: +DELETE FROM chapters +WHERE _id = :chapterId; + +update: +UPDATE chapters SET + manga_id = coalesce(:mangaId, manga_id), + url = coalesce(:url, url), + name = coalesce(:name, name), + scanlator = coalesce(:scanlator, scanlator), + read = coalesce(:read, read), + bookmark = coalesce(:bookmark, bookmark), + last_page_read = coalesce(:lastPageRead, last_page_read), + pages_left = coalesce(:pagesLeft, pages_left), + chapter_number = coalesce(:chapterNumber, chapter_number), + source_order = coalesce(:sourceOrder, source_order), + date_fetch = coalesce(:dateFetch, date_fetch), + date_upload = coalesce(:dateUpload, date_upload) +WHERE _id = :chapterId; + +fixSourceOrder: +UPDATE chapters SET source_order = :sourceOrder +WHERE url = :url AND manga_id = :mangaId; + +insert: +INSERT INTO chapters (manga_id, url, name, scanlator, read, bookmark, last_page_read, pages_left, chapter_number, source_order, date_fetch, date_upload) +VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :pagesLeft, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload); + +selectLastInsertedRowId: +SELECT last_insert_rowid(); diff --git a/data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq b/data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq index 171b2ac5bf..27e01754ef 100644 --- a/data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/commonMain/sqldelight/tachiyomi/data/mangas.sq @@ -30,6 +30,16 @@ findAll: SELECT * FROM mangas; +findByUrlAndSource: +SELECT * +FROM mangas +WHERE url = :url AND source = :source; + +findById: +SELECT * +FROM mangas +WHERE _id = :mangaId; + insert: INSERT INTO mangas (source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, initialized, viewer, hide_title, chapter_flags, date_added, filtered_scanlators, update_strategy) VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :initialized, :viewer, :hideTitle, :chapterFlags, :dateAdded, :filteredScanlators, :updateStrategy); diff --git a/domain/src/commonMain/kotlin/yokai/domain/chapter/models/ChapterUpdate.kt b/domain/src/commonMain/kotlin/yokai/domain/chapter/models/ChapterUpdate.kt new file mode 100644 index 0000000000..8d43a76c09 --- /dev/null +++ b/domain/src/commonMain/kotlin/yokai/domain/chapter/models/ChapterUpdate.kt @@ -0,0 +1,17 @@ +package yokai.domain.chapter.models + +data class ChapterUpdate( + val id: Long, + val mangaId: Long? = null, + val url: String? = null, + val name: String? = null, + val scanlator: String? = null, + val read: Boolean? = null, + val bookmark: Boolean? = null, + val lastPageRead: Long? = null, + val pagesLeft: Long? = null, + val chapterNumber: Double? = null, + val sourceOrder: Long? = null, + val dateFetch: Long? = null, + val dateUpload: Long? = null, +)