From 2c87a8fd02999aecc5b9ac34002614e6846bc6c3 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 2 Jul 2022 18:55:34 +0200 Subject: [PATCH] Use SQLDelight on Library screen (#7432) - Uses the new `asObservable` function to change the database calls to use SQLDelight, which should make the impact minimal when it comes to bugs. - Use interactors where they already exist - The todos are for the Compose rewrite - Removed unused StorIO methods/queries - Tested loading library, move manga to new category, unfavorite multiple manga, move multiple manga from one category to another, change filter, sort and display settings (with and without per category settings), (un)mark chapters, start/delete downloads Thank Syer for asObservable Co-authored-by: jobobby04 <17078382+jobobby04@users.noreply.github.com> Co-authored-by: jobobby04 <17078382+jobobby04@users.noreply.github.com> (cherry picked from commit 05085fe57fe4c3ada497f93b8cd282a5009cdbbb) # Conflicts: # app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt # app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt # app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt # app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt # app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt # app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt # app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt --- .../eu/kanade/core/util/RxJavaExtensions.kt | 36 +++ .../eu/kanade/data/AndroidDatabaseHandler.kt | 5 + .../data/category/CategoryRepositoryImpl.kt | 24 +- .../java/eu/kanade/data/manga/LibraryQuery.kt | 97 +++++++ .../kanade/data/manga/MangaRepositoryImpl.kt | 62 +++-- .../kanade/data/track/TrackRepositoryImpl.kt | 16 +- .../java/eu/kanade/domain/DomainModule.kt | 4 +- .../category/interactor/GetCategories.kt | 12 +- ...aToCategories.kt => SetMangaCategories.kt} | 4 +- .../category/repository/CategoryRepository.kt | 10 +- .../domain/manga/interactor/UpdateManga.kt | 4 + .../manga/repository/MangaRepository.kt | 4 +- .../domain/track/interactor/GetTracks.kt | 19 +- .../track/repository/TrackRepository.kt | 8 +- .../data/database/queries/CategoryQueries.kt | 2 - .../data/database/queries/MangaQueries.kt | 30 --- .../data/database/queries/RawQueries.kt | 33 --- .../ui/library/LibraryCategoryView.kt | 18 +- .../tachiyomi/ui/library/LibraryController.kt | 42 ++-- .../tachiyomi/ui/library/LibraryPresenter.kt | 236 +++++++++++------- .../ui/library/LibrarySettingsSheet.kt | 47 +++- .../tachiyomi/ui/manga/MangaPresenter.kt | 6 +- app/src/main/sqldelight/data/manga_sync.sq | 4 + app/src/main/sqldelight/data/mangas.sq | 55 ++++ 24 files changed, 556 insertions(+), 222 deletions(-) create mode 100644 app/src/main/java/eu/kanade/data/manga/LibraryQuery.kt rename app/src/main/java/eu/kanade/domain/category/interactor/{MoveMangaToCategories.kt => SetMangaCategories.kt} (79%) diff --git a/app/src/main/java/eu/kanade/core/util/RxJavaExtensions.kt b/app/src/main/java/eu/kanade/core/util/RxJavaExtensions.kt index 4d1ef452d..b54fa63ab 100644 --- a/app/src/main/java/eu/kanade/core/util/RxJavaExtensions.kt +++ b/app/src/main/java/eu/kanade/core/util/RxJavaExtensions.kt @@ -1,10 +1,17 @@ package eu.kanade.core.util +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch +import rx.Emitter import rx.Observable import rx.Observer +import kotlin.coroutines.CoroutineContext fun Observable.asFlow(): Flow = callbackFlow { val observer = object : Observer { @@ -23,3 +30,32 @@ fun Observable.asFlow(): Flow = callbackFlow { val subscription = subscribe(observer) awaitClose { subscription.unsubscribe() } } + +fun Flow.asObservable( + context: CoroutineContext = Dispatchers.Unconfined, + backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE, +): Observable { + return Observable.create( + { emitter -> + /* + * ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if + * asObservable is already invoked from unconfined + */ + val job = GlobalScope.launch(context = context, start = CoroutineStart.ATOMIC) { + try { + collect { emitter.onNext(it) } + emitter.onCompleted() + } catch (e: Throwable) { + // Ignore `CancellationException` as error, since it indicates "normal cancellation" + if (e !is CancellationException) { + emitter.onError(e) + } else { + emitter.onCompleted() + } + } + } + emitter.setCancellation { job.cancel() } + }, + backpressureMode, + ) +} diff --git a/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt b/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt index 9f4f804c3..468f8713e 100644 --- a/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt +++ b/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt @@ -7,6 +7,7 @@ import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToOne import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull +import eu.kanade.data.manga.LibraryQuery import eu.kanade.tachiyomi.Database import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -87,4 +88,8 @@ class AndroidDatabaseHandler( val context = getCurrentDatabaseContext() return withContext(context) { block(db) } } + + // SY --> + fun getLibraryQuery() = LibraryQuery(driver) + // SY <-- } diff --git a/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt index ddad9581b..9ecff4985 100644 --- a/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt @@ -12,10 +12,28 @@ class CategoryRepositoryImpl( private val handler: DatabaseHandler, ) : CategoryRepository { + // SY --> + override suspend fun awaitAll(): List { + return handler.awaitList { categoriesQueries.getCategories(categoryMapper) } + } + // SY <-- + override fun getAll(): Flow> { return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) } } + override suspend fun getCategoriesByMangaId(mangaId: Long): List { + return handler.awaitList { + categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper) + } + } + + override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow> { + return handler.subscribeToList { + categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper) + } + } + @Throws(DuplicateNameException::class) override suspend fun insert(name: String, order: Long) { if (checkDuplicateName(name)) throw DuplicateNameException(name) @@ -55,12 +73,6 @@ class CategoryRepositoryImpl( } } - override suspend fun getCategoriesForManga(mangaId: Long): List { - return handler.awaitList { - categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper) - } - } - override suspend fun checkDuplicateName(name: String): Boolean { return handler .awaitList { categoriesQueries.getCategories() } diff --git a/app/src/main/java/eu/kanade/data/manga/LibraryQuery.kt b/app/src/main/java/eu/kanade/data/manga/LibraryQuery.kt new file mode 100644 index 000000000..9382ae7b4 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/manga/LibraryQuery.kt @@ -0,0 +1,97 @@ +package eu.kanade.data.manga + +import com.squareup.sqldelight.Query +import com.squareup.sqldelight.db.SqlCursor +import com.squareup.sqldelight.db.SqlDriver +import com.squareup.sqldelight.internal.copyOnWriteList +import eu.kanade.tachiyomi.data.database.models.LibraryManga +import exh.source.MERGED_SOURCE_ID + +private val mapper = { cursor: SqlCursor -> + LibraryManga().apply { + id = cursor.getLong(0)!! + source = cursor.getLong(1)!! + url = cursor.getString(2)!! + artist = cursor.getString(3) + author = cursor.getString(4) + description = cursor.getString(5) + genre = cursor.getString(6) + title = cursor.getString(7)!! + status = cursor.getLong(8)!!.toInt() + thumbnail_url = cursor.getString(9) + favorite = cursor.getLong(10)!! == 1L + last_update = cursor.getLong(11) ?: 0 + initialized = cursor.getLong(13)!! == 1L + viewer_flags = cursor.getLong(14)!!.toInt() + chapter_flags = cursor.getLong(15)!!.toInt() + cover_last_modified = cursor.getLong(16)!! + date_added = cursor.getLong(17)!! + filtered_scanlators = cursor.getString(18) + unreadCount = cursor.getLong(19)!!.toInt() + readCount = cursor.getLong(20)!!.toInt() + category = cursor.getLong(21)!!.toInt() + } +} + +class LibraryQuery(val driver: SqlDriver) : Query(copyOnWriteList(), mapper) { + override fun execute(): SqlCursor { + return driver.executeQuery( + null, + """ + SELECT M.*, COALESCE(MC.category_id, 0) AS category + FROM ( + SELECT mangas.*, COALESCE(C.unreadCount, 0) AS unread_count, COALESCE(R.readCount, 0) AS read_count + FROM mangas + LEFT JOIN ( + SELECT chapters.manga_id, COUNT(*) AS unreadCount + FROM chapters + WHERE chapters.read = 0 + GROUP BY chapters.manga_id + ) AS C + ON mangas._id = C.manga_id + LEFT JOIN ( + SELECT chapters.manga_id, COUNT(*) AS readCount + FROM chapters + WHERE chapters.read = 1 + GROUP BY chapters.manga_id + ) AS R + ON mangas._id = R.manga_id + WHERE mangas.favorite = 1 AND mangas.source <> $MERGED_SOURCE_ID + GROUP BY mangas._id + UNION + SELECT mangas.*, COALESCE(C.unreadCount, 0) AS unread_count, COALESCE(R.readCount, 0) AS read_count + FROM mangas + LEFT JOIN ( + SELECT merged.merge_id, COUNT(*) as unreadCount + FROM merged + JOIN chapters + ON chapters.manga_id = merged.manga_id + WHERE chapters.read = 0 + GROUP BY merged.merge_id + ) AS C + ON mangas._id = C.merge_id + LEFT JOIN ( + SELECT merged.merge_id, COUNT(*) as readCount + FROM merged + JOIN chapters + ON chapters.manga_id = merged.manga_id + WHERE chapters.read = 1 + GROUP BY merged.merge_id + ) AS R + ON mangas._id = R.merge_id + WHERE mangas.favorite = 1 AND mangas.source = $MERGED_SOURCE_ID + GROUP BY mangas._id + ORDER BY mangas.title + ) AS M + LEFT JOIN ( + SELECT * + FROM mangas_categories + ) AS MC + ON M._id = MC.manga_id; + """.trimIndent(), + 1, + ) + } + + override fun toString(): String = "LibraryQuery.sq:get" +} diff --git a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt index 04abec76d..f0b2a7575 100644 --- a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt @@ -47,7 +47,7 @@ class MangaRepositoryImpl( } } - override suspend fun moveMangaToCategories(mangaId: Long, categoryIds: List) { + override suspend fun setMangaCategories(mangaId: Long, categoryIds: List) { handler.await(inTransaction = true) { mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId) categoryIds.map { categoryId -> @@ -58,32 +58,48 @@ class MangaRepositoryImpl( override suspend fun update(update: MangaUpdate): Boolean { return try { - handler.await { - mangasQueries.update( - source = update.source, - url = update.url, - artist = update.artist, - author = update.author, - description = update.description, - genre = update.genre?.let(listOfStringsAdapter::encode), - title = update.title, - status = update.status, - thumbnailUrl = update.thumbnailUrl, - favorite = update.favorite?.toLong(), - lastUpdate = update.lastUpdate, - initialized = update.initialized?.toLong(), - viewer = update.viewerFlags, - chapterFlags = update.chapterFlags, - coverLastModified = update.coverLastModified, - dateAdded = update.dateAdded, - mangaId = update.id, - filteredScanlators = update.filteredScanlators?.let(listOfStringsAndAdapter::encode), - ) - } + partialUpdate(update) true } catch (e: Exception) { logcat(LogPriority.ERROR, e) false } } + + override suspend fun updateAll(values: List): Boolean { + return try { + partialUpdate(*values.toTypedArray()) + true + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + false + } + } + + private suspend fun partialUpdate(vararg values: MangaUpdate) { + handler.await(inTransaction = true) { + values.forEach { value -> + mangasQueries.update( + source = value.source, + url = value.url, + artist = value.artist, + author = value.author, + description = value.description, + genre = value.genre?.let(listOfStringsAdapter::encode), + title = value.title, + status = value.status, + thumbnailUrl = value.thumbnailUrl, + favorite = value.favorite?.toLong(), + lastUpdate = value.lastUpdate, + initialized = value.initialized?.toLong(), + viewer = value.viewerFlags, + chapterFlags = value.chapterFlags, + coverLastModified = value.coverLastModified, + dateAdded = value.dateAdded, + filteredScanlators = value.filteredScanlators?.let(listOfStringsAndAdapter::encode), + mangaId = value.id, + ) + } + } + } } diff --git a/app/src/main/java/eu/kanade/data/track/TrackRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/track/TrackRepositoryImpl.kt index ac8942eb7..78bea3fc0 100644 --- a/app/src/main/java/eu/kanade/data/track/TrackRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/track/TrackRepositoryImpl.kt @@ -9,13 +9,27 @@ class TrackRepositoryImpl( private val handler: DatabaseHandler, ) : TrackRepository { + // SY --> + override suspend fun getTracks(): List { + return handler.awaitList { + manga_syncQueries.getTracks(trackMapper) + } + } + // SY <-- + override suspend fun getTracksByMangaId(mangaId: Long): List { return handler.awaitList { manga_syncQueries.getTracksByMangaId(mangaId, trackMapper) } } - override suspend fun subscribeTracksByMangaId(mangaId: Long): Flow> { + override fun getTracksAsFlow(): Flow> { + return handler.subscribeToList { + manga_syncQueries.getTracks(trackMapper) + } + } + + override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow> { return handler.subscribeToList { manga_syncQueries.getTracksByMangaId(mangaId, trackMapper) } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index bcd296777..622060862 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -9,7 +9,7 @@ import eu.kanade.data.track.TrackRepositoryImpl import eu.kanade.domain.category.interactor.DeleteCategory import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.InsertCategory -import eu.kanade.domain.category.interactor.MoveMangaToCategories +import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.category.interactor.UpdateCategory import eu.kanade.domain.category.repository.CategoryRepository import eu.kanade.domain.chapter.interactor.GetChapter @@ -77,7 +77,7 @@ class DomainModule : InjektModule { addFactory { ResetViewerFlags(get()) } addFactory { SetMangaChapterFlags(get()) } addFactory { UpdateManga(get()) } - addFactory { MoveMangaToCategories(get()) } + addFactory { SetMangaCategories(get()) } addSingletonFactory { TrackRepositoryImpl(get()) } addFactory { DeleteTrack(get()) } diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/GetCategories.kt b/app/src/main/java/eu/kanade/domain/category/interactor/GetCategories.kt index ab1f5f240..e3861164b 100644 --- a/app/src/main/java/eu/kanade/domain/category/interactor/GetCategories.kt +++ b/app/src/main/java/eu/kanade/domain/category/interactor/GetCategories.kt @@ -8,11 +8,21 @@ class GetCategories( private val categoryRepository: CategoryRepository, ) { + // SY --> + suspend fun await(): List { + return categoryRepository.awaitAll() + } + // SY <-- + fun subscribe(): Flow> { return categoryRepository.getAll() } + fun subscribe(mangaId: Long): Flow> { + return categoryRepository.getCategoriesByMangaIdAsFlow(mangaId) + } + suspend fun await(mangaId: Long): List { - return categoryRepository.getCategoriesForManga(mangaId) + return categoryRepository.getCategoriesByMangaId(mangaId) } } diff --git a/app/src/main/java/eu/kanade/domain/category/interactor/MoveMangaToCategories.kt b/app/src/main/java/eu/kanade/domain/category/interactor/SetMangaCategories.kt similarity index 79% rename from app/src/main/java/eu/kanade/domain/category/interactor/MoveMangaToCategories.kt rename to app/src/main/java/eu/kanade/domain/category/interactor/SetMangaCategories.kt index 567ec133b..1ddb55c3b 100644 --- a/app/src/main/java/eu/kanade/domain/category/interactor/MoveMangaToCategories.kt +++ b/app/src/main/java/eu/kanade/domain/category/interactor/SetMangaCategories.kt @@ -4,13 +4,13 @@ import eu.kanade.domain.manga.repository.MangaRepository import eu.kanade.tachiyomi.util.system.logcat import logcat.LogPriority -class MoveMangaToCategories( +class SetMangaCategories( private val mangaRepository: MangaRepository, ) { suspend fun await(mangaId: Long, categoryIds: List) { try { - mangaRepository.moveMangaToCategories(mangaId, categoryIds) + mangaRepository.setMangaCategories(mangaId, categoryIds) } catch (e: Exception) { logcat(LogPriority.ERROR, e) } diff --git a/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt b/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt index 231cd81f8..5e339a02e 100644 --- a/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt +++ b/app/src/main/java/eu/kanade/domain/category/repository/CategoryRepository.kt @@ -6,8 +6,16 @@ import kotlinx.coroutines.flow.Flow interface CategoryRepository { + // SY --> + suspend fun awaitAll(): List + // SY <-- + fun getAll(): Flow> + suspend fun getCategoriesByMangaId(mangaId: Long): List + + fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow> + @Throws(DuplicateNameException::class) suspend fun insert(name: String, order: Long) @@ -16,8 +24,6 @@ interface CategoryRepository { suspend fun delete(categoryId: Long) - suspend fun getCategoriesForManga(mangaId: Long): List - suspend fun checkDuplicateName(name: String): Boolean } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt index 1071997ac..329359253 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/UpdateManga.kt @@ -20,6 +20,10 @@ class UpdateManga( return mangaRepository.update(mangaUpdate) } + suspend fun awaitAll(values: List): Boolean { + return mangaRepository.updateAll(values) + } + suspend fun awaitUpdateFromSource( localManga: Manga, remoteManga: MangaInfo, diff --git a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt index 60c07ba84..d5d282832 100644 --- a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt +++ b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt @@ -18,7 +18,9 @@ interface MangaRepository { suspend fun resetViewerFlags(): Boolean - suspend fun moveMangaToCategories(mangaId: Long, categoryIds: List) + suspend fun setMangaCategories(mangaId: Long, categoryIds: List) suspend fun update(update: MangaUpdate): Boolean + + suspend fun updateAll(values: List): Boolean } diff --git a/app/src/main/java/eu/kanade/domain/track/interactor/GetTracks.kt b/app/src/main/java/eu/kanade/domain/track/interactor/GetTracks.kt index a48a40421..99e0f3ba8 100644 --- a/app/src/main/java/eu/kanade/domain/track/interactor/GetTracks.kt +++ b/app/src/main/java/eu/kanade/domain/track/interactor/GetTracks.kt @@ -10,6 +10,17 @@ class GetTracks( private val trackRepository: TrackRepository, ) { + // SY --> + suspend fun await(): List { + return try { + trackRepository.getTracks() + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + emptyList() + } + } + // SY <-- + suspend fun await(mangaId: Long): List { return try { trackRepository.getTracksByMangaId(mangaId) @@ -19,7 +30,11 @@ class GetTracks( } } - suspend fun subscribe(mangaId: Long): Flow> { - return trackRepository.subscribeTracksByMangaId(mangaId) + fun subscribe(): Flow> { + return trackRepository.getTracksAsFlow() + } + + fun subscribe(mangaId: Long): Flow> { + return trackRepository.getTracksByMangaIdAsFlow(mangaId) } } diff --git a/app/src/main/java/eu/kanade/domain/track/repository/TrackRepository.kt b/app/src/main/java/eu/kanade/domain/track/repository/TrackRepository.kt index 38207a2f2..232fc9c69 100644 --- a/app/src/main/java/eu/kanade/domain/track/repository/TrackRepository.kt +++ b/app/src/main/java/eu/kanade/domain/track/repository/TrackRepository.kt @@ -5,9 +5,15 @@ import kotlinx.coroutines.flow.Flow interface TrackRepository { + // SY --> + suspend fun getTracks(): List + // SY <-- + suspend fun getTracksByMangaId(mangaId: Long): List - suspend fun subscribeTracksByMangaId(mangaId: Long): Flow> + fun getTracksAsFlow(): Flow> + + fun getTracksByMangaIdAsFlow(mangaId: Long): Flow> suspend fun delete(mangaId: Long, syncId: Long) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt index f5246930e..f53f9ddd0 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt @@ -29,7 +29,5 @@ interface CategoryQueries : DbProvider { ) .prepare() - fun insertCategory(category: Category) = db.put().`object`(category).prepare() - fun insertCategories(categories: List) = db.put().objects(categories).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 5285fb24a..40b32cc00 100755 --- 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 @@ -137,36 +137,6 @@ interface MangaQueries : DbProvider { fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() - fun getLastReadManga() = db.get() - .listOfObjects(Manga::class.java) - .withQuery( - RawQuery.builder() - .query(getLastReadMangaQuery()) - .observesTables(MangaTable.TABLE) - .build(), - ) - .prepare() - - fun getLatestChapterManga() = db.get() - .listOfObjects(Manga::class.java) - .withQuery( - RawQuery.builder() - .query(getLatestChapterMangaQuery()) - .observesTables(MangaTable.TABLE) - .build(), - ) - .prepare() - - fun getChapterFetchDateManga() = db.get() - .listOfObjects(Manga::class.java) - .withQuery( - RawQuery.builder() - .query(getChapterFetchDateMangaQuery()) - .observesTables(MangaTable.TABLE) - .build(), - ) - .prepare() - // SY --> fun getMangaWithMetadata() = db.get() .listOfObjects(Manga::class.java) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index 593500242..e44d7b7dc 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -152,39 +152,6 @@ fun getHistoryByMangaId() = """ // SY <-- -fun getLastReadMangaQuery() = - """ - SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max - FROM ${Manga.TABLE} - JOIN ${Chapter.TABLE} - ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} - JOIN ${History.TABLE} - ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} - WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1 - GROUP BY ${Manga.TABLE}.${Manga.COL_ID} - ORDER BY max DESC -""" - -fun getLatestChapterMangaQuery() = - """ - SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max - FROM ${Manga.TABLE} - JOIN ${Chapter.TABLE} - ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} - GROUP BY ${Manga.TABLE}.${Manga.COL_ID} - ORDER by max DESC -""" - -fun getChapterFetchDateMangaQuery() = - """ - SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max - FROM ${Manga.TABLE} - JOIN ${Chapter.TABLE} - ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} - GROUP BY ${Manga.TABLE}.${Manga.COL_ID} - ORDER by max DESC -""" - /** * Query to get the categories for a manga. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt index 12506aa2a..268330208 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt @@ -9,6 +9,8 @@ import androidx.recyclerview.widget.RecyclerView import dev.chrisbanes.insetter.applyInsetter import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.SelectableAdapter +import eu.kanade.domain.category.interactor.UpdateCategory +import eu.kanade.domain.category.model.CategoryUpdate import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category @@ -425,6 +427,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att } } + private val updateCategory: UpdateCategory by injectLazy() + override fun onItemMove(fromPosition: Int, toPosition: Int) { if (fromPosition == toPosition) return controller.invalidateActionMode() @@ -433,13 +437,23 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att if (category.id == 0) { preferences.defaultMangaOrder().set(mangaIds.joinToString("/")) } else { - db.insertCategory(category).asRxObservable().subscribe() + scope.launch { + updateCategory.await(CategoryUpdate(category.id!!.toLong(), mangaOrder = mangaIds)) + } } if (preferences.categorizedDisplaySettings().get() && category.id != 0) { if (SortModeSetting.fromFlag(category.sortMode) != SortModeSetting.DRAG_AND_DROP) { category.sortMode = SortModeSetting.DRAG_AND_DROP.flag category.sortDirection = SortDirectionSetting.ASCENDING.flag - db.insertCategory(category).asRxObservable().subscribe() + scope.launch { + updateCategory.await( + CategoryUpdate( + id = category.id!!.toLong(), + flags = category.flags.toLong(), + mangaOrder = mangaIds, + ), + ) + } } } else if (preferences.librarySortingMode().get() != SortModeSetting.DRAG_AND_DROP) { preferences.librarySortingAscending().set(SortDirectionSetting.ASCENDING) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index fe528a51c..1fba6bde4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -36,6 +36,8 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.preference.asImmediateFlow import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.openInBrowser @@ -53,6 +55,7 @@ import exh.source.mangaDexSourceIds import exh.source.nHentaiSourceIds import exh.ui.LoaderManager import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn @@ -267,6 +270,7 @@ class LibraryController( destroyActionModeIfNeeded() adapter?.onDestroy() adapter = null + settingsSheet?.sheetScope?.cancel() settingsSheet = null tabsVisibilitySubscription?.unsubscribe() tabsVisibilitySubscription = null @@ -633,25 +637,29 @@ class LibraryController( * Move the selected manga to a list of categories. */ private fun showMangaCategoriesDialog() { - // Create a copy of selected manga - val mangas = selectedMangas.toList() + viewScope.launchIO { + // Create a copy of selected manga + val mangas = selectedMangas.toList() - // Hide the default category because it has a different behavior than the ones from db. - val categories = presenter.categories.filter { it.id != 0 } + // Hide the default category because it has a different behavior than the ones from db. + val categories = presenter.categories.filter { it.id != 0 } - // Get indexes of the common categories to preselect. - val common = presenter.getCommonCategories(mangas) - // Get indexes of the mix categories to preselect. - val mix = presenter.getMixCategories(mangas) - val preselected = categories.map { - when (it) { - in common -> QuadStateTextView.State.CHECKED.ordinal - in mix -> QuadStateTextView.State.INDETERMINATE.ordinal - else -> QuadStateTextView.State.UNCHECKED.ordinal + // Get indexes of the common categories to preselect. + val common = presenter.getCommonCategories(mangas) + // Get indexes of the mix categories to preselect. + val mix = presenter.getMixCategories(mangas) + val preselected = categories.map { + when (it) { + in common -> QuadStateTextView.State.CHECKED.ordinal + in mix -> QuadStateTextView.State.INDETERMINATE.ordinal + else -> QuadStateTextView.State.UNCHECKED.ordinal + } + }.toTypedArray() + launchUI { + ChangeMangaCategoriesDialog(this@LibraryController, mangas, categories, preselected) + .showDialog(router) } - }.toTypedArray() - ChangeMangaCategoriesDialog(this, mangas, categories, preselected) - .showDialog(router) + } } private fun downloadUnreadChapters() { @@ -691,7 +699,7 @@ class LibraryController( // SY <-- override fun updateCategoriesForMangas(mangas: List, addCategories: List, removeCategories: List) { - presenter.updateMangasToCategories(mangas, addCategories, removeCategories) + presenter.setMangaCategories(mangas, addCategories, removeCategories) destroyActionModeIfNeeded() } 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 20f8721d4..be9a008df 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -2,13 +2,27 @@ package eu.kanade.tachiyomi.ui.library import android.os.Bundle import com.jakewharton.rxrelay.BehaviorRelay +import eu.kanade.core.util.asObservable +import eu.kanade.data.AndroidDatabaseHandler +import eu.kanade.data.DatabaseHandler +import eu.kanade.domain.category.interactor.GetCategories +import eu.kanade.domain.category.interactor.SetMangaCategories +import eu.kanade.domain.category.model.toDbCategory +import eu.kanade.domain.chapter.interactor.GetChapterByMangaId +import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId +import eu.kanade.domain.chapter.interactor.UpdateChapter +import eu.kanade.domain.chapter.model.ChapterUpdate +import eu.kanade.domain.chapter.model.toDbChapter +import eu.kanade.domain.manga.interactor.GetMergedMangaById +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.MangaUpdate +import eu.kanade.domain.manga.model.toDbManga +import eu.kanade.domain.track.interactor.GetTracks import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache -import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper @@ -32,9 +46,10 @@ import exh.md.utils.FollowStatus import exh.md.utils.MdUtil import exh.source.MERGED_SOURCE_ID import exh.source.isEhBasedManga -import exh.util.executeOnIO import exh.util.isLewd import exh.util.nullIfBlank +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -59,7 +74,13 @@ private typealias LibraryMap = Map> * Presenter of [LibraryController]. */ class LibraryPresenter( - private val db: DatabaseHelper = Injekt.get(), + private val handler: DatabaseHandler = Injekt.get(), + private val getTracks: GetTracks = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(), + private val updateChapter: UpdateChapter = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), + private val setMangaCategories: SetMangaCategories = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), @@ -67,6 +88,8 @@ class LibraryPresenter( private val trackManager: TrackManager = Injekt.get(), // SY --> private val customMangaManager: CustomMangaManager = Injekt.get(), + private val getMergedMangaById: GetMergedMangaById = Injekt.get(), + private val getMergedChaptersByMangaId: GetMergedChapterByMangaId = Injekt.get(), // SY <-- ) : BasePresenter() { @@ -128,6 +151,7 @@ class LibraryPresenter( * Subscribes to library if needed. */ fun subscribeLibrary() { + // TODO: Move this to a coroutine world if (librarySubscription.isNullOrUnsubscribed()) { librarySubscription = getLibraryObservable() .combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> @@ -160,7 +184,7 @@ class LibraryPresenter( * * @param map the map to filter. */ - private fun applyFilters(map: LibraryMap, trackMap: Map>): LibraryMap { + private fun applyFilters(map: LibraryMap, trackMap: Map>): LibraryMap { val downloadedOnly = preferences.downloadedOnly().get() val filterDownloaded = preferences.filterDownloaded().get() val filterUnread = preferences.filterUnread().get() @@ -294,7 +318,11 @@ class LibraryPresenter( item.downloadCount = if (showDownloadBadges) { // SY --> if (item.manga.source == MERGED_SOURCE_ID) { - item.manga.id?.let { mergeMangaId -> db.getMergedMangas(mergeMangaId).executeAsBlocking().map { downloadManager.getDownloadCount(it) }.sum() } ?: 0 + item.manga.id?.let { mergeMangaId -> + runBlocking { + getMergedMangaById.await(mergeMangaId) + }.sumOf { downloadManager.getDownloadCount(it.toDbManga()) } + } ?: 0 } else /* SY <-- */ downloadManager.getDownloadCount(item.manga) } else { // Unset download count if not enabled @@ -333,18 +361,30 @@ class LibraryPresenter( private fun applySort(categories: List, map: LibraryMap): LibraryMap { val lastReadManga by lazy { var counter = 0 - // Result comes as newest to oldest so it's reversed - db.getLastReadManga().executeAsBlocking().reversed().associate { it.id!! to counter++ } + // TODO: Make [applySort] a suspended function + runBlocking { + handler.awaitList { + mangasQueries.getLastRead() + }.associate { it._id to counter++ } + } } val latestChapterManga by lazy { var counter = 0 - // Result comes as newest to oldest so it's reversed - db.getLatestChapterManga().executeAsBlocking().reversed().associate { it.id!! to counter++ } + // TODO: Make [applySort] a suspended function + runBlocking { + handler.awaitList { + mangasQueries.getLatestByChapterUploadDate() + }.associate { it._id to counter++ } + } } val chapterFetchDateManga by lazy { var counter = 0 - // Result comes as newest to oldest so it's reversed - db.getChapterFetchDateManga().executeAsBlocking().reversed().associate { it.id!! to counter++ } + // TODO: Make [applySort] a suspended function + runBlocking { + handler.awaitList { + mangasQueries.getLatestByChapterFetchDate() + }.associate { it._id to counter++ } + } } // SY --> @@ -509,7 +549,7 @@ class LibraryPresenter( * @return an observable of the categories. */ private fun getCategoriesObservable(): Observable> { - return db.getCategories().asRxObservable() + return getCategories.subscribe().map { it.map { it.toDbCategory() } }.asObservable() } /** @@ -521,7 +561,40 @@ class LibraryPresenter( private fun getLibraryMangasObservable(): Observable { val defaultLibraryDisplayMode = preferences.libraryDisplayMode() val shouldSetFromCategory = preferences.categorizedDisplaySettings() - return db.getLibraryMangas().asRxObservable() + + // TODO: Move this to domain/data layer + return handler + .subscribeToList { + // SY --> + (handler as AndroidDatabaseHandler).getLibraryQuery() + /*mangasQueries.getLibrary { _id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List?, title: String, status: Long, thumbnail_url: String?, favorite: Boolean, last_update: Long?, next_update: Long?, initialized: Boolean, viewer: Long, chapter_flags: Long, cover_last_modified: Long, date_added: Long, filteredScanlators: List?, unread_count: Long, read_count: Long, category: Long -> + LibraryManga().apply { + this.id = _id + this.source = source + this.url = url + this.artist = artist + this.author = author + this.description = description + this.genre = genre?.joinToString() + this.title = title + this.status = status.toInt() + this.thumbnail_url = thumbnail_url + this.favorite = favorite + this.last_update = last_update ?: 0 + this.initialized = initialized + this.viewer_flags = viewer.toInt() + this.chapter_flags = chapter_flags.toInt() + this.cover_last_modified = cover_last_modified + this.date_added = date_added + this.filtered_scanlators = filteredScanlators?.let(listOfStringsAndAdapter::encode) + this.unreadCount = unread_count.toInt() + this.readCount = read_count.toInt() + this.category = category.toInt() + } + }*/ + // SY <-- + } + .asObservable() .map { list -> list.map { libraryManga -> // Display mode based on user preference: take it from global library setting or category @@ -539,7 +612,7 @@ class LibraryPresenter( * * @return an observable of tracked manga. */ - private fun getFilterObservable(): Observable>> { + private fun getFilterObservable(): Observable>> { return getTracksObservable().combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { tracks, _ -> tracks } } @@ -548,16 +621,20 @@ class LibraryPresenter( * * @return an observable of tracked manga. */ - private fun getTracksObservable(): Observable>> { - return db.getTracks().asRxObservable().map { tracks -> - tracks.groupBy { it.manga_id } - .mapValues { tracksForMangaId -> - // Check if any of the trackers is logged in for the current manga id - tracksForMangaId.value.associate { - Pair(it.sync_id, trackManager.getService(it.sync_id.toLong())?.isLogged.takeUnless { isLogged -> isLogged == true && it.sync_id.toLong() == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int } ?: false) + private fun getTracksObservable(): Observable>> { + // TODO: Move this to domain/data layer + return getTracks.subscribe() + .asObservable().map { tracks -> + tracks + .groupBy { it.mangaId } + .mapValues { tracksForMangaId -> + // Check if any of the trackers is logged in for the current manga id + tracksForMangaId.value.associate { + Pair(it.syncId, trackManager.getService(it.syncId)?.isLogged.takeUnless { isLogged -> isLogged == true && it.syncId == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int.toLong() } ?: false) + } } - } - }.observeOn(Schedulers.io()) + } + .observeOn(Schedulers.io()) } /** @@ -611,11 +688,11 @@ class LibraryPresenter( * * @param mangas the list of manga. */ - fun getCommonCategories(mangas: List): Collection { + suspend fun getCommonCategories(mangas: List): Collection { if (mangas.isEmpty()) return emptyList() return mangas.toSet() - .map { db.getCategoriesForManga(it).executeAsBlocking() } - .reduce { set1: Iterable, set2 -> set1.intersect(set2).toMutableList() } + .map { getCategories.await(it.id!!).map { it.toDbCategory() } } + .reduce { set1, set2 -> set1.intersect(set2).toMutableList() } } /** @@ -623,9 +700,9 @@ class LibraryPresenter( * * @param mangas the list of manga. */ - fun getMixCategories(mangas: List): Collection { + suspend fun getMixCategories(mangas: List): Collection { if (mangas.isEmpty()) return emptyList() - val mangaCategories = mangas.toSet().map { db.getCategoriesForManga(it).executeAsBlocking() } + val mangaCategories = mangas.toSet().map { getCategories.await(it.id!!).map { it.toDbCategory() } } val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() } return mangaCategories.flatten().distinct().subtract(common).toMutableList() } @@ -640,25 +717,25 @@ class LibraryPresenter( launchIO { if (manga.source == MERGED_SOURCE_ID) { val mergedSource = sourceManager.get(MERGED_SOURCE_ID) as MergedSource - val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking() + val mergedMangas = getMergedMangaById.await(manga.id!!) mergedSource .getChaptersAsBlocking(manga.id!!) .filter { !it.read } .groupBy { it.manga_id!! } .forEach ab@{ (mangaId, chapters) -> val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@ab - downloadManager.downloadChapters(mergedManga, chapters) + downloadManager.downloadChapters(mergedManga.toDbManga(), chapters) } } else { /* SY --> */ val chapters = if (manga.isEhBasedManga()) { - db.getChapters(manga).executeOnIO().minByOrNull { it.source_order }?.let { chapter -> + getChapterByMangaId.await(manga.id!!).minByOrNull { it.sourceOrder }?.let { chapter -> if (!chapter.read) listOf(chapter) else emptyList() } ?: emptyList() - } else /* SY <-- */ db.getChapters(manga).executeAsBlocking() + } else /* SY <-- */ getChapterByMangaId.await(manga.id!!) .filter { !it.read } - downloadManager.downloadChapters(manga, chapters) + downloadManager.downloadChapters(manga, chapters.map { it.toDbChapter() }) } } } @@ -711,21 +788,20 @@ class LibraryPresenter( fun markReadStatus(mangas: List, read: Boolean) { mangas.forEach { manga -> launchIO { - val chapters = if (manga.source == MERGED_SOURCE_ID) { - (sourceManager.get(MERGED_SOURCE_ID) as MergedSource).getChaptersAsBlocking(manga.id!!) - } else { - db.getChapters(manga).executeAsBlocking() - } - chapters.forEach { - it.read = read - if (!read) { - it.last_page_read = 0 + val chapters = if (manga.source == MERGED_SOURCE_ID) getMergedChaptersByMangaId.await(manga.id!!) else getChapterByMangaId.await(manga.id!!) + + val toUpdate = chapters + .map { chapter -> + ChapterUpdate( + read = read, + lastPageRead = if (read) 0 else null, + id = chapter.id, + ) } - } - db.updateChaptersProgress(chapters).executeAsBlocking() + updateChapter.awaitAll(toUpdate) if (read && preferences.removeAfterMarkedAsRead()) { - deleteChapters(manga, chapters) + deleteChapters(manga, chapters.map { it.toDbChapter() }) } } } @@ -735,12 +811,12 @@ class LibraryPresenter( sourceManager.get(manga.source)?.let { source -> // SY --> if (source is MergedSource) { - val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking() + val mergedMangas = runBlocking { getMergedMangaById.await(manga.id!!) } val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) } chapters.groupBy { it.manga_id }.forEach { (mangaId, chapters) -> val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@forEach val mergedMangaSource = sources.firstOrNull { it.id == mergedManga.source } ?: return@forEach - downloadManager.deleteChapters(chapters, mergedManga, mergedMangaSource) + downloadManager.deleteChapters(chapters, mergedManga.toDbManga(), mergedMangaSource) } } else /* SY <-- */ downloadManager.deleteChapters(chapters, manga, source) } @@ -749,20 +825,23 @@ class LibraryPresenter( /** * Remove the selected manga. * - * @param mangas the list of manga to delete. + * @param mangaList the list of manga to delete. * @param deleteFromLibrary whether to delete manga from library. * @param deleteChapters whether to delete downloaded chapters. */ - fun removeMangas(mangas: List, deleteFromLibrary: Boolean, deleteChapters: Boolean) { + fun removeMangas(mangaList: List, deleteFromLibrary: Boolean, deleteChapters: Boolean) { launchIO { - val mangaToDelete = mangas.distinctBy { it.id } + val mangaToDelete = mangaList.distinctBy { it.id } if (deleteFromLibrary) { - mangaToDelete.forEach { - it.favorite = false + val toDelete = mangaToDelete.map { it.removeCovers(coverCache) + MangaUpdate( + favorite = false, + id = it.id!!, + ) } - db.insertMangas(mangaToDelete).executeAsBlocking() + updateManga.awaitAll(toDelete) } if (deleteChapters) { @@ -770,11 +849,11 @@ class LibraryPresenter( val source = sourceManager.get(manga.source) as? HttpSource if (source != null) { if (source is MergedSource) { - val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking() + val mergedMangas = getMergedMangaById.await(manga.id!!) val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) } mergedMangas.forEach merge@{ mergedManga -> val mergedSource = sources.firstOrNull { mergedManga.source == it.id } as? HttpSource ?: return@merge - downloadManager.deleteManga(mergedManga, mergedSource) + downloadManager.deleteManga(mergedManga.toDbManga(), mergedSource) } } else downloadManager.deleteManga(manga, source) } @@ -784,36 +863,23 @@ class LibraryPresenter( } /** - * Move the given list of manga to categories. + * Bulk update categories of manga using old and new common categories. * - * @param categories the selected categories. - * @param mangas the list of manga to move. - */ - fun moveMangasToCategories(categories: List, mangas: List) { - val mc = mutableListOf() - - for (manga in mangas) { - categories.mapTo(mc) { MangaCategory.create(manga, it) } - } - - db.setMangaCategories(mc, mangas) - } - - /** - * Bulk update categories of mangas using old and new common categories. - * - * @param mangas the list of manga to move. + * @param mangaList the list of manga to move. * @param addCategories the categories to add for all mangas. * @param removeCategories the categories to remove in all mangas. */ - fun updateMangasToCategories(mangas: List, addCategories: List, removeCategories: List) { - val mangaCategories = mangas.map { manga -> - val categories = db.getCategoriesForManga(manga).executeAsBlocking() - .subtract(removeCategories).plus(addCategories).distinct() - categories.map { MangaCategory.create(manga, it) } - }.flatten() - - db.setMangaCategories(mangaCategories, mangas) + fun setMangaCategories(mangaList: List, addCategories: List, removeCategories: List) { + presenterScope.launchIO { + mangaList.map { manga -> + val categoryIds = getCategories.await(manga.id!!) + .map { it.toDbCategory() } + .subtract(removeCategories) + .plus(addCategories) + .mapNotNull { it.id?.toLong() } + setMangaCategories.await(manga.id!!, categoryIds) + } + } } // SY --> @@ -821,7 +887,7 @@ class LibraryPresenter( fun getFirstUnread(manga: Manga): Chapter? { val chapters = if (manga.source == MERGED_SOURCE_ID) { (sourceManager.get(MERGED_SOURCE_ID) as MergedSource).getChaptersAsBlocking(manga.id!!) - } else db.getChapters(manga).executeAsBlocking() + } else runBlocking { getChapterByMangaId.await(manga.id!!) }.map { it.toDbChapter() } return if (manga.isEhBasedManga()) { val chapter = chapters.sortedBy { it.source_order }.getOrNull(0) if (chapter?.read == false) chapter else null @@ -867,10 +933,10 @@ class LibraryPresenter( when (groupType) { LibraryGroup.BY_TRACK_STATUS -> { - val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id } + val tracks = runBlocking { getTracks.await() }.groupBy { it.mangaId } libraryManga.forEach { libraryItem -> val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track -> - TrackStatus.parseTrackerStatus(track.sync_id.toLong(), track.status) + TrackStatus.parseTrackerStatus(track.syncId, track.status.toInt()) } ?: TrackStatus.OTHER map.getOrPut(status.int) { mutableListOf() } += libraryItem diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt index abf70d28a..a70828aa3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt @@ -4,8 +4,10 @@ import android.content.Context import android.util.AttributeSet import android.view.View import com.bluelinelabs.conductor.Router +import eu.kanade.domain.category.interactor.GetCategories +import eu.kanade.domain.category.interactor.UpdateCategory +import eu.kanade.domain.category.model.CategoryUpdate import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager @@ -13,9 +15,14 @@ import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting +import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.runBlocking import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -23,6 +30,7 @@ import uy.kohesive.injekt.injectLazy class LibrarySettingsSheet( router: Router, private val trackManager: TrackManager = Injekt.get(), + private val updateCategory: UpdateCategory = Injekt.get(), onGroupClickListener: (ExtendedNavigationView.Group) -> Unit, ) : TabbedBottomSheetDialog(router.activity!!) { @@ -30,7 +38,8 @@ class LibrarySettingsSheet( private val sort: Sort private val display: Display private val grouping: Grouping - private val db: DatabaseHelper by injectLazy() + + val sheetScope = CoroutineScope(Job() + Dispatchers.IO) init { filters = Filter(router.activity!!) @@ -295,8 +304,14 @@ class LibrarySettingsSheet( if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0 /* SY --> */ && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT /* SY <-- */) { currentCategory?.sortDirection = flag.flag - - db.insertCategory(currentCategory!!).executeAsBlocking() + sheetScope.launchIO { + updateCategory.await( + CategoryUpdate( + id = currentCategory!!.id?.toLong()!!, + flags = currentCategory!!.flags.toLong(), + ), + ) + } } else { preferences.librarySortingAscending().set(flag) } @@ -321,8 +336,14 @@ class LibrarySettingsSheet( if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0 /* SY --> */ && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT /* SY <-- */) { currentCategory?.sortMode = flag.flag - - db.insertCategory(currentCategory!!).executeAsBlocking() + sheetScope.launchIO { + updateCategory.await( + CategoryUpdate( + id = currentCategory!!.id?.toLong()!!, + flags = currentCategory!!.flags.toLong(), + ), + ) + } } else { preferences.librarySortingMode().set(flag) } @@ -418,8 +439,14 @@ class LibrarySettingsSheet( if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0 /* SY --> */ && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT /* SY <-- */) { currentCategory?.displayMode = flag.flag - - db.insertCategory(currentCategory!!).executeAsBlocking() + sheetScope.launchIO { + updateCategory.await( + CategoryUpdate( + id = currentCategory!!.id?.toLong()!!, + flags = currentCategory!!.flags.toLong(), + ), + ) + } } else { preferences.libraryDisplayMode().set(flag) } @@ -518,7 +545,9 @@ class LibrarySettingsSheet( inner class InternalGroup : Group { private val groupItems = mutableListOf() private val trackManager: TrackManager = Injekt.get() - private val hasCategories = Injekt.get().getCategories().executeAsBlocking().size != 0 + private val hasCategories = runBlocking { + Injekt.get().await().isNotEmpty() + } init { val groupingItems = mutableListOf( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 64a27760a..91eb99515 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -4,7 +4,7 @@ import android.content.Context import android.os.Bundle import androidx.compose.runtime.Immutable import eu.kanade.domain.category.interactor.GetCategories -import eu.kanade.domain.category.interactor.MoveMangaToCategories +import eu.kanade.domain.category.interactor.SetMangaCategories import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay @@ -135,7 +135,7 @@ class MangaPresenter( private val getCategories: GetCategories = Injekt.get(), private val deleteTrack: DeleteTrack = Injekt.get(), private val getTracks: GetTracks = Injekt.get(), - private val moveMangaToCategories: MoveMangaToCategories = Injekt.get(), + private val setMangaCategories: SetMangaCategories = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(), private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(), ) : BasePresenter() { @@ -697,7 +697,7 @@ class MangaPresenter( val mangaId = manga.id ?: return val categoryIds = categories.mapNotNull { it.id?.toLong() } presenterScope.launchIO { - moveMangaToCategories.await(mangaId, categoryIds) + setMangaCategories.await(mangaId, categoryIds) } } diff --git a/app/src/main/sqldelight/data/manga_sync.sq b/app/src/main/sqldelight/data/manga_sync.sq index c3276a7b8..93b5b6bb8 100644 --- a/app/src/main/sqldelight/data/manga_sync.sq +++ b/app/src/main/sqldelight/data/manga_sync.sq @@ -21,6 +21,10 @@ delete: DELETE FROM manga_sync WHERE manga_id = :mangaId AND sync_id = :syncId; +getTracks: +SELECT * +FROM manga_sync; + getTracksByMangaId: SELECT * FROM manga_sync diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq index 823871106..74f632da1 100644 --- a/app/src/main/sqldelight/data/mangas.sq +++ b/app/src/main/sqldelight/data/mangas.sq @@ -94,6 +94,61 @@ AND C.date_upload > :after AND C.date_fetch > M.date_added ORDER BY C.date_upload DESC; +getLibrary: +SELECT M.*, COALESCE(MC.category_id, 0) AS category +FROM ( + SELECT mangas.*, COALESCE(C.unreadCount, 0) AS unread_count, COALESCE(R.readCount, 0) AS read_count + FROM mangas + LEFT JOIN ( + SELECT chapters.manga_id, COUNT(*) AS unreadCount + FROM chapters + WHERE chapters.read = 0 + GROUP BY chapters.manga_id + ) AS C + ON mangas._id = C.manga_id + LEFT JOIN ( + SELECT chapters.manga_id, COUNT(*) AS readCount + FROM chapters + WHERE chapters.read = 1 + GROUP BY chapters.manga_id + ) AS R + WHERE mangas.favorite = 1 + GROUP BY mangas._id + ORDER BY mangas.title +) AS M +LEFT JOIN ( + SELECT * + FROM mangas_categories +) AS MC +ON M._id = MC.manga_id; + +getLastRead: +SELECT M.*, MAX(H.last_read) AS max +FROM mangas M +JOIN chapters C +ON M._id = C.manga_id +JOIN history H +ON C._id = H.chapter_id +WHERE M.favorite = 1 +GROUP BY M._id +ORDER BY max ASC; + +getLatestByChapterUploadDate: +SELECT M.*, MAX(C.date_upload) AS max +FROM mangas M +JOIN chapters C +ON M._id = C.manga_id +GROUP BY M._id +ORDER BY max ASC; + +getLatestByChapterFetchDate: +SELECT M.*, MAX(C.date_fetch) AS max +FROM mangas M +JOIN chapters C +ON M._id = C.manga_id +GROUP BY M._id +ORDER BY max ASC; + deleteMangasNotInLibraryBySourceIds: DELETE FROM mangas WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN (