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 4d1ef452d5..b54fa63abc 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/category/CategoryRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt index 44b78cd6dd..ef90ad085f 100644 --- a/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/category/CategoryRepositoryImpl.kt @@ -15,6 +15,18 @@ class CategoryRepositoryImpl( 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) @@ -48,12 +60,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/MangaRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt index b90a46f87d..9058eac34c 100644 --- a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt @@ -46,7 +46,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 -> @@ -57,31 +57,47 @@ 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, - ) - } + 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, + 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 ac8942eb79..30481edaf4 100644 --- a/app/src/main/java/eu/kanade/data/track/TrackRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/track/TrackRepositoryImpl.kt @@ -15,7 +15,13 @@ class TrackRepositoryImpl( } } - 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 bcd296777b..6220608626 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 ab1f5f2400..5cedc70c38 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 @@ -12,7 +12,11 @@ class GetCategories( 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 567ec133ba..1ddb55c3bb 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 231cd81f82..19418f4388 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 @@ -8,6 +8,10 @@ interface CategoryRepository { 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 +20,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 1071997ac2..329359253e 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 60c07ba84d..d5d282832d 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 a48a40421b..519d8a8430 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 @@ -19,7 +19,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 38207a2f2f..65e4937f76 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 @@ -7,7 +7,9 @@ interface TrackRepository { 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/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index e87fa54f55..3e2db4bbb9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.data.database.queries.CategoryQueries import eu.kanade.tachiyomi.data.database.queries.ChapterQueries import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries import eu.kanade.tachiyomi.data.database.queries.MangaQueries -import eu.kanade.tachiyomi.data.database.queries.TrackQueries /** * This class provides operations to manage the database through its interfaces. @@ -26,7 +25,7 @@ import eu.kanade.tachiyomi.data.database.queries.TrackQueries class DatabaseHelper( openHelper: SupportSQLiteOpenHelper, ) : - MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries { + MangaQueries, ChapterQueries, CategoryQueries, MangaCategoryQueries { override val db = DefaultStorIOSQLite.builder() .sqliteOpenHelper(openHelper) 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 78755ed831..48b1e5bebe 100644 --- 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 @@ -28,6 +28,4 @@ interface CategoryQueries : DbProvider { .build(), ) .prepare() - - fun insertCategory(category: Category) = db.put().`object`(category).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 35ab51f449..edaddecb9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -60,8 +60,6 @@ interface MangaQueries : DbProvider { fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() - fun insertMangas(mangas: List) = db.put().objects(mangas).prepare() - fun updateChapterFlags(manga: Manga) = db.put() .`object`(manga) .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags)) @@ -76,34 +74,4 @@ interface MangaQueries : DbProvider { .`object`(manga) .withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags)) .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() } 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 4ae9592416..6eebf2f330 100644 --- 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 @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.database.queries import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter -import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga @@ -38,39 +37,6 @@ val libraryQuery = ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID} """ -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/data/database/queries/TrackQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt deleted file mode 100644 index a0a917ab59..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.data.database.queries - -import com.pushtorefresh.storio.sqlite.queries.Query -import eu.kanade.tachiyomi.data.database.DbProvider -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.database.tables.TrackTable - -interface TrackQueries : DbProvider { - - fun getTracks() = db.get() - .listOfObjects(Track::class.java) - .withQuery( - Query.builder() - .table(TrackTable.TABLE) - .build(), - ) - .prepare() -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt index 560281829d..323b0b4eb2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search import android.os.Bundle import com.jakewharton.rxrelay.BehaviorRelay 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.GetChapterByMangaId import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.chapter.interactor.UpdateChapter @@ -48,7 +48,7 @@ class SearchPresenter( private val getCategories: GetCategories = Injekt.get(), private val getTracks: GetTracks = Injekt.get(), private val insertTrack: InsertTrack = Injekt.get(), - private val moveMangaToCategories: MoveMangaToCategories = Injekt.get(), + private val setMangaCategories: SetMangaCategories = Injekt.get(), ) : GlobalSearchPresenter(initialQuery) { private val replacingMangaRelay = BehaviorRelay.create>() @@ -164,7 +164,7 @@ class SearchPresenter( // Update categories if (migrateCategories) { val categoryIds = getCategories.await(prevDomainManga.id).map { it.id } - moveMangaToCategories.await(domainManga.id, categoryIds) + setMangaCategories.await(domainManga.id, categoryIds) } // Update track 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 4318989fe7..806a4084d4 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 @@ -29,6 +29,8 @@ import eu.kanade.tachiyomi.ui.base.controller.pushController 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.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 @@ -36,6 +38,7 @@ import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.widget.ActionModeWithToolbar import eu.kanade.tachiyomi.widget.EmptyView import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -226,6 +229,7 @@ class LibraryController( destroyActionModeIfNeeded() adapter?.onDestroy() adapter = null + settingsSheet?.sheetScope?.cancel() settingsSheet = null tabsVisibilitySubscription?.unsubscribe() tabsVisibilitySubscription = null @@ -541,25 +545,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() { @@ -579,7 +587,7 @@ class LibraryController( } 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 63d7bfc5b5..a5297d98ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -2,12 +2,23 @@ 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.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.UpdateChapter +import eu.kanade.domain.chapter.model.ChapterUpdate +import eu.kanade.domain.chapter.model.toDbChapter +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.MangaUpdate +import eu.kanade.domain.track.interactor.GetTracks 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.LibraryManga 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.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager @@ -23,6 +34,8 @@ import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -47,7 +60,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(), @@ -92,6 +111,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, _ -> @@ -115,7 +135,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() @@ -252,18 +272,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++ } + } } val sortingModes = categories.associate { category -> @@ -366,7 +398,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() } /** @@ -378,7 +410,36 @@ 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 { + 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, 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.unreadCount = unread_count.toInt() + this.readCount = read_count.toInt() + this.category = category.toInt() + } + } + } + .asObservable() .map { list -> list.map { libraryManga -> // Display mode based on user preference: take it from global library setting or category @@ -396,7 +457,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 } } @@ -405,16 +466,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 ?: 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 ?: false) + } } - } - }.observeOn(Schedulers.io()) + } + .observeOn(Schedulers.io()) } /** @@ -451,11 +516,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() } } /** @@ -463,9 +528,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() } @@ -478,8 +543,9 @@ class LibraryPresenter( fun downloadUnreadChapters(mangas: List) { mangas.forEach { manga -> launchIO { - val chapters = db.getChapters(manga).executeAsBlocking() + val chapters = getChapterByMangaId.await(manga.id!!) .filter { !it.read } + .map { it.toDbChapter() } downloadManager.downloadChapters(manga, chapters) } @@ -494,17 +560,20 @@ class LibraryPresenter( fun markReadStatus(mangas: List, read: Boolean) { mangas.forEach { manga -> launchIO { - val chapters = db.getChapters(manga).executeAsBlocking() - chapters.forEach { - it.read = read - if (!read) { - it.last_page_read = 0 + val chapters = 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() }) } } } @@ -519,20 +588,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) { @@ -547,35 +619,22 @@ 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) + } + } } } 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 1795d31c2e..700735d078 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,9 @@ import android.content.Context import android.util.AttributeSet import android.view.View import com.bluelinelabs.conductor.Router +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 +14,13 @@ 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 uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy @@ -23,13 +28,15 @@ 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!!) { val filters: Filter private val sort: Sort private val display: Display - private val db: DatabaseHelper by injectLazy() + + val sheetScope = CoroutineScope(Job() + Dispatchers.IO) init { filters = Filter(router.activity!!) @@ -250,8 +257,14 @@ class LibrarySettingsSheet( if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) { 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) } @@ -272,8 +285,14 @@ class LibrarySettingsSheet( if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) { 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) } @@ -361,8 +380,14 @@ class LibrarySettingsSheet( if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) { 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) } 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 12e77fff95..92569e22f2 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 @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.manga 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.SyncChaptersWithSource import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay import eu.kanade.domain.chapter.interactor.UpdateChapter @@ -90,7 +90,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() { @@ -358,7 +358,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 c3276a7b81..93b5b6bb84 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 ad4246f4fe..7532852990 100644 --- a/app/src/main/sqldelight/data/mangas.sq +++ b/app/src/main/sqldelight/data/mangas.sq @@ -86,6 +86,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 :sourceIds;