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 (