refactor: Migrated most manga queries and some chapter queries to SQLDelight

This commit is contained in:
Ahmad Ansori Palembani 2024-06-19 11:12:37 +07:00
parent 5ed2934b73
commit f6080cd5eb
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
26 changed files with 450 additions and 211 deletions

View file

@ -28,6 +28,9 @@
- Update dependency com.google.gms:google-services to v4.4.2 - Update dependency com.google.gms:google-services to v4.4.2
- Add crashlytics integration for Kermit - Add crashlytics integration for Kermit
- Replace ProgressBar with ProgressIndicator from Material3 to improve UI consistency - 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 Japenese translation
- Update dependency com.github.tachiyomiorg:unifile to a9de196cc7 - Update dependency com.github.tachiyomiorg:unifile to a9de196cc7

View file

@ -4,16 +4,17 @@ import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import yokai.domain.category.interactor.GetCategories
class CategoriesBackupRestorer( class CategoriesBackupRestorer(
private val db: DatabaseHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(),
) { ) {
@Suppress("RedundantSuspendModifier")
suspend fun restoreCategories(backupCategories: List<BackupCategory>, onComplete: () -> Unit) { suspend fun restoreCategories(backupCategories: List<BackupCategory>, 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 { db.inTransaction {
// Get categories from file and from db
val dbCategories = db.getCategories().executeAsBlocking()
// Iterate over them // Iterate over them
backupCategories.map { it.getCategoryImpl() }.forEach { category -> backupCategories.map { it.getCategoryImpl() }.forEach { category ->
// Used to know if the category is already in the db // Used to know if the category is already in the db

View file

@ -17,14 +17,24 @@ import eu.kanade.tachiyomi.util.manga.MangaUtil
import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.launchNow
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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.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 import kotlin.math.max
class MangaBackupRestorer( class MangaBackupRestorer(
private val db: DatabaseHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(),
private val customMangaManager: CustomMangaManager = 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, backupManga: BackupManga,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
onComplete: (Manga) -> Unit, onComplete: (Manga) -> Unit,
@ -40,19 +50,19 @@ class MangaBackupRestorer(
val filteredScanlators = backupManga.excludedScanlators val filteredScanlators = backupManga.excludedScanlators
try { try {
val dbManga = db.getManga(manga.url, manga.source).executeAsBlocking() val dbManga = getManga.awaitByUrlAndSource(manga.url, manga.source)
if (dbManga == null) { if (dbManga == null) {
// Manga not in database // Manga not in database
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories, filteredScanlators, customManga) restoreNewManga(manga, chapters, categories, history, tracks, backupCategories, filteredScanlators, customManga)
} else { } else {
// Manga in database // Manga in database
// Copy information from manga already in database // Copy information from manga already in database
manga.id = dbManga.id manga.id = dbManga.id
manga.filtered_scanlators = dbManga.filtered_scanlators manga.filtered_scanlators = dbManga.filtered_scanlators
manga.copyFrom(dbManga) manga.copyFrom(dbManga)
db.insertManga(manga).executeAsBlocking() updateManga.await(manga.toMangaUpdate())
// Fetch rest of manga information // 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) { } catch (e: Exception) {
onError(manga, e) onError(manga, e)
@ -69,7 +79,7 @@ class MangaBackupRestorer(
* @param chapters chapters of manga that needs updating * @param chapters chapters of manga that needs updating
* @param categories categories that need updating * @param categories categories that need updating
*/ */
private fun restoreExistingManga( private suspend fun restoreNewManga(
manga: Manga, manga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
@ -81,7 +91,7 @@ class MangaBackupRestorer(
) { ) {
val fetchedManga = manga.also { val fetchedManga = manga.also {
it.initialized = it.description != null it.initialized = it.description != null
it.id = db.insertManga(it).executeAsBlocking().insertedId() it.id = insertManga.await(it)
} }
fetchedManga.id ?: return fetchedManga.id ?: return
@ -89,7 +99,7 @@ class MangaBackupRestorer(
restoreExtras(fetchedManga, categories, history, tracks, backupCategories, filteredScanlators, customManga) restoreExtras(fetchedManga, categories, history, tracks, backupCategories, filteredScanlators, customManga)
} }
private fun restoreNewManga( private suspend fun restoreExistingManga(
backupManga: Manga, backupManga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categories: List<Int>,
@ -103,8 +113,8 @@ class MangaBackupRestorer(
restoreExtras(backupManga, categories, history, tracks, backupCategories, filteredScanlators, customManga) restoreExtras(backupManga, categories, history, tracks, backupCategories, filteredScanlators, customManga)
} }
private fun restoreChapters(manga: Manga, chapters: List<Chapter>) { private suspend fun restoreChapters(manga: Manga, chapters: List<Chapter>) {
val dbChapters = db.getChapters(manga).executeAsBlocking() val dbChapters = getChapters.await(manga)
chapters.forEach { chapter -> chapters.forEach { chapter ->
val dbChapter = dbChapters.find { it.url == chapter.url } val dbChapter = dbChapters.find { it.url == chapter.url }
@ -130,7 +140,7 @@ class MangaBackupRestorer(
newChapters[false]?.let { db.insertChapters(it).executeAsBlocking() } newChapters[false]?.let { db.insertChapters(it).executeAsBlocking() }
} }
private fun restoreExtras( private suspend fun restoreExtras(
manga: Manga, manga: Manga,
categories: List<Int>, categories: List<Int>,
history: List<BackupHistory>, history: List<BackupHistory>,
@ -157,8 +167,8 @@ class MangaBackupRestorer(
* @param manga the manga whose categories have to be restored. * @param manga the manga whose categories have to be restored.
* @param categories the categories to restore. * @param categories the categories to restore.
*/ */
private fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) { private suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
val dbCategories = db.getCategories().executeAsBlocking() val dbCategories = getCategories.await()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size) val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
categories.forEach { backupCategoryOrder -> categories.forEach { backupCategoryOrder ->
backupCategories.firstOrNull { backupCategories.firstOrNull {
@ -184,7 +194,7 @@ class MangaBackupRestorer(
* *
* @param history list containing history to be restored * @param history list containing history to be restored
*/ */
internal fun restoreHistoryForManga(history: List<BackupHistory>) { internal suspend fun restoreHistoryForManga(history: List<BackupHistory>) {
// List containing history to be updated // List containing history to be updated
val historyToBeUpdated = ArrayList<History>(history.size) val historyToBeUpdated = ArrayList<History>(history.size)
for ((url, lastRead, readDuration) in history) { for ((url, lastRead, readDuration) in history) {
@ -216,7 +226,7 @@ class MangaBackupRestorer(
* @param manga the manga whose sync have to be restored. * @param manga the manga whose sync have to be restored.
* @param tracks the track list to restore. * @param tracks the track list to restore.
*/ */
private fun restoreTrackForManga(manga: Manga, tracks: List<Track>) { private suspend fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
// Fix foreign keys with the current manga id // Fix foreign keys with the current manga id
tracks.map { it.manga_id = manga.id!! } tracks.map { it.manga_id = manga.id!! }
@ -253,8 +263,8 @@ class MangaBackupRestorer(
} }
} }
private fun restoreFilteredScanlatorsForManga(manga: Manga, filteredScanlators: List<String>) { private suspend fun restoreFilteredScanlatorsForManga(manga: Manga, filteredScanlators: List<String>) {
val actualList = ChapterUtil.getScanlators(manga.filtered_scanlators) + filteredScanlators val actualList = ChapterUtil.getScanlators(manga.filtered_scanlators) + filteredScanlators
MangaUtil.setScanlatorFilter(db, manga, actualList.toSet()) MangaUtil.setScanlatorFilter(updateManga, manga, actualList.toSet())
} }
} }

View file

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import yokai.data.updateStrategyAdapter import yokai.data.updateStrategyAdapter
import yokai.domain.manga.models.MangaUpdate
import java.util.* import java.util.*
// TODO: Transform into data class // TODO: Transform into data class
@ -327,6 +328,30 @@ interface Manga : SManga {
MangaCoverMetadata.addCoverColor(this, value.first, value.second) 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 { companion object {
// Generic filter that does not filter anything // Generic filter that does not filter anything

View file

@ -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.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter 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.ChapterKnownBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver 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.resolvers.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.util.lang.sqLite import eu.kanade.tachiyomi.util.lang.sqLite
@ -88,15 +86,6 @@ interface ChapterQueries : DbProvider {
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare() fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterBackupPutResolver())
.prepare()
fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put() fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters) .objects(chapters)
.withPutResolver(ChapterKnownBackupPutResolver()) .withPutResolver(ChapterKnownBackupPutResolver())
@ -111,9 +100,4 @@ interface ChapterQueries : DbProvider {
.objects(chapters) .objects(chapters)
.withPutResolver(ChapterProgressPutResolver()) .withPutResolver(ChapterProgressPutResolver())
.prepare() .prepare()
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver())
.prepare()
} }

View file

@ -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.models.SourceIdMangaCount
import eu.kanade.tachiyomi.data.database.resolvers.MangaDateAddedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaDateAddedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver 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.MangaTitlePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable 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 insertManga(manga: Manga) = db.put().`object`(manga).prepare()
fun updateChapterFlags(manga: Manga) = db.put() // FIXME: Migrate to SQLDelight, on halt: used by StorIO's inTransaction
.`object`(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
.prepare()
fun updateChapterFlags(manga: List<Manga>) = 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<Manga>) = 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()
fun updateMangaFavorite(manga: Manga) = db.put() fun updateMangaFavorite(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaFavoritePutResolver()) .withPutResolver(MangaFavoritePutResolver())
.prepare() .prepare()
// FIXME: Migrate to SQLDelight, on halt: used by StorIO's inTransaction
fun updateMangaAdded(manga: Manga) = db.put() fun updateMangaAdded(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaDateAddedPutResolver()) .withPutResolver(MangaDateAddedPutResolver())
.prepare() .prepare()
// FIXME: Migrate to SQLDelight, on halt: used by StorIO's inTransaction
fun updateMangaTitle(manga: Manga) = db.put() fun updateMangaTitle(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaTitlePutResolver()) .withPutResolver(MangaTitlePutResolver())
.prepare() .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<Manga>) = db.delete().objects(mangas).prepare()
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete() fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
@ -166,14 +131,6 @@ interface MangaQueries : DbProvider {
) )
.prepare() .prepare()
fun deleteMangas() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.build(),
)
.prepare()
fun getReadNotInLibraryMangas() = db.get() fun getReadNotInLibraryMangas() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery( .withQuery(
@ -182,12 +139,4 @@ interface MangaQueries : DbProvider {
.build(), .build(),
) )
.prepare() .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()
} }

View file

@ -402,7 +402,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val fetchedChapters = source.getChapterList(manga.copy()) val fetchedChapters = source.getChapterList(manga.copy())
if (fetchedChapters.isNotEmpty()) { if (fetchedChapters.isNotEmpty()) {
val newChapters = syncChaptersWithSource(db, fetchedChapters, manga, source) val newChapters = syncChaptersWithSource(fetchedChapters, manga, source)
if (newChapters.first.isNotEmpty()) { if (newChapters.first.isNotEmpty()) {
if (shouldDownload) { if (shouldDownload) {
downloadChapters( downloadChapters(

View file

@ -1136,7 +1136,7 @@ class LibraryPresenter(
val mangaToDelete = mangas.distinctBy { it.id } val mangaToDelete = mangas.distinctBy { it.id }
.mapNotNull { if (it.id != null) MangaUpdate(it.id!!, favorite = false) else null } .mapNotNull { if (it.id != null) MangaUpdate(it.id!!, favorite = false) else null }
withIOContext { updateManga.updateAll(mangaToDelete) } withIOContext { updateManga.awaitAll(mangaToDelete) }
getLibrary() getLibrary()
} }
} }
@ -1173,7 +1173,7 @@ class LibraryPresenter(
val mangaToAdd = mangas.distinctBy { it.id } val mangaToAdd = mangas.distinctBy { it.id }
.mapNotNull { if (it.id != null) MangaUpdate(it.id!!, favorite = true) else null } .mapNotNull { if (it.id != null) MangaUpdate(it.id!!, favorite = true) else null }
withIOContext { updateManga.updateAll(mangaToAdd) } withIOContext { updateManga.awaitAll(mangaToAdd) }
(view as? FilteredLibraryController)?.updateStatsPage() (view as? FilteredLibraryController)?.updateStatsPage()
getLibrary() getLibrary()
} }
@ -1504,8 +1504,8 @@ class LibraryPresenter(
fun updateDB() { fun updateDB() {
val db: DatabaseHelper = Injekt.get() val db: DatabaseHelper = Injekt.get()
val getLibraryManga: GetLibraryManga by injectLazy() val getLibraryManga: GetLibraryManga by injectLazy()
val libraryManga = runBlocking { getLibraryManga.await() }
db.inTransaction { db.inTransaction {
val libraryManga = runBlocking { getLibraryManga.await() }
libraryManga.forEach { manga -> libraryManga.forEach { manga ->
if (manga.date_added == 0L) { if (manga.date_added == 0L) {
val chapters = db.getChapters(manga).executeAsBlocking() val chapters = db.getChapters(manga).executeAsBlocking()
@ -1529,8 +1529,8 @@ class LibraryPresenter(
val db: DatabaseHelper = Injekt.get() val db: DatabaseHelper = Injekt.get()
val cc: CoverCache = Injekt.get() val cc: CoverCache = Injekt.get()
val getLibraryManga: GetLibraryManga by injectLazy() val getLibraryManga: GetLibraryManga by injectLazy()
val libraryManga = runBlocking { getLibraryManga.await() }
db.inTransaction { db.inTransaction {
val libraryManga = runBlocking { getLibraryManga.await() }
libraryManga.forEach { manga -> libraryManga.forEach { manga ->
if (manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) { if (manga.thumbnail_url?.startsWith("custom", ignoreCase = true) == true) {
val file = cc.getCoverFile(manga) val file = cc.getCoverFile(manga)

View file

@ -53,10 +53,10 @@ import eu.kanade.tachiyomi.util.manga.MangaUtil
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.ImageUtil 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.launchIO
import eu.kanade.tachiyomi.util.system.launchNow import eu.kanade.tachiyomi.util.system.launchNow
import eu.kanade.tachiyomi.util.system.launchUI 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.util.system.withUIContext
import eu.kanade.tachiyomi.widget.TriStateCheckBox import eu.kanade.tachiyomi.widget.TriStateCheckBox
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -74,6 +74,8 @@ import uy.kohesive.injekt.injectLazy
import yokai.domain.chapter.interactor.GetAvailableScanlators import yokai.domain.chapter.interactor.GetAvailableScanlators
import yokai.domain.chapter.interactor.GetChapters import yokai.domain.chapter.interactor.GetChapters
import yokai.domain.library.custom.model.CustomMangaInfo 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 yokai.domain.storage.StorageManager
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@ -92,6 +94,7 @@ class MangaDetailsPresenter(
) : BaseCoroutinePresenter<MangaDetailsController>(), DownloadQueue.DownloadListener { ) : BaseCoroutinePresenter<MangaDetailsController>(), DownloadQueue.DownloadListener {
private val getAvailableScanlators: GetAvailableScanlators by injectLazy() private val getAvailableScanlators: GetAvailableScanlators by injectLazy()
private val getChapters: GetChapters by injectLazy() private val getChapters: GetChapters by injectLazy()
private val updateManga: UpdateManga by injectLazy()
private val customMangaManager: CustomMangaManager by injectLazy() private val customMangaManager: CustomMangaManager by injectLazy()
private val mangaShortcutManager: MangaShortcutManager by injectLazy() private val mangaShortcutManager: MangaShortcutManager by injectLazy()
@ -404,7 +407,7 @@ class MangaDetailsPresenter(
} }
val finChapters = chapters.await() val finChapters = chapters.await()
if (finChapters.isNotEmpty()) { if (finChapters.isNotEmpty()) {
val newChapters = syncChaptersWithSource(db, finChapters, manga, source) val newChapters = withIOContext { syncChaptersWithSource(finChapters, manga, source) }
if (newChapters.first.isNotEmpty()) { if (newChapters.first.isNotEmpty()) {
if (manga.shouldDownloadNewChapters(db, preferences)) { if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters( downloadChapters(
@ -469,7 +472,7 @@ class MangaDetailsPresenter(
} }
isLoading = false isLoading = false
try { try {
syncChaptersWithSource(db, chapters, manga, source) syncChaptersWithSource(chapters, manga, source)
getChapters() getChapters()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -555,14 +558,14 @@ class MangaDetailsPresenter(
if (mangaSortMatchesDefault()) { if (mangaSortMatchesDefault()) {
manga.setSortToGlobal() manga.setSortToGlobal()
} }
asyncUpdateMangaAndChapters() presenterScope.launchIO { asyncUpdateMangaAndChapters() }
} }
fun setGlobalChapterSort(sort: Int, descend: Boolean) { fun setGlobalChapterSort(sort: Int, descend: Boolean) {
preferences.sortChapterOrder().set(sort) preferences.sortChapterOrder().set(sort)
preferences.chaptersDescAsDefault().set(descend) preferences.chaptersDescAsDefault().set(descend)
manga.setSortToGlobal() manga.setSortToGlobal()
asyncUpdateMangaAndChapters() presenterScope.launchIO { asyncUpdateMangaAndChapters() }
} }
fun mangaSortMatchesDefault(): Boolean { fun mangaSortMatchesDefault(): Boolean {
@ -583,7 +586,7 @@ class MangaDetailsPresenter(
fun resetSortingToDefault() { fun resetSortingToDefault() {
manga.setSortToGlobal() manga.setSortToGlobal()
asyncUpdateMangaAndChapters() presenterScope.launchIO { asyncUpdateMangaAndChapters() }
} }
/** /**
@ -613,7 +616,7 @@ class MangaDetailsPresenter(
if (mangaFilterMatchesDefault()) { if (mangaFilterMatchesDefault()) {
manga.setFilterToGlobal() manga.setFilterToGlobal()
} }
asyncUpdateMangaAndChapters() presenterScope.launchIO { asyncUpdateMangaAndChapters() }
} }
/** /**
@ -623,7 +626,7 @@ class MangaDetailsPresenter(
fun hideTitle(hide: Boolean) { fun hideTitle(hide: Boolean) {
manga.displayMode = if (hide) Manga.CHAPTER_DISPLAY_NUMBER else Manga.CHAPTER_DISPLAY_NAME manga.displayMode = if (hide) Manga.CHAPTER_DISPLAY_NUMBER else Manga.CHAPTER_DISPLAY_NAME
manga.setFilterToLocal() manga.setFilterToLocal()
db.updateChapterFlags(manga).executeAsBlocking() presenterScope.launchIO { updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags)) }
if (mangaFilterMatchesDefault()) { if (mangaFilterMatchesDefault()) {
manga.setFilterToGlobal() manga.setFilterToGlobal()
} }
@ -632,7 +635,7 @@ class MangaDetailsPresenter(
fun resetFilterToDefault() { fun resetFilterToDefault() {
manga.setFilterToGlobal() manga.setFilterToGlobal()
asyncUpdateMangaAndChapters() presenterScope.launchIO { asyncUpdateMangaAndChapters() }
} }
fun setGlobalChapterFilters( fun setGlobalChapterFilters(
@ -663,15 +666,13 @@ class MangaDetailsPresenter(
) )
preferences.hideChapterTitlesByDefault().set(manga.hideChapterTitles) preferences.hideChapterTitlesByDefault().set(manga.hideChapterTitles)
manga.setFilterToGlobal() manga.setFilterToGlobal()
asyncUpdateMangaAndChapters() presenterScope.launchIO { asyncUpdateMangaAndChapters() }
} }
private fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) { private suspend fun asyncUpdateMangaAndChapters(justChapters: Boolean = false) {
presenterScope.launch { if (!justChapters) updateManga.await(MangaUpdate(manga.id!!, chapterFlags = manga.chapter_flags))
if (!justChapters) db.updateChapterFlags(manga).executeOnIO() getChapters()
getChapters() withUIContext { view?.updateChapters(chapters) }
withContext(Dispatchers.Main) { view?.updateChapters(chapters) }
}
} }
private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true private fun isScanlatorFiltered() = manga.filtered_scanlators?.isNotEmpty() == true
@ -690,13 +691,15 @@ class MangaDetailsPresenter(
} }
fun setScanlatorFilter(filteredScanlators: Set<String>) { fun setScanlatorFilter(filteredScanlators: Set<String>) {
val manga = manga presenterScope.launchIO {
MangaUtil.setScanlatorFilter( val manga = manga
db, MangaUtil.setScanlatorFilter(
manga, updateManga,
if (filteredScanlators.size == allChapterScanlators.size) emptySet() else filteredScanlators manga,
) if (filteredScanlators.size == allChapterScanlators.size) emptySet() else filteredScanlators
asyncUpdateMangaAndChapters() )
asyncUpdateMangaAndChapters()
}
} }
fun toggleFavorite(): Boolean { fun toggleFavorite(): Boolean {
@ -802,11 +805,23 @@ class MangaDetailsPresenter(
} }
} }
manga.viewer_flags = -1 manga.viewer_flags = -1
db.updateViewerFlags(manga).executeAsBlocking() presenterScope.launchIO { updateManga.await(MangaUpdate(manga.id!!, viewerFlags = manga.viewer_flags)) }
} }
manga.status = status ?: SManga.UNKNOWN manga.status = status ?: SManga.UNKNOWN
LocalSource(downloadManager.context).updateMangaInfo(manga, lang) 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 { } else {
var genre = if (!tags.isNullOrEmpty() && tags.joinToString(", ") != manga.originalGenre) { var genre = if (!tags.isNullOrEmpty() && tags.joinToString(", ") != manga.originalGenre) {
tags.map { tag -> tag.replaceFirstChar { it.titlecase(Locale.getDefault()) } } tags.map { tag -> tag.replaceFirstChar { it.titlecase(Locale.getDefault()) } }
@ -817,7 +832,7 @@ class MangaDetailsPresenter(
if (seriesType != null) { if (seriesType != null) {
genre = setSeriesType(seriesType, genre?.joinToString()) genre = setSeriesType(seriesType, genre?.joinToString())
manga.viewer_flags = -1 manga.viewer_flags = -1
db.updateViewerFlags(manga).executeAsBlocking() presenterScope.launchIO { updateManga.await(MangaUpdate(manga.id!!, viewerFlags = manga.viewer_flags)) }
} }
val manga = CustomMangaInfo( val manga = CustomMangaInfo(
mangaId = manga.id!!, mangaId = manga.id!!,

View file

@ -44,6 +44,7 @@ import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.system.toast 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.RecyclerWindowInsetsListener
import eu.kanade.tachiyomi.util.view.activityBinding import eu.kanade.tachiyomi.util.view.activityBinding
import eu.kanade.tachiyomi.util.view.isControllerVisible import eu.kanade.tachiyomi.util.view.isControllerVisible
@ -61,7 +62,7 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class MigrationListController(bundle: Bundle? = null) : class MigrationListController(bundle: Bundle? = null) :
@ -190,7 +191,6 @@ class MigrationListController(bundle: Bundle? = null) :
val chapters = source.getChapterList(localManga) val chapters = source.getChapterList(localManga)
try { try {
syncChaptersWithSource( syncChaptersWithSource(
db,
chapters, chapters,
localManga, localManga,
source, source,
@ -232,7 +232,7 @@ class MigrationListController(bundle: Bundle? = null) :
emptyList() emptyList()
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
syncChaptersWithSource(db, chapters, localManga, source) syncChaptersWithSource(chapters, localManga, source)
} }
localManga localManga
} else { } else {
@ -368,7 +368,7 @@ class MigrationListController(bundle: Bundle? = null) :
val localManga = smartSearchEngine.networkToLocalManga(manga, source.id) val localManga = smartSearchEngine.networkToLocalManga(manga, source.id)
try { try {
val chapters = source.getChapterList(localManga) val chapters = source.getChapterList(localManga)
syncChaptersWithSource(db, chapters, localManga, source) withIOContext { syncChaptersWithSource(chapters, localManga, source) }
} catch (e: Exception) { } catch (e: Exception) {
return@async null return@async null
} }

View file

@ -75,6 +75,8 @@ import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.chapter.interactor.GetChapters import yokai.domain.chapter.interactor.GetChapters
import yokai.domain.download.DownloadPreferences import yokai.domain.download.DownloadPreferences
import yokai.domain.manga.interactor.UpdateManga
import yokai.domain.manga.models.MangaUpdate
import yokai.domain.storage.StorageManager import yokai.domain.storage.StorageManager
import java.util.* import java.util.*
import java.util.concurrent.* import java.util.concurrent.*
@ -94,6 +96,7 @@ class ReaderViewModel(
private val downloadPreferences: DownloadPreferences = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(),
) : ViewModel() { ) : ViewModel() {
private val getChapters: GetChapters by injectLazy() private val getChapters: GetChapters by injectLazy()
private val updateManga: UpdateManga by injectLazy()
private val mutableState = MutableStateFlow(State()) private val mutableState = MutableStateFlow(State())
val state = mutableState.asStateFlow() val state = mutableState.asStateFlow()
@ -333,7 +336,6 @@ class ReaderViewModel(
val chapterId: Long val chapterId: Long
if (chapters.isNotEmpty()) { if (chapters.isNotEmpty()) {
val newChapters = syncChaptersWithSource( val newChapters = syncChaptersWithSource(
db,
chapters, chapters,
manga, manga,
delegatedSource.delegate!!, delegatedSource.delegate!!,
@ -678,7 +680,7 @@ class ReaderViewModel(
manga.viewer_flags = 0 manga.viewer_flags = 0
} }
manga.readingModeType = if (cantSwitchToLTR) 0 else readerType 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 return if (manga.readingModeType == 0) default else manga.readingModeType
} }
@ -689,9 +691,9 @@ class ReaderViewModel(
fun setMangaReadingMode(readingModeType: Int) { fun setMangaReadingMode(readingModeType: Int) {
val manga = manga ?: return val manga = manga ?: return
runBlocking(Dispatchers.IO) { viewModelScope.launchIO {
manga.readingModeType = readingModeType manga.readingModeType = readingModeType
db.updateViewerFlags(manga).executeAsBlocking() updateManga.await(MangaUpdate(manga.id!!, viewerFlags = manga.viewer_flags))
val currChapters = state.value.viewerChapters val currChapters = state.value.viewerChapters
if (currChapters != null) { if (currChapters != null) {
// Save current page // Save current page
@ -726,12 +728,11 @@ class ReaderViewModel(
fun setMangaOrientationType(rotationType: Int) { fun setMangaOrientationType(rotationType: Int) {
val manga = manga ?: return val manga = manga ?: return
this.manga?.orientationType = rotationType this.manga?.orientationType = rotationType
db.updateViewerFlags(manga).executeAsBlocking()
Logger.i { "Manga orientation is ${manga.orientationType}" } Logger.i { "Manga orientation is ${manga.orientationType}" }
viewModelScope.launchIO { viewModelScope.launchIO {
db.updateViewerFlags(manga).executeAsBlocking() updateManga.await(MangaUpdate(manga.id!!, viewerFlags = manga.viewer_flags))
val currChapters = state.value.viewerChapters val currChapters = state.value.viewerChapters
if (currChapters != null) { if (currChapters != null) {
mutableState.update { mutableState.update {

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.util.chapter 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.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager 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.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.chapter.ChapterFilter.Companion.filterChaptersByScanlators 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 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.* import java.util.*
/** /**
@ -20,11 +29,16 @@ import java.util.*
* @param source the source of the chapters. * @param source the source of the chapters.
* @return a pair of new insertions and deletions. * @return a pair of new insertions and deletions.
*/ */
fun syncChaptersWithSource( suspend fun syncChaptersWithSource(
db: DatabaseHelper,
rawSourceChapters: List<SChapter>, rawSourceChapters: List<SChapter>,
manga: Manga, manga: Manga,
source: Source, 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<Chapter>, List<Chapter>> { ): Pair<List<Chapter>, List<Chapter>> {
if (rawSourceChapters.isEmpty()) { if (rawSourceChapters.isEmpty()) {
throw Exception("No chapters found") throw Exception("No chapters found")
@ -32,7 +46,7 @@ fun syncChaptersWithSource(
val downloadManager: DownloadManager by injectLazy() val downloadManager: DownloadManager by injectLazy()
// Chapters from db. // Chapters from db.
val dbChapters = db.getChapters(manga).executeAsBlocking() val dbChapters = getChapters.await(manga)
val sourceChapters = rawSourceChapters val sourceChapters = rawSourceChapters
.distinctBy { it.url } .distinctBy { it.url }
@ -49,7 +63,7 @@ fun syncChaptersWithSource(
val toAdd = mutableListOf<Chapter>() val toAdd = mutableListOf<Chapter>()
// Chapters whose metadata have changed. // Chapters whose metadata have changed.
val toChange = mutableListOf<Chapter>() val toChange = mutableListOf<ChapterUpdate>()
for (sourceChapter in sourceChapters) { for (sourceChapter in sourceChapters) {
val dbChapter = dbChapters.find { it.url == sourceChapter.url } val dbChapter = dbChapters.find { it.url == sourceChapter.url }
@ -71,12 +85,15 @@ fun syncChaptersWithSource(
) { ) {
downloadManager.renameChapter(source, manga, dbChapter, sourceChapter) downloadManager.renameChapter(source, manga, dbChapter, sourceChapter)
} }
dbChapter.scanlator = sourceChapter.scanlator val update = ChapterUpdate(
dbChapter.name = sourceChapter.name dbChapter.id!!,
dbChapter.date_upload = sourceChapter.date_upload scanlator = sourceChapter.scanlator,
dbChapter.chapter_number = sourceChapter.chapter_number name = sourceChapter.name,
dbChapter.source_order = sourceChapter.source_order dateUpload = sourceChapter.date_upload,
toChange.add(dbChapter) 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 val newestDate = dbChapters.maxOfOrNull { it.date_upload } ?: 0L
if (newestDate != 0L && newestDate != manga.last_update) { if (newestDate != 0L && newestDate != manga.last_update) {
manga.last_update = newestDate manga.last_update = newestDate
db.updateLastUpdated(manga).executeAsBlocking() val update = MangaUpdate(manga.id!!, lastUpdate = manga.last_update)
updateManga.await(update)
} }
return Pair(emptyList(), emptyList()) return Pair(emptyList(), emptyList())
} }
val readded = mutableListOf<Chapter>() val readded = mutableListOf<Chapter>()
db.inTransaction { val deletedChapterNumbers = TreeSet<Float>()
val deletedChapterNumbers = TreeSet<Float>() val deletedReadChapterNumbers = TreeSet<Float>()
val deletedReadChapterNumbers = TreeSet<Float>() if (toDelete.isNotEmpty()) {
if (toDelete.isNotEmpty()) { for (c in toDelete) {
for (c in toDelete) { if (c.read) {
if (c.read) { deletedReadChapterNumbers.add(c.chapter_number)
deletedReadChapterNumbers.add(c.chapter_number)
}
deletedChapterNumbers.add(c.chapter_number)
} }
db.deleteChapters(toDelete).executeAsBlocking() deletedChapterNumbers.add(c.chapter_number)
} }
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)
}
}
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()
} }
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() val reAddedSet = readded.toSet()
return Pair( return Pair(
toAdd.subtract(reAddedSet).toList().filterChaptersByScanlators(manga), toAdd.subtract(reAddedSet).toList().filterChaptersByScanlators(manga),

View file

@ -1,13 +1,17 @@
package eu.kanade.tachiyomi.util.manga 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.data.database.models.Manga
import eu.kanade.tachiyomi.util.chapter.ChapterUtil import eu.kanade.tachiyomi.util.chapter.ChapterUtil
import yokai.domain.manga.interactor.UpdateManga
import yokai.domain.manga.models.MangaUpdate
object MangaUtil { object MangaUtil {
fun setScanlatorFilter(db: DatabaseHelper, manga: Manga, filteredScanlators: Set<String>) { suspend fun setScanlatorFilter(updateManga: UpdateManga, manga: Manga, filteredScanlators: Set<String>) {
if (manga.id == null) return
manga.filtered_scanlators = manga.filtered_scanlators =
if (filteredScanlators.isEmpty()) null else ChapterUtil.getScanlatorString(filteredScanlators) if (filteredScanlators.isEmpty()) null else ChapterUtil.getScanlatorString(filteredScanlators)
db.updateMangaFilteredScanlators(manga).executeAsBlocking()
updateManga.await(MangaUpdate(manga.id!!, filteredScanlators = manga.filtered_scanlators))
} }
} }

View file

@ -13,8 +13,11 @@ import yokai.data.manga.MangaRepositoryImpl
import yokai.domain.category.CategoryRepository import yokai.domain.category.CategoryRepository
import yokai.domain.category.interactor.GetCategories import yokai.domain.category.interactor.GetCategories
import yokai.domain.chapter.ChapterRepository import yokai.domain.chapter.ChapterRepository
import yokai.domain.chapter.interactor.DeleteChapters
import yokai.domain.chapter.interactor.GetAvailableScanlators import yokai.domain.chapter.interactor.GetAvailableScanlators
import yokai.domain.chapter.interactor.GetChapters 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.interactor.TrustExtension
import yokai.domain.extension.repo.ExtensionRepoRepository import yokai.domain.extension.repo.ExtensionRepoRepository
import yokai.domain.extension.repo.interactor.CreateExtensionRepo 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.library.custom.interactor.RelinkCustomManga
import yokai.domain.manga.MangaRepository import yokai.domain.manga.MangaRepository
import yokai.domain.manga.interactor.GetLibraryManga import yokai.domain.manga.interactor.GetLibraryManga
import yokai.domain.manga.interactor.GetManga
import yokai.domain.manga.interactor.InsertManga import yokai.domain.manga.interactor.InsertManga
import yokai.domain.manga.interactor.UpdateManga import yokai.domain.manga.interactor.UpdateManga
@ -52,13 +56,17 @@ class DomainModule : InjektModule {
addFactory { RelinkCustomManga(get()) } addFactory { RelinkCustomManga(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) } addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetManga(get()) }
addFactory { GetLibraryManga(get()) } addFactory { GetLibraryManga(get()) }
addFactory { InsertManga(get()) } addFactory { InsertManga(get()) }
addFactory { UpdateManga(get()) } addFactory { UpdateManga(get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) } addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { DeleteChapters(get()) }
addFactory { GetAvailableScanlators(get()) } addFactory { GetAvailableScanlators(get()) }
addFactory { GetChapters(get()) } addFactory { GetChapters(get()) }
addFactory { InsertChapters(get()) }
addFactory { UpdateChapters(get()) }
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) } addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
addFactory { GetCategories(get()) } addFactory { GetCategories(get()) }

View file

@ -1,10 +1,12 @@
package yokai.data.chapter package yokai.data.chapter
import co.touchlab.kermit.Logger
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.util.system.toInt import eu.kanade.tachiyomi.util.system.toInt
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import yokai.data.DatabaseHandler import yokai.data.DatabaseHandler
import yokai.domain.chapter.ChapterRepository import yokai.domain.chapter.ChapterRepository
import yokai.domain.chapter.models.ChapterUpdate
class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepository { class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepository {
override suspend fun getChapters(mangaId: Long, filterScanlators: Boolean): List<Chapter> = override suspend fun getChapters(mangaId: Long, filterScanlators: Boolean): List<Chapter> =
@ -18,4 +20,93 @@ class ChapterRepositoryImpl(private val handler: DatabaseHandler) : ChapterRepos
override fun getScanlatorsByChapterAsFlow(mangaId: Long): Flow<List<String>> = override fun getScanlatorsByChapterAsFlow(mangaId: Long): Flow<List<String>> =
handler.subscribeToList { chaptersQueries.getScanlatorsByMangaId(mangaId) { it.orEmpty() } } 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<Chapter>) =
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<ChapterUpdate>): 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()
}
}
} }

View file

@ -11,10 +11,16 @@ import yokai.domain.manga.MangaRepository
import yokai.domain.manga.models.MangaUpdate import yokai.domain.manga.models.MangaUpdate
class MangaRepositoryImpl(private val handler: DatabaseHandler) : MangaRepository { class MangaRepositoryImpl(private val handler: DatabaseHandler) : MangaRepository {
override suspend fun getManga(): List<Manga> = override suspend fun getMangaList(): List<Manga> =
handler.awaitList { mangasQueries.findAll(Manga::mapper) } handler.awaitList { mangasQueries.findAll(Manga::mapper) }
override fun getMangaAsFlow(): Flow<List<Manga>> = 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<List<Manga>> =
handler.subscribeToList { mangasQueries.findAll(Manga::mapper) } handler.subscribeToList { mangasQueries.findAll(Manga::mapper) }
override suspend fun getLibraryManga(): List<LibraryManga> = override suspend fun getLibraryManga(): List<LibraryManga> =

View file

@ -2,6 +2,7 @@ package yokai.domain.chapter
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import yokai.domain.chapter.models.ChapterUpdate
interface ChapterRepository { interface ChapterRepository {
suspend fun getChapters(mangaId: Long, filterScanlators: Boolean): List<Chapter> suspend fun getChapters(mangaId: Long, filterScanlators: Boolean): List<Chapter>
@ -9,4 +10,12 @@ interface ChapterRepository {
suspend fun getScanlatorsByChapter(mangaId: Long): List<String> suspend fun getScanlatorsByChapter(mangaId: Long): List<String>
fun getScanlatorsByChapterAsFlow(mangaId: Long): Flow<List<String>> fun getScanlatorsByChapterAsFlow(mangaId: Long): Flow<List<String>>
suspend fun delete(chapter: Chapter): Boolean
suspend fun deleteAll(chapters: List<Chapter>): Boolean
suspend fun update(update: ChapterUpdate): Boolean
suspend fun updateAll(updates: List<ChapterUpdate>): Boolean
suspend fun insert(chapter: Chapter): Long?
} }

View file

@ -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<Chapter>) = chapterRepository.deleteAll(chapters)
}

View file

@ -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)
}

View file

@ -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<ChapterUpdate>) = chapterRepository.updateAll(chapters)
}

View file

@ -6,8 +6,10 @@ import kotlinx.coroutines.flow.Flow
import yokai.domain.manga.models.MangaUpdate import yokai.domain.manga.models.MangaUpdate
interface MangaRepository { interface MangaRepository {
suspend fun getManga(): List<Manga> suspend fun getMangaList(): List<Manga>
fun getMangaAsFlow(): Flow<List<Manga>> suspend fun getMangaByUrlAndSource(url: String, source: Long): Manga?
suspend fun getMangaById(id: Long): Manga?
fun getMangaListAsFlow(): Flow<List<Manga>>
suspend fun getLibraryManga(): List<LibraryManga> suspend fun getLibraryManga(): List<LibraryManga>
fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>> fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>>
suspend fun update(update: MangaUpdate): Boolean suspend fun update(update: MangaUpdate): Boolean

View file

@ -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)
}

View file

@ -6,6 +6,6 @@ import yokai.domain.manga.models.MangaUpdate
class UpdateManga ( class UpdateManga (
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
) { ) {
suspend fun update(update: MangaUpdate) = mangaRepository.update(update) suspend fun await(update: MangaUpdate) = mangaRepository.update(update)
suspend fun updateAll(updates: List<MangaUpdate>) = mangaRepository.updateAll(updates) suspend fun awaitAll(updates: List<MangaUpdate>) = mangaRepository.updateAll(updates)
} }

View file

@ -36,3 +36,34 @@ getScanlatorsByMangaId:
SELECT scanlator SELECT scanlator
FROM chapters FROM chapters
WHERE manga_id = :mangaId; 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();

View file

@ -30,6 +30,16 @@ findAll:
SELECT * SELECT *
FROM mangas; FROM mangas;
findByUrlAndSource:
SELECT *
FROM mangas
WHERE url = :url AND source = :source;
findById:
SELECT *
FROM mangas
WHERE _id = :mangaId;
insert: 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) 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); VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :initialized, :viewer, :hideTitle, :chapterFlags, :dateAdded, :filteredScanlators, :updateStrategy);

View file

@ -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,
)