Use SQLDelight on Library screen (#7432)

- Uses the new `asObservable` function to change the database calls to use SQLDelight, which should make the impact minimal when it comes to bugs.
- Use interactors where they already exist
- The todos are for the Compose rewrite
- Removed unused StorIO methods/queries
- Tested loading library, move manga to new category, unfavorite multiple manga, move multiple manga from one category to another, change filter, sort and display settings (with and without per category settings), (un)mark chapters, start/delete downloads

Thank Syer for asObservable

Co-authored-by: jobobby04 <17078382+jobobby04@users.noreply.github.com>

Co-authored-by: jobobby04 <17078382+jobobby04@users.noreply.github.com>
This commit is contained in:
Andreas 2022-07-02 18:55:34 +02:00 committed by GitHub
parent ff32ab09fb
commit 05085fe57f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 373 additions and 227 deletions

View file

@ -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 <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val observer = object : Observer<T> {
@ -23,3 +30,32 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val subscription = subscribe(observer)
awaitClose { subscription.unsubscribe() }
}
fun <T : Any> Flow<T>.asObservable(
context: CoroutineContext = Dispatchers.Unconfined,
backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE,
): Observable<T> {
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,
)
}

View file

@ -15,6 +15,18 @@ class CategoryRepositoryImpl(
return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) }
}
override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> {
return handler.awaitList {
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
}
}
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
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<Category> {
return handler.awaitList {
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
}
}
override suspend fun checkDuplicateName(name: String): Boolean {
return handler
.awaitList { categoriesQueries.getCategories() }

View file

@ -46,7 +46,7 @@ class MangaRepositoryImpl(
}
}
override suspend fun moveMangaToCategories(mangaId: Long, categoryIds: List<Long>) {
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
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<MangaUpdate>): 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,
)
}
}
}
}

View file

@ -15,7 +15,13 @@ class TrackRepositoryImpl(
}
}
override suspend fun subscribeTracksByMangaId(mangaId: Long): Flow<List<Track>> {
override fun getTracksAsFlow(): Flow<List<Track>> {
return handler.subscribeToList {
manga_syncQueries.getTracks(trackMapper)
}
}
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
return handler.subscribeToList {
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
}

View file

@ -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<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { DeleteTrack(get()) }

View file

@ -12,7 +12,11 @@ class GetCategories(
return categoryRepository.getAll()
}
fun subscribe(mangaId: Long): Flow<List<Category>> {
return categoryRepository.getCategoriesByMangaIdAsFlow(mangaId)
}
suspend fun await(mangaId: Long): List<Category> {
return categoryRepository.getCategoriesForManga(mangaId)
return categoryRepository.getCategoriesByMangaId(mangaId)
}
}

View file

@ -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<Long>) {
try {
mangaRepository.moveMangaToCategories(mangaId, categoryIds)
mangaRepository.setMangaCategories(mangaId, categoryIds)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}

View file

@ -8,6 +8,10 @@ interface CategoryRepository {
fun getAll(): Flow<List<Category>>
suspend fun getCategoriesByMangaId(mangaId: Long): List<Category>
fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>>
@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<Category>
suspend fun checkDuplicateName(name: String): Boolean
}

View file

@ -20,6 +20,10 @@ class UpdateManga(
return mangaRepository.update(mangaUpdate)
}
suspend fun awaitAll(values: List<MangaUpdate>): Boolean {
return mangaRepository.updateAll(values)
}
suspend fun awaitUpdateFromSource(
localManga: Manga,
remoteManga: MangaInfo,

View file

@ -18,7 +18,9 @@ interface MangaRepository {
suspend fun resetViewerFlags(): Boolean
suspend fun moveMangaToCategories(mangaId: Long, categoryIds: List<Long>)
suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>)
suspend fun update(update: MangaUpdate): Boolean
suspend fun updateAll(values: List<MangaUpdate>): Boolean
}

View file

@ -19,7 +19,11 @@ class GetTracks(
}
}
suspend fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.subscribeTracksByMangaId(mangaId)
fun subscribe(): Flow<List<Track>> {
return trackRepository.getTracksAsFlow()
}
fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.getTracksByMangaIdAsFlow(mangaId)
}
}

View file

@ -7,7 +7,9 @@ interface TrackRepository {
suspend fun getTracksByMangaId(mangaId: Long): List<Track>
suspend fun subscribeTracksByMangaId(mangaId: Long): Flow<List<Track>>
fun getTracksAsFlow(): Flow<List<Track>>
fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>>
suspend fun delete(mangaId: Long, syncId: Long)

View file

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

View file

@ -28,6 +28,4 @@ interface CategoryQueries : DbProvider {
.build(),
)
.prepare()
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
}

View file

@ -60,8 +60,6 @@ interface MangaQueries : DbProvider {
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
fun insertMangas(mangas: List<Manga>) = 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()
}

View file

@ -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.
*/

View file

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

View file

@ -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<Pair<Boolean, Manga?>>()
@ -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

View file

@ -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<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
presenter.updateMangasToCategories(mangas, addCategories, removeCategories)
presenter.setMangaCategories(mangas, addCategories, removeCategories)
destroyActionModeIfNeeded()
}

View file

@ -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<Int, List<LibraryItem>>
* 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<Long, Map<Int, Boolean>>): LibraryMap {
private fun applyFilters(map: LibraryMap, trackMap: Map<Long, Map<Long, Boolean>>): 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<Category>, 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<List<Category>> {
return db.getCategories().asRxObservable()
return getCategories.subscribe().map { it.map { it.toDbCategory() } }.asObservable()
}
/**
@ -378,7 +410,36 @@ class LibraryPresenter(
private fun getLibraryMangasObservable(): Observable<LibraryMap> {
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<String>?, 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<Map<Long, Map<Int, Boolean>>> {
private fun getFilterObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
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<Map<Long, Map<Int, Boolean>>> {
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<Map<Long, Map<Long, Boolean>>> {
// 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<Manga>): Collection<Category> {
suspend fun getCommonCategories(mangas: List<Manga>): Collection<Category> {
if (mangas.isEmpty()) return emptyList()
return mangas.toSet()
.map { db.getCategoriesForManga(it).executeAsBlocking() }
.reduce { set1: Iterable<Category>, 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<Manga>): Collection<Category> {
suspend fun getMixCategories(mangas: List<Manga>): Collection<Category> {
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<Manga>) {
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<Manga>, 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<Manga>, deleteFromLibrary: Boolean, deleteChapters: Boolean) {
fun removeMangas(mangaList: List<Manga>, 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<Category>, mangas: List<Manga>) {
val mc = mutableListOf<MangaCategory>()
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<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
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<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
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)
}
}
}
}

View file

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

View file

@ -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<MangaController>() {
@ -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)
}
}

View file

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

View file

@ -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;