Use SQLDelight on Library screen (#7432)

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

Thank Syer for asObservable

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

Co-authored-by: jobobby04 <17078382+jobobby04@users.noreply.github.com>
(cherry picked from commit 05085fe57fe4c3ada497f93b8cd282a5009cdbbb)

# Conflicts:
#	app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/CategoryQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/TrackQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt
This commit is contained in:
Andreas 2022-07-02 18:55:34 +02:00 committed by Jobobby04
parent 1087f07614
commit 2c87a8fd02
24 changed files with 556 additions and 222 deletions

View File

@ -1,10 +1,17 @@
package eu.kanade.core.util 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.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import rx.Emitter
import rx.Observable import rx.Observable
import rx.Observer import rx.Observer
import kotlin.coroutines.CoroutineContext
fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow { fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val observer = object : Observer<T> { val observer = object : Observer<T> {
@ -23,3 +30,32 @@ fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
val subscription = subscribe(observer) val subscription = subscribe(observer)
awaitClose { subscription.unsubscribe() } 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

@ -7,6 +7,7 @@ import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList import com.squareup.sqldelight.runtime.coroutines.mapToList
import com.squareup.sqldelight.runtime.coroutines.mapToOne import com.squareup.sqldelight.runtime.coroutines.mapToOne
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
import eu.kanade.data.manga.LibraryQuery
import eu.kanade.tachiyomi.Database import eu.kanade.tachiyomi.Database
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -87,4 +88,8 @@ class AndroidDatabaseHandler(
val context = getCurrentDatabaseContext() val context = getCurrentDatabaseContext()
return withContext(context) { block(db) } return withContext(context) { block(db) }
} }
// SY -->
fun getLibraryQuery() = LibraryQuery(driver)
// SY <--
} }

View File

@ -12,10 +12,28 @@ class CategoryRepositoryImpl(
private val handler: DatabaseHandler, private val handler: DatabaseHandler,
) : CategoryRepository { ) : CategoryRepository {
// SY -->
override suspend fun awaitAll(): List<Category> {
return handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
}
// SY <--
override fun getAll(): Flow<List<Category>> { override fun getAll(): Flow<List<Category>> {
return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) } 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) @Throws(DuplicateNameException::class)
override suspend fun insert(name: String, order: Long) { override suspend fun insert(name: String, order: Long) {
if (checkDuplicateName(name)) throw DuplicateNameException(name) if (checkDuplicateName(name)) throw DuplicateNameException(name)
@ -55,12 +73,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 { override suspend fun checkDuplicateName(name: String): Boolean {
return handler return handler
.awaitList { categoriesQueries.getCategories() } .awaitList { categoriesQueries.getCategories() }

View File

@ -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<LibraryManga>(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"
}

View File

@ -47,7 +47,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) { handler.await(inTransaction = true) {
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId) mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
categoryIds.map { categoryId -> categoryIds.map { categoryId ->
@ -58,32 +58,48 @@ class MangaRepositoryImpl(
override suspend fun update(update: MangaUpdate): Boolean { override suspend fun update(update: MangaUpdate): Boolean {
return try { return try {
handler.await { partialUpdate(update)
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),
)
}
true true
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
false 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,
filteredScanlators = value.filteredScanlators?.let(listOfStringsAndAdapter::encode),
mangaId = value.id,
)
}
}
}
} }

View File

@ -9,13 +9,27 @@ class TrackRepositoryImpl(
private val handler: DatabaseHandler, private val handler: DatabaseHandler,
) : TrackRepository { ) : TrackRepository {
// SY -->
override suspend fun getTracks(): List<Track> {
return handler.awaitList {
manga_syncQueries.getTracks(trackMapper)
}
}
// SY <--
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> { override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
return handler.awaitList { return handler.awaitList {
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper) manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
} }
} }
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 { return handler.subscribeToList {
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper) 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.DeleteCategory
import eu.kanade.domain.category.interactor.GetCategories import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.InsertCategory 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.interactor.UpdateCategory
import eu.kanade.domain.category.repository.CategoryRepository import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.chapter.interactor.GetChapter import eu.kanade.domain.chapter.interactor.GetChapter
@ -77,7 +77,7 @@ class DomainModule : InjektModule {
addFactory { ResetViewerFlags(get()) } addFactory { ResetViewerFlags(get()) }
addFactory { SetMangaChapterFlags(get()) } addFactory { SetMangaChapterFlags(get()) }
addFactory { UpdateManga(get()) } addFactory { UpdateManga(get()) }
addFactory { MoveMangaToCategories(get()) } addFactory { SetMangaCategories(get()) }
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) } addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
addFactory { DeleteTrack(get()) } addFactory { DeleteTrack(get()) }

View File

@ -8,11 +8,21 @@ class GetCategories(
private val categoryRepository: CategoryRepository, private val categoryRepository: CategoryRepository,
) { ) {
// SY -->
suspend fun await(): List<Category> {
return categoryRepository.awaitAll()
}
// SY <--
fun subscribe(): Flow<List<Category>> { fun subscribe(): Flow<List<Category>> {
return categoryRepository.getAll() return categoryRepository.getAll()
} }
fun subscribe(mangaId: Long): Flow<List<Category>> {
return categoryRepository.getCategoriesByMangaIdAsFlow(mangaId)
}
suspend fun await(mangaId: Long): List<Category> { 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 eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority import logcat.LogPriority
class MoveMangaToCategories( class SetMangaCategories(
private val mangaRepository: MangaRepository, private val mangaRepository: MangaRepository,
) { ) {
suspend fun await(mangaId: Long, categoryIds: List<Long>) { suspend fun await(mangaId: Long, categoryIds: List<Long>) {
try { try {
mangaRepository.moveMangaToCategories(mangaId, categoryIds) mangaRepository.setMangaCategories(mangaId, categoryIds)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
} }

View File

@ -6,8 +6,16 @@ import kotlinx.coroutines.flow.Flow
interface CategoryRepository { interface CategoryRepository {
// SY -->
suspend fun awaitAll(): List<Category>
// SY <--
fun getAll(): Flow<List<Category>> fun getAll(): Flow<List<Category>>
suspend fun getCategoriesByMangaId(mangaId: Long): List<Category>
fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>>
@Throws(DuplicateNameException::class) @Throws(DuplicateNameException::class)
suspend fun insert(name: String, order: Long) suspend fun insert(name: String, order: Long)
@ -16,8 +24,6 @@ interface CategoryRepository {
suspend fun delete(categoryId: Long) suspend fun delete(categoryId: Long)
suspend fun getCategoriesForManga(mangaId: Long): List<Category>
suspend fun checkDuplicateName(name: String): Boolean suspend fun checkDuplicateName(name: String): Boolean
} }

View File

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

View File

@ -18,7 +18,9 @@ interface MangaRepository {
suspend fun resetViewerFlags(): Boolean 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 update(update: MangaUpdate): Boolean
suspend fun updateAll(values: List<MangaUpdate>): Boolean
} }

View File

@ -10,6 +10,17 @@ class GetTracks(
private val trackRepository: TrackRepository, private val trackRepository: TrackRepository,
) { ) {
// SY -->
suspend fun await(): List<Track> {
return try {
trackRepository.getTracks()
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
emptyList()
}
}
// SY <--
suspend fun await(mangaId: Long): List<Track> { suspend fun await(mangaId: Long): List<Track> {
return try { return try {
trackRepository.getTracksByMangaId(mangaId) trackRepository.getTracksByMangaId(mangaId)
@ -19,7 +30,11 @@ class GetTracks(
} }
} }
suspend fun subscribe(mangaId: Long): Flow<List<Track>> { fun subscribe(): Flow<List<Track>> {
return trackRepository.subscribeTracksByMangaId(mangaId) return trackRepository.getTracksAsFlow()
}
fun subscribe(mangaId: Long): Flow<List<Track>> {
return trackRepository.getTracksByMangaIdAsFlow(mangaId)
} }
} }

View File

@ -5,9 +5,15 @@ import kotlinx.coroutines.flow.Flow
interface TrackRepository { interface TrackRepository {
// SY -->
suspend fun getTracks(): List<Track>
// SY <--
suspend fun getTracksByMangaId(mangaId: Long): List<Track> 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) suspend fun delete(mangaId: Long, syncId: Long)

View File

@ -29,7 +29,5 @@ interface CategoryQueries : DbProvider {
) )
.prepare() .prepare()
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare() fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
} }

View File

@ -137,36 +137,6 @@ interface MangaQueries : DbProvider {
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() 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 --> // SY -->
fun getMangaWithMetadata() = db.get() fun getMangaWithMetadata() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)

View File

@ -152,39 +152,6 @@ fun getHistoryByMangaId() =
""" """
// SY <-- // 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. * Query to get the categories for a manga.
*/ */

View File

@ -9,6 +9,8 @@ import androidx.recyclerview.widget.RecyclerView
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter 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.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category 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) { override fun onItemMove(fromPosition: Int, toPosition: Int) {
if (fromPosition == toPosition) return if (fromPosition == toPosition) return
controller.invalidateActionMode() controller.invalidateActionMode()
@ -433,13 +437,23 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
if (category.id == 0) { if (category.id == 0) {
preferences.defaultMangaOrder().set(mangaIds.joinToString("/")) preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
} else { } else {
db.insertCategory(category).asRxObservable().subscribe() scope.launch {
updateCategory.await(CategoryUpdate(category.id!!.toLong(), mangaOrder = mangaIds))
}
} }
if (preferences.categorizedDisplaySettings().get() && category.id != 0) { if (preferences.categorizedDisplaySettings().get() && category.id != 0) {
if (SortModeSetting.fromFlag(category.sortMode) != SortModeSetting.DRAG_AND_DROP) { if (SortModeSetting.fromFlag(category.sortMode) != SortModeSetting.DRAG_AND_DROP) {
category.sortMode = SortModeSetting.DRAG_AND_DROP.flag category.sortMode = SortModeSetting.DRAG_AND_DROP.flag
category.sortDirection = SortDirectionSetting.ASCENDING.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) { } else if (preferences.librarySortingMode().get() != SortModeSetting.DRAG_AND_DROP) {
preferences.librarySortingAscending().set(SortDirectionSetting.ASCENDING) preferences.librarySortingAscending().set(SortDirectionSetting.ASCENDING)

View File

@ -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.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity 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.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
@ -53,6 +55,7 @@ import exh.source.mangaDexSourceIds
import exh.source.nHentaiSourceIds import exh.source.nHentaiSourceIds
import exh.ui.LoaderManager import exh.ui.LoaderManager
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -267,6 +270,7 @@ class LibraryController(
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
adapter?.onDestroy() adapter?.onDestroy()
adapter = null adapter = null
settingsSheet?.sheetScope?.cancel()
settingsSheet = null settingsSheet = null
tabsVisibilitySubscription?.unsubscribe() tabsVisibilitySubscription?.unsubscribe()
tabsVisibilitySubscription = null tabsVisibilitySubscription = null
@ -633,25 +637,29 @@ class LibraryController(
* Move the selected manga to a list of categories. * Move the selected manga to a list of categories.
*/ */
private fun showMangaCategoriesDialog() { private fun showMangaCategoriesDialog() {
// Create a copy of selected manga viewScope.launchIO {
val mangas = selectedMangas.toList() // 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. // Hide the default category because it has a different behavior than the ones from db.
val categories = presenter.categories.filter { it.id != 0 } val categories = presenter.categories.filter { it.id != 0 }
// Get indexes of the common categories to preselect. // Get indexes of the common categories to preselect.
val common = presenter.getCommonCategories(mangas) val common = presenter.getCommonCategories(mangas)
// Get indexes of the mix categories to preselect. // Get indexes of the mix categories to preselect.
val mix = presenter.getMixCategories(mangas) val mix = presenter.getMixCategories(mangas)
val preselected = categories.map { val preselected = categories.map {
when (it) { when (it) {
in common -> QuadStateTextView.State.CHECKED.ordinal in common -> QuadStateTextView.State.CHECKED.ordinal
in mix -> QuadStateTextView.State.INDETERMINATE.ordinal in mix -> QuadStateTextView.State.INDETERMINATE.ordinal
else -> QuadStateTextView.State.UNCHECKED.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() { private fun downloadUnreadChapters() {
@ -691,7 +699,7 @@ class LibraryController(
// SY <-- // SY <--
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) { override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
presenter.updateMangasToCategories(mangas, addCategories, removeCategories) presenter.setMangaCategories(mangas, addCategories, removeCategories)
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
} }

View File

@ -2,13 +2,27 @@ package eu.kanade.tachiyomi.ui.library
import android.os.Bundle import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay 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.R
import eu.kanade.tachiyomi.data.cache.CoverCache 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.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -32,9 +46,10 @@ import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.isEhBasedManga import exh.source.isEhBasedManga
import exh.util.executeOnIO
import exh.util.isLewd import exh.util.isLewd
import exh.util.nullIfBlank import exh.util.nullIfBlank
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -59,7 +74,13 @@ private typealias LibraryMap = Map<Int, List<LibraryItem>>
* Presenter of [LibraryController]. * Presenter of [LibraryController].
*/ */
class LibraryPresenter( 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 preferences: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
@ -67,6 +88,8 @@ class LibraryPresenter(
private val trackManager: TrackManager = Injekt.get(), private val trackManager: TrackManager = Injekt.get(),
// SY --> // SY -->
private val customMangaManager: CustomMangaManager = Injekt.get(), private val customMangaManager: CustomMangaManager = Injekt.get(),
private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
private val getMergedChaptersByMangaId: GetMergedChapterByMangaId = Injekt.get(),
// SY <-- // SY <--
) : BasePresenter<LibraryController>() { ) : BasePresenter<LibraryController>() {
@ -128,6 +151,7 @@ class LibraryPresenter(
* Subscribes to library if needed. * Subscribes to library if needed.
*/ */
fun subscribeLibrary() { fun subscribeLibrary() {
// TODO: Move this to a coroutine world
if (librarySubscription.isNullOrUnsubscribed()) { if (librarySubscription.isNullOrUnsubscribed()) {
librarySubscription = getLibraryObservable() librarySubscription = getLibraryObservable()
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ -> .combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
@ -160,7 +184,7 @@ class LibraryPresenter(
* *
* @param map the map to filter. * @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 downloadedOnly = preferences.downloadedOnly().get()
val filterDownloaded = preferences.filterDownloaded().get() val filterDownloaded = preferences.filterDownloaded().get()
val filterUnread = preferences.filterUnread().get() val filterUnread = preferences.filterUnread().get()
@ -294,7 +318,11 @@ class LibraryPresenter(
item.downloadCount = if (showDownloadBadges) { item.downloadCount = if (showDownloadBadges) {
// SY --> // SY -->
if (item.manga.source == MERGED_SOURCE_ID) { 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 /* SY <-- */ downloadManager.getDownloadCount(item.manga)
} else { } else {
// Unset download count if not enabled // Unset download count if not enabled
@ -333,18 +361,30 @@ class LibraryPresenter(
private fun applySort(categories: List<Category>, map: LibraryMap): LibraryMap { private fun applySort(categories: List<Category>, map: LibraryMap): LibraryMap {
val lastReadManga by lazy { val lastReadManga by lazy {
var counter = 0 var counter = 0
// Result comes as newest to oldest so it's reversed // TODO: Make [applySort] a suspended function
db.getLastReadManga().executeAsBlocking().reversed().associate { it.id!! to counter++ } runBlocking {
handler.awaitList {
mangasQueries.getLastRead()
}.associate { it._id to counter++ }
}
} }
val latestChapterManga by lazy { val latestChapterManga by lazy {
var counter = 0 var counter = 0
// Result comes as newest to oldest so it's reversed // TODO: Make [applySort] a suspended function
db.getLatestChapterManga().executeAsBlocking().reversed().associate { it.id!! to counter++ } runBlocking {
handler.awaitList {
mangasQueries.getLatestByChapterUploadDate()
}.associate { it._id to counter++ }
}
} }
val chapterFetchDateManga by lazy { val chapterFetchDateManga by lazy {
var counter = 0 var counter = 0
// Result comes as newest to oldest so it's reversed // TODO: Make [applySort] a suspended function
db.getChapterFetchDateManga().executeAsBlocking().reversed().associate { it.id!! to counter++ } runBlocking {
handler.awaitList {
mangasQueries.getLatestByChapterFetchDate()
}.associate { it._id to counter++ }
}
} }
// SY --> // SY -->
@ -509,7 +549,7 @@ class LibraryPresenter(
* @return an observable of the categories. * @return an observable of the categories.
*/ */
private fun getCategoriesObservable(): Observable<List<Category>> { private fun getCategoriesObservable(): Observable<List<Category>> {
return db.getCategories().asRxObservable() return getCategories.subscribe().map { it.map { it.toDbCategory() } }.asObservable()
} }
/** /**
@ -521,7 +561,40 @@ class LibraryPresenter(
private fun getLibraryMangasObservable(): Observable<LibraryMap> { private fun getLibraryMangasObservable(): Observable<LibraryMap> {
val defaultLibraryDisplayMode = preferences.libraryDisplayMode() val defaultLibraryDisplayMode = preferences.libraryDisplayMode()
val shouldSetFromCategory = preferences.categorizedDisplaySettings() 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<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, filteredScanlators: List<String>?, 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 -> .map { list ->
list.map { libraryManga -> list.map { libraryManga ->
// Display mode based on user preference: take it from global library setting or category // 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. * @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 } return getTracksObservable().combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { tracks, _ -> tracks }
} }
@ -548,16 +621,20 @@ class LibraryPresenter(
* *
* @return an observable of tracked manga. * @return an observable of tracked manga.
*/ */
private fun getTracksObservable(): Observable<Map<Long, Map<Int, Boolean>>> { private fun getTracksObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
return db.getTracks().asRxObservable().map { tracks -> // TODO: Move this to domain/data layer
tracks.groupBy { it.manga_id } return getTracks.subscribe()
.mapValues { tracksForMangaId -> .asObservable().map { tracks ->
// Check if any of the trackers is logged in for the current manga id tracks
tracksForMangaId.value.associate { .groupBy { it.mangaId }
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) .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. * @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() if (mangas.isEmpty()) return emptyList()
return mangas.toSet() return mangas.toSet()
.map { db.getCategoriesForManga(it).executeAsBlocking() } .map { getCategories.await(it.id!!).map { it.toDbCategory() } }
.reduce { set1: Iterable<Category>, set2 -> set1.intersect(set2).toMutableList() } .reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
} }
/** /**
@ -623,9 +700,9 @@ class LibraryPresenter(
* *
* @param mangas the list of manga. * @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() 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() } val common = mangaCategories.reduce { set1, set2 -> set1.intersect(set2).toMutableList() }
return mangaCategories.flatten().distinct().subtract(common).toMutableList() return mangaCategories.flatten().distinct().subtract(common).toMutableList()
} }
@ -640,25 +717,25 @@ class LibraryPresenter(
launchIO { launchIO {
if (manga.source == MERGED_SOURCE_ID) { if (manga.source == MERGED_SOURCE_ID) {
val mergedSource = sourceManager.get(MERGED_SOURCE_ID) as MergedSource val mergedSource = sourceManager.get(MERGED_SOURCE_ID) as MergedSource
val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking() val mergedMangas = getMergedMangaById.await(manga.id!!)
mergedSource mergedSource
.getChaptersAsBlocking(manga.id!!) .getChaptersAsBlocking(manga.id!!)
.filter { !it.read } .filter { !it.read }
.groupBy { it.manga_id!! } .groupBy { it.manga_id!! }
.forEach ab@{ (mangaId, chapters) -> .forEach ab@{ (mangaId, chapters) ->
val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@ab val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@ab
downloadManager.downloadChapters(mergedManga, chapters) downloadManager.downloadChapters(mergedManga.toDbManga(), chapters)
} }
} else { } else {
/* SY --> */ /* SY --> */
val chapters = if (manga.isEhBasedManga()) { 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() if (!chapter.read) listOf(chapter) else emptyList()
} ?: emptyList() } ?: emptyList()
} else /* SY <-- */ db.getChapters(manga).executeAsBlocking() } else /* SY <-- */ getChapterByMangaId.await(manga.id!!)
.filter { !it.read } .filter { !it.read }
downloadManager.downloadChapters(manga, chapters) downloadManager.downloadChapters(manga, chapters.map { it.toDbChapter() })
} }
} }
} }
@ -711,21 +788,20 @@ class LibraryPresenter(
fun markReadStatus(mangas: List<Manga>, read: Boolean) { fun markReadStatus(mangas: List<Manga>, read: Boolean) {
mangas.forEach { manga -> mangas.forEach { manga ->
launchIO { launchIO {
val chapters = if (manga.source == MERGED_SOURCE_ID) { val chapters = if (manga.source == MERGED_SOURCE_ID) getMergedChaptersByMangaId.await(manga.id!!) else getChapterByMangaId.await(manga.id!!)
(sourceManager.get(MERGED_SOURCE_ID) as MergedSource).getChaptersAsBlocking(manga.id!!)
} else { val toUpdate = chapters
db.getChapters(manga).executeAsBlocking() .map { chapter ->
} ChapterUpdate(
chapters.forEach { read = read,
it.read = read lastPageRead = if (read) 0 else null,
if (!read) { id = chapter.id,
it.last_page_read = 0 )
} }
} updateChapter.awaitAll(toUpdate)
db.updateChaptersProgress(chapters).executeAsBlocking()
if (read && preferences.removeAfterMarkedAsRead()) { 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 -> sourceManager.get(manga.source)?.let { source ->
// SY --> // SY -->
if (source is MergedSource) { 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) } val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) }
chapters.groupBy { it.manga_id }.forEach { (mangaId, chapters) -> chapters.groupBy { it.manga_id }.forEach { (mangaId, chapters) ->
val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@forEach val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@forEach
val mergedMangaSource = sources.firstOrNull { it.id == mergedManga.source } ?: 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) } else /* SY <-- */ downloadManager.deleteChapters(chapters, manga, source)
} }
@ -749,20 +825,23 @@ class LibraryPresenter(
/** /**
* Remove the selected manga. * 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 deleteFromLibrary whether to delete manga from library.
* @param deleteChapters whether to delete downloaded chapters. * @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 { launchIO {
val mangaToDelete = mangas.distinctBy { it.id } val mangaToDelete = mangaList.distinctBy { it.id }
if (deleteFromLibrary) { if (deleteFromLibrary) {
mangaToDelete.forEach { val toDelete = mangaToDelete.map {
it.favorite = false
it.removeCovers(coverCache) it.removeCovers(coverCache)
MangaUpdate(
favorite = false,
id = it.id!!,
)
} }
db.insertMangas(mangaToDelete).executeAsBlocking() updateManga.awaitAll(toDelete)
} }
if (deleteChapters) { if (deleteChapters) {
@ -770,11 +849,11 @@ class LibraryPresenter(
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
if (source != null) { if (source != null) {
if (source is MergedSource) { 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) } val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) }
mergedMangas.forEach merge@{ mergedManga -> mergedMangas.forEach merge@{ mergedManga ->
val mergedSource = sources.firstOrNull { mergedManga.source == it.id } as? HttpSource ?: return@merge 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) } 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 mangaList the list of manga to move.
* @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 addCategories the categories to add for all mangas. * @param addCategories the categories to add for all mangas.
* @param removeCategories the categories to remove in all mangas. * @param removeCategories the categories to remove in all mangas.
*/ */
fun updateMangasToCategories(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) { fun setMangaCategories(mangaList: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
val mangaCategories = mangas.map { manga -> presenterScope.launchIO {
val categories = db.getCategoriesForManga(manga).executeAsBlocking() mangaList.map { manga ->
.subtract(removeCategories).plus(addCategories).distinct() val categoryIds = getCategories.await(manga.id!!)
categories.map { MangaCategory.create(manga, it) } .map { it.toDbCategory() }
}.flatten() .subtract(removeCategories)
.plus(addCategories)
db.setMangaCategories(mangaCategories, mangas) .mapNotNull { it.id?.toLong() }
setMangaCategories.await(manga.id!!, categoryIds)
}
}
} }
// SY --> // SY -->
@ -821,7 +887,7 @@ class LibraryPresenter(
fun getFirstUnread(manga: Manga): Chapter? { fun getFirstUnread(manga: Manga): Chapter? {
val chapters = if (manga.source == MERGED_SOURCE_ID) { val chapters = if (manga.source == MERGED_SOURCE_ID) {
(sourceManager.get(MERGED_SOURCE_ID) as MergedSource).getChaptersAsBlocking(manga.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()) { return if (manga.isEhBasedManga()) {
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0) val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
if (chapter?.read == false) chapter else null if (chapter?.read == false) chapter else null
@ -867,10 +933,10 @@ class LibraryPresenter(
when (groupType) { when (groupType) {
LibraryGroup.BY_TRACK_STATUS -> { LibraryGroup.BY_TRACK_STATUS -> {
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id } val tracks = runBlocking { getTracks.await() }.groupBy { it.mangaId }
libraryManga.forEach { libraryItem -> libraryManga.forEach { libraryItem ->
val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track -> 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 } ?: TrackStatus.OTHER
map.getOrPut(status.int) { mutableListOf() } += libraryItem map.getOrPut(status.int) { mutableListOf() } += libraryItem

View File

@ -4,8 +4,10 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import com.bluelinelabs.conductor.Router 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.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager 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.DisplayModeSetting
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting 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
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog 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.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -23,6 +30,7 @@ import uy.kohesive.injekt.injectLazy
class LibrarySettingsSheet( class LibrarySettingsSheet(
router: Router, router: Router,
private val trackManager: TrackManager = Injekt.get(), private val trackManager: TrackManager = Injekt.get(),
private val updateCategory: UpdateCategory = Injekt.get(),
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit, onGroupClickListener: (ExtendedNavigationView.Group) -> Unit,
) : TabbedBottomSheetDialog(router.activity!!) { ) : TabbedBottomSheetDialog(router.activity!!) {
@ -30,7 +38,8 @@ class LibrarySettingsSheet(
private val sort: Sort private val sort: Sort
private val display: Display private val display: Display
private val grouping: Grouping private val grouping: Grouping
private val db: DatabaseHelper by injectLazy()
val sheetScope = CoroutineScope(Job() + Dispatchers.IO)
init { init {
filters = Filter(router.activity!!) 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 <-- */) { if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0 /* SY --> */ && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT /* SY <-- */) {
currentCategory?.sortDirection = flag.flag currentCategory?.sortDirection = flag.flag
sheetScope.launchIO {
db.insertCategory(currentCategory!!).executeAsBlocking() updateCategory.await(
CategoryUpdate(
id = currentCategory!!.id?.toLong()!!,
flags = currentCategory!!.flags.toLong(),
),
)
}
} else { } else {
preferences.librarySortingAscending().set(flag) 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 <-- */) { if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0 /* SY --> */ && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT /* SY <-- */) {
currentCategory?.sortMode = flag.flag currentCategory?.sortMode = flag.flag
sheetScope.launchIO {
db.insertCategory(currentCategory!!).executeAsBlocking() updateCategory.await(
CategoryUpdate(
id = currentCategory!!.id?.toLong()!!,
flags = currentCategory!!.flags.toLong(),
),
)
}
} else { } else {
preferences.librarySortingMode().set(flag) 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 <-- */) { if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0 /* SY --> */ && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT /* SY <-- */) {
currentCategory?.displayMode = flag.flag currentCategory?.displayMode = flag.flag
sheetScope.launchIO {
db.insertCategory(currentCategory!!).executeAsBlocking() updateCategory.await(
CategoryUpdate(
id = currentCategory!!.id?.toLong()!!,
flags = currentCategory!!.flags.toLong(),
),
)
}
} else { } else {
preferences.libraryDisplayMode().set(flag) preferences.libraryDisplayMode().set(flag)
} }
@ -518,7 +545,9 @@ class LibrarySettingsSheet(
inner class InternalGroup : Group { inner class InternalGroup : Group {
private val groupItems = mutableListOf<Item.DrawableSelection>() private val groupItems = mutableListOf<Item.DrawableSelection>()
private val trackManager: TrackManager = Injekt.get() private val trackManager: TrackManager = Injekt.get()
private val hasCategories = Injekt.get<DatabaseHelper>().getCategories().executeAsBlocking().size != 0 private val hasCategories = runBlocking {
Injekt.get<GetCategories>().await().isNotEmpty()
}
init { init {
val groupingItems = mutableListOf( val groupingItems = mutableListOf(

View File

@ -4,7 +4,7 @@ import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import eu.kanade.domain.category.interactor.GetCategories 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.GetMergedChapterByMangaId
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
@ -135,7 +135,7 @@ class MangaPresenter(
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val deleteTrack: DeleteTrack = Injekt.get(), private val deleteTrack: DeleteTrack = Injekt.get(),
private val getTracks: GetTracks = 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 insertTrack: InsertTrack = Injekt.get(),
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(), private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
) : BasePresenter<MangaController>() { ) : BasePresenter<MangaController>() {
@ -697,7 +697,7 @@ class MangaPresenter(
val mangaId = manga.id ?: return val mangaId = manga.id ?: return
val categoryIds = categories.mapNotNull { it.id?.toLong() } val categoryIds = categories.mapNotNull { it.id?.toLong() }
presenterScope.launchIO { presenterScope.launchIO {
moveMangaToCategories.await(mangaId, categoryIds) setMangaCategories.await(mangaId, categoryIds)
} }
} }

View File

@ -21,6 +21,10 @@ delete:
DELETE FROM manga_sync DELETE FROM manga_sync
WHERE manga_id = :mangaId AND sync_id = :syncId; WHERE manga_id = :mangaId AND sync_id = :syncId;
getTracks:
SELECT *
FROM manga_sync;
getTracksByMangaId: getTracksByMangaId:
SELECT * SELECT *
FROM manga_sync FROM manga_sync

View File

@ -94,6 +94,61 @@ AND C.date_upload > :after
AND C.date_fetch > M.date_added AND C.date_fetch > M.date_added
ORDER BY C.date_upload DESC; 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: deleteMangasNotInLibraryBySourceIds:
DELETE FROM mangas DELETE FROM mangas
WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN ( WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN (