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:
parent
1087f07614
commit
2c87a8fd02
@ -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,
|
||||
)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToOne
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
|
||||
import eu.kanade.data.manga.LibraryQuery
|
||||
import eu.kanade.tachiyomi.Database
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -87,4 +88,8 @@ class AndroidDatabaseHandler(
|
||||
val context = getCurrentDatabaseContext()
|
||||
return withContext(context) { block(db) }
|
||||
}
|
||||
|
||||
// SY -->
|
||||
fun getLibraryQuery() = LibraryQuery(driver)
|
||||
// SY <--
|
||||
}
|
||||
|
@ -12,10 +12,28 @@ class CategoryRepositoryImpl(
|
||||
private val handler: DatabaseHandler,
|
||||
) : CategoryRepository {
|
||||
|
||||
// SY -->
|
||||
override suspend fun awaitAll(): List<Category> {
|
||||
return handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
|
||||
}
|
||||
// SY <--
|
||||
|
||||
override fun getAll(): Flow<List<Category>> {
|
||||
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)
|
||||
@ -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 {
|
||||
return handler
|
||||
.awaitList { categoriesQueries.getCategories() }
|
||||
|
97
app/src/main/java/eu/kanade/data/manga/LibraryQuery.kt
Normal file
97
app/src/main/java/eu/kanade/data/manga/LibraryQuery.kt
Normal 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"
|
||||
}
|
@ -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) {
|
||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
|
||||
categoryIds.map { categoryId ->
|
||||
@ -58,32 +58,48 @@ class MangaRepositoryImpl(
|
||||
|
||||
override suspend fun update(update: MangaUpdate): Boolean {
|
||||
return try {
|
||||
handler.await {
|
||||
mangasQueries.update(
|
||||
source = update.source,
|
||||
url = update.url,
|
||||
artist = update.artist,
|
||||
author = update.author,
|
||||
description = update.description,
|
||||
genre = update.genre?.let(listOfStringsAdapter::encode),
|
||||
title = update.title,
|
||||
status = update.status,
|
||||
thumbnailUrl = update.thumbnailUrl,
|
||||
favorite = update.favorite?.toLong(),
|
||||
lastUpdate = update.lastUpdate,
|
||||
initialized = update.initialized?.toLong(),
|
||||
viewer = update.viewerFlags,
|
||||
chapterFlags = update.chapterFlags,
|
||||
coverLastModified = update.coverLastModified,
|
||||
dateAdded = update.dateAdded,
|
||||
mangaId = update.id,
|
||||
filteredScanlators = update.filteredScanlators?.let(listOfStringsAndAdapter::encode),
|
||||
)
|
||||
}
|
||||
partialUpdate(update)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateAll(values: List<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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,13 +9,27 @@ class TrackRepositoryImpl(
|
||||
private val handler: DatabaseHandler,
|
||||
) : TrackRepository {
|
||||
|
||||
// SY -->
|
||||
override suspend fun getTracks(): List<Track> {
|
||||
return handler.awaitList {
|
||||
manga_syncQueries.getTracks(trackMapper)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
|
||||
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
|
||||
return handler.awaitList {
|
||||
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 {
|
||||
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
|
||||
}
|
||||
|
@ -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()) }
|
||||
|
@ -8,11 +8,21 @@ class GetCategories(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
// SY -->
|
||||
suspend fun await(): List<Category> {
|
||||
return categoryRepository.awaitAll()
|
||||
}
|
||||
// SY <--
|
||||
|
||||
fun subscribe(): Flow<List<Category>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -6,8 +6,16 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface CategoryRepository {
|
||||
|
||||
// SY -->
|
||||
suspend fun awaitAll(): List<Category>
|
||||
// SY <--
|
||||
|
||||
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 +24,6 @@ interface CategoryRepository {
|
||||
|
||||
suspend fun delete(categoryId: Long)
|
||||
|
||||
suspend fun getCategoriesForManga(mangaId: Long): List<Category>
|
||||
|
||||
suspend fun checkDuplicateName(name: String): Boolean
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -10,6 +10,17 @@ class GetTracks(
|
||||
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> {
|
||||
return try {
|
||||
trackRepository.getTracksByMangaId(mangaId)
|
||||
@ -19,7 +30,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)
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,15 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TrackRepository {
|
||||
|
||||
// SY -->
|
||||
suspend fun getTracks(): List<Track>
|
||||
// SY <--
|
||||
|
||||
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)
|
||||
|
||||
|
@ -29,7 +29,5 @@ interface CategoryQueries : DbProvider {
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
||||
|
||||
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
|
||||
}
|
||||
|
@ -137,36 +137,6 @@ interface MangaQueries : DbProvider {
|
||||
|
||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun getLastReadManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getLastReadMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getLatestChapterManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getLatestChapterMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
fun getChapterFetchDateManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getChapterFetchDateMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.prepare()
|
||||
|
||||
// SY -->
|
||||
fun getMangaWithMetadata() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
|
@ -152,39 +152,6 @@ fun getHistoryByMangaId() =
|
||||
"""
|
||||
// SY <--
|
||||
|
||||
fun getLastReadMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
JOIN ${History.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER BY max DESC
|
||||
"""
|
||||
|
||||
fun getLatestChapterMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER by max DESC
|
||||
"""
|
||||
|
||||
fun getChapterFetchDateMangaQuery() =
|
||||
"""
|
||||
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER by max DESC
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the categories for a manga.
|
||||
*/
|
||||
|
@ -9,6 +9,8 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
import eu.kanade.domain.category.interactor.UpdateCategory
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
@ -425,6 +427,8 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
}
|
||||
}
|
||||
|
||||
private val updateCategory: UpdateCategory by injectLazy()
|
||||
|
||||
override fun onItemMove(fromPosition: Int, toPosition: Int) {
|
||||
if (fromPosition == toPosition) return
|
||||
controller.invalidateActionMode()
|
||||
@ -433,13 +437,23 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
if (category.id == 0) {
|
||||
preferences.defaultMangaOrder().set(mangaIds.joinToString("/"))
|
||||
} else {
|
||||
db.insertCategory(category).asRxObservable().subscribe()
|
||||
scope.launch {
|
||||
updateCategory.await(CategoryUpdate(category.id!!.toLong(), mangaOrder = mangaIds))
|
||||
}
|
||||
}
|
||||
if (preferences.categorizedDisplaySettings().get() && category.id != 0) {
|
||||
if (SortModeSetting.fromFlag(category.sortMode) != SortModeSetting.DRAG_AND_DROP) {
|
||||
category.sortMode = SortModeSetting.DRAG_AND_DROP.flag
|
||||
category.sortDirection = SortDirectionSetting.ASCENDING.flag
|
||||
db.insertCategory(category).asRxObservable().subscribe()
|
||||
scope.launch {
|
||||
updateCategory.await(
|
||||
CategoryUpdate(
|
||||
id = category.id!!.toLong(),
|
||||
flags = category.flags.toLong(),
|
||||
mangaOrder = mangaIds,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (preferences.librarySortingMode().get() != SortModeSetting.DRAG_AND_DROP) {
|
||||
preferences.librarySortingAscending().set(SortDirectionSetting.ASCENDING)
|
||||
|
@ -36,6 +36,8 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
@ -53,6 +55,7 @@ import exh.source.mangaDexSourceIds
|
||||
import exh.source.nHentaiSourceIds
|
||||
import exh.ui.LoaderManager
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -267,6 +270,7 @@ class LibraryController(
|
||||
destroyActionModeIfNeeded()
|
||||
adapter?.onDestroy()
|
||||
adapter = null
|
||||
settingsSheet?.sheetScope?.cancel()
|
||||
settingsSheet = null
|
||||
tabsVisibilitySubscription?.unsubscribe()
|
||||
tabsVisibilitySubscription = null
|
||||
@ -633,6 +637,7 @@ class LibraryController(
|
||||
* Move the selected manga to a list of categories.
|
||||
*/
|
||||
private fun showMangaCategoriesDialog() {
|
||||
viewScope.launchIO {
|
||||
// Create a copy of selected manga
|
||||
val mangas = selectedMangas.toList()
|
||||
|
||||
@ -650,9 +655,12 @@ class LibraryController(
|
||||
else -> QuadStateTextView.State.UNCHECKED.ordinal
|
||||
}
|
||||
}.toTypedArray()
|
||||
ChangeMangaCategoriesDialog(this, mangas, categories, preselected)
|
||||
launchUI {
|
||||
ChangeMangaCategoriesDialog(this@LibraryController, mangas, categories, preselected)
|
||||
.showDialog(router)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadUnreadChapters() {
|
||||
val mangas = selectedMangas.toList()
|
||||
@ -691,7 +699,7 @@ class LibraryController(
|
||||
// SY <--
|
||||
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, addCategories: List<Category>, removeCategories: List<Category>) {
|
||||
presenter.updateMangasToCategories(mangas, addCategories, removeCategories)
|
||||
presenter.setMangaCategories(mangas, addCategories, removeCategories)
|
||||
destroyActionModeIfNeeded()
|
||||
}
|
||||
|
||||
|
@ -2,13 +2,27 @@ package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import eu.kanade.core.util.asObservable
|
||||
import eu.kanade.data.AndroidDatabaseHandler
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
||||
import eu.kanade.domain.category.model.toDbCategory
|
||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||
import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId
|
||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||
import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.manga.interactor.GetMergedMangaById
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.MangaUpdate
|
||||
import eu.kanade.domain.manga.model.toDbManga
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.library.CustomMangaManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
@ -32,9 +46,10 @@ import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.source.MERGED_SOURCE_ID
|
||||
import exh.source.isEhBasedManga
|
||||
import exh.util.executeOnIO
|
||||
import exh.util.isLewd
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
@ -59,7 +74,13 @@ private typealias LibraryMap = Map<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(),
|
||||
@ -67,6 +88,8 @@ class LibraryPresenter(
|
||||
private val trackManager: TrackManager = Injekt.get(),
|
||||
// SY -->
|
||||
private val customMangaManager: CustomMangaManager = Injekt.get(),
|
||||
private val getMergedMangaById: GetMergedMangaById = Injekt.get(),
|
||||
private val getMergedChaptersByMangaId: GetMergedChapterByMangaId = Injekt.get(),
|
||||
// SY <--
|
||||
) : BasePresenter<LibraryController>() {
|
||||
|
||||
@ -128,6 +151,7 @@ class LibraryPresenter(
|
||||
* Subscribes to library if needed.
|
||||
*/
|
||||
fun subscribeLibrary() {
|
||||
// TODO: Move this to a coroutine world
|
||||
if (librarySubscription.isNullOrUnsubscribed()) {
|
||||
librarySubscription = getLibraryObservable()
|
||||
.combineLatest(badgeTriggerRelay.observeOn(Schedulers.io())) { lib, _ ->
|
||||
@ -160,7 +184,7 @@ class LibraryPresenter(
|
||||
*
|
||||
* @param map the map to filter.
|
||||
*/
|
||||
private fun applyFilters(map: LibraryMap, trackMap: Map<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()
|
||||
@ -294,7 +318,11 @@ class LibraryPresenter(
|
||||
item.downloadCount = if (showDownloadBadges) {
|
||||
// SY -->
|
||||
if (item.manga.source == MERGED_SOURCE_ID) {
|
||||
item.manga.id?.let { mergeMangaId -> db.getMergedMangas(mergeMangaId).executeAsBlocking().map { downloadManager.getDownloadCount(it) }.sum() } ?: 0
|
||||
item.manga.id?.let { mergeMangaId ->
|
||||
runBlocking {
|
||||
getMergedMangaById.await(mergeMangaId)
|
||||
}.sumOf { downloadManager.getDownloadCount(it.toDbManga()) }
|
||||
} ?: 0
|
||||
} else /* SY <-- */ downloadManager.getDownloadCount(item.manga)
|
||||
} else {
|
||||
// Unset download count if not enabled
|
||||
@ -333,18 +361,30 @@ class LibraryPresenter(
|
||||
private fun applySort(categories: List<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++ }
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@ -509,7 +549,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()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -521,7 +561,40 @@ 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 {
|
||||
// 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 ->
|
||||
list.map { libraryManga ->
|
||||
// Display mode based on user preference: take it from global library setting or category
|
||||
@ -539,7 +612,7 @@ class LibraryPresenter(
|
||||
*
|
||||
* @return an observable of tracked manga.
|
||||
*/
|
||||
private fun getFilterObservable(): Observable<Map<Long, Map<Int, Boolean>>> {
|
||||
private fun getFilterObservable(): Observable<Map<Long, Map<Long, Boolean>>> {
|
||||
return getTracksObservable().combineLatest(filterTriggerRelay.observeOn(Schedulers.io())) { tracks, _ -> tracks }
|
||||
}
|
||||
|
||||
@ -548,16 +621,20 @@ class LibraryPresenter(
|
||||
*
|
||||
* @return an observable of tracked manga.
|
||||
*/
|
||||
private fun getTracksObservable(): Observable<Map<Long, Map<Int, Boolean>>> {
|
||||
return db.getTracks().asRxObservable().map { tracks ->
|
||||
tracks.groupBy { it.manga_id }
|
||||
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.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)
|
||||
Pair(it.syncId, trackManager.getService(it.syncId)?.isLogged.takeUnless { isLogged -> isLogged == true && it.syncId == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int.toLong() } ?: false)
|
||||
}
|
||||
}
|
||||
}.observeOn(Schedulers.io())
|
||||
}
|
||||
.observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -611,11 +688,11 @@ class LibraryPresenter(
|
||||
*
|
||||
* @param mangas the list of manga.
|
||||
*/
|
||||
fun getCommonCategories(mangas: List<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() }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -623,9 +700,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()
|
||||
}
|
||||
@ -640,25 +717,25 @@ class LibraryPresenter(
|
||||
launchIO {
|
||||
if (manga.source == MERGED_SOURCE_ID) {
|
||||
val mergedSource = sourceManager.get(MERGED_SOURCE_ID) as MergedSource
|
||||
val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking()
|
||||
val mergedMangas = getMergedMangaById.await(manga.id!!)
|
||||
mergedSource
|
||||
.getChaptersAsBlocking(manga.id!!)
|
||||
.filter { !it.read }
|
||||
.groupBy { it.manga_id!! }
|
||||
.forEach ab@{ (mangaId, chapters) ->
|
||||
val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@ab
|
||||
downloadManager.downloadChapters(mergedManga, chapters)
|
||||
downloadManager.downloadChapters(mergedManga.toDbManga(), chapters)
|
||||
}
|
||||
} else {
|
||||
/* SY --> */
|
||||
val chapters = if (manga.isEhBasedManga()) {
|
||||
db.getChapters(manga).executeOnIO().minByOrNull { it.source_order }?.let { chapter ->
|
||||
getChapterByMangaId.await(manga.id!!).minByOrNull { it.sourceOrder }?.let { chapter ->
|
||||
if (!chapter.read) listOf(chapter) else emptyList()
|
||||
} ?: emptyList()
|
||||
} else /* SY <-- */ db.getChapters(manga).executeAsBlocking()
|
||||
} else /* SY <-- */ getChapterByMangaId.await(manga.id!!)
|
||||
.filter { !it.read }
|
||||
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
downloadManager.downloadChapters(manga, chapters.map { it.toDbChapter() })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -711,21 +788,20 @@ class LibraryPresenter(
|
||||
fun markReadStatus(mangas: List<Manga>, read: Boolean) {
|
||||
mangas.forEach { manga ->
|
||||
launchIO {
|
||||
val chapters = if (manga.source == MERGED_SOURCE_ID) {
|
||||
(sourceManager.get(MERGED_SOURCE_ID) as MergedSource).getChaptersAsBlocking(manga.id!!)
|
||||
} else {
|
||||
db.getChapters(manga).executeAsBlocking()
|
||||
val chapters = if (manga.source == MERGED_SOURCE_ID) getMergedChaptersByMangaId.await(manga.id!!) else getChapterByMangaId.await(manga.id!!)
|
||||
|
||||
val toUpdate = chapters
|
||||
.map { chapter ->
|
||||
ChapterUpdate(
|
||||
read = read,
|
||||
lastPageRead = if (read) 0 else null,
|
||||
id = chapter.id,
|
||||
)
|
||||
}
|
||||
chapters.forEach {
|
||||
it.read = read
|
||||
if (!read) {
|
||||
it.last_page_read = 0
|
||||
}
|
||||
}
|
||||
db.updateChaptersProgress(chapters).executeAsBlocking()
|
||||
updateChapter.awaitAll(toUpdate)
|
||||
|
||||
if (read && preferences.removeAfterMarkedAsRead()) {
|
||||
deleteChapters(manga, chapters)
|
||||
deleteChapters(manga, chapters.map { it.toDbChapter() })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -735,12 +811,12 @@ class LibraryPresenter(
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
// SY -->
|
||||
if (source is MergedSource) {
|
||||
val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking()
|
||||
val mergedMangas = runBlocking { getMergedMangaById.await(manga.id!!) }
|
||||
val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) }
|
||||
chapters.groupBy { it.manga_id }.forEach { (mangaId, chapters) ->
|
||||
val mergedManga = mergedMangas.firstOrNull { it.id == mangaId } ?: return@forEach
|
||||
val mergedMangaSource = sources.firstOrNull { it.id == mergedManga.source } ?: return@forEach
|
||||
downloadManager.deleteChapters(chapters, mergedManga, mergedMangaSource)
|
||||
downloadManager.deleteChapters(chapters, mergedManga.toDbManga(), mergedMangaSource)
|
||||
}
|
||||
} else /* SY <-- */ downloadManager.deleteChapters(chapters, manga, source)
|
||||
}
|
||||
@ -749,20 +825,23 @@ class LibraryPresenter(
|
||||
/**
|
||||
* Remove the selected manga.
|
||||
*
|
||||
* @param mangas the list of manga to delete.
|
||||
* @param mangaList the list of manga to delete.
|
||||
* @param deleteFromLibrary whether to delete manga from library.
|
||||
* @param deleteChapters whether to delete downloaded chapters.
|
||||
*/
|
||||
fun removeMangas(mangas: List<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) {
|
||||
@ -770,11 +849,11 @@ class LibraryPresenter(
|
||||
val source = sourceManager.get(manga.source) as? HttpSource
|
||||
if (source != null) {
|
||||
if (source is MergedSource) {
|
||||
val mergedMangas = db.getMergedMangas(manga.id!!).executeAsBlocking()
|
||||
val mergedMangas = getMergedMangaById.await(manga.id!!)
|
||||
val sources = mergedMangas.distinctBy { it.source }.map { sourceManager.getOrStub(it.source) }
|
||||
mergedMangas.forEach merge@{ mergedManga ->
|
||||
val mergedSource = sources.firstOrNull { mergedManga.source == it.id } as? HttpSource ?: return@merge
|
||||
downloadManager.deleteManga(mergedManga, mergedSource)
|
||||
downloadManager.deleteManga(mergedManga.toDbManga(), mergedSource)
|
||||
}
|
||||
} else downloadManager.deleteManga(manga, source)
|
||||
}
|
||||
@ -784,36 +863,23 @@ class LibraryPresenter(
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given list of manga to categories.
|
||||
* Bulk update categories of manga using old and new common categories.
|
||||
*
|
||||
* @param categories the selected categories.
|
||||
* @param mangas the list of manga to move.
|
||||
*/
|
||||
fun moveMangasToCategories(categories: List<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SY -->
|
||||
@ -821,7 +887,7 @@ class LibraryPresenter(
|
||||
fun getFirstUnread(manga: Manga): Chapter? {
|
||||
val chapters = if (manga.source == MERGED_SOURCE_ID) {
|
||||
(sourceManager.get(MERGED_SOURCE_ID) as MergedSource).getChaptersAsBlocking(manga.id!!)
|
||||
} else db.getChapters(manga).executeAsBlocking()
|
||||
} else runBlocking { getChapterByMangaId.await(manga.id!!) }.map { it.toDbChapter() }
|
||||
return if (manga.isEhBasedManga()) {
|
||||
val chapter = chapters.sortedBy { it.source_order }.getOrNull(0)
|
||||
if (chapter?.read == false) chapter else null
|
||||
@ -867,10 +933,10 @@ class LibraryPresenter(
|
||||
|
||||
when (groupType) {
|
||||
LibraryGroup.BY_TRACK_STATUS -> {
|
||||
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
|
||||
val tracks = runBlocking { getTracks.await() }.groupBy { it.mangaId }
|
||||
libraryManga.forEach { libraryItem ->
|
||||
val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track ->
|
||||
TrackStatus.parseTrackerStatus(track.sync_id.toLong(), track.status)
|
||||
TrackStatus.parseTrackerStatus(track.syncId, track.status.toInt())
|
||||
} ?: TrackStatus.OTHER
|
||||
|
||||
map.getOrPut(status.int) { mutableListOf() } += libraryItem
|
||||
|
@ -4,8 +4,10 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.interactor.UpdateCategory
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
@ -13,9 +15,14 @@ import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
||||
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@ -23,6 +30,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
class LibrarySettingsSheet(
|
||||
router: Router,
|
||||
private val trackManager: TrackManager = Injekt.get(),
|
||||
private val updateCategory: UpdateCategory = Injekt.get(),
|
||||
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit,
|
||||
) : TabbedBottomSheetDialog(router.activity!!) {
|
||||
|
||||
@ -30,7 +38,8 @@ class LibrarySettingsSheet(
|
||||
private val sort: Sort
|
||||
private val display: Display
|
||||
private val grouping: Grouping
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
val sheetScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
init {
|
||||
filters = Filter(router.activity!!)
|
||||
@ -295,8 +304,14 @@ class LibrarySettingsSheet(
|
||||
|
||||
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0 /* SY --> */ && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT /* SY <-- */) {
|
||||
currentCategory?.sortDirection = flag.flag
|
||||
|
||||
db.insertCategory(currentCategory!!).executeAsBlocking()
|
||||
sheetScope.launchIO {
|
||||
updateCategory.await(
|
||||
CategoryUpdate(
|
||||
id = currentCategory!!.id?.toLong()!!,
|
||||
flags = currentCategory!!.flags.toLong(),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
preferences.librarySortingAscending().set(flag)
|
||||
}
|
||||
@ -321,8 +336,14 @@ class LibrarySettingsSheet(
|
||||
|
||||
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0 /* SY --> */ && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT /* SY <-- */) {
|
||||
currentCategory?.sortMode = flag.flag
|
||||
|
||||
db.insertCategory(currentCategory!!).executeAsBlocking()
|
||||
sheetScope.launchIO {
|
||||
updateCategory.await(
|
||||
CategoryUpdate(
|
||||
id = currentCategory!!.id?.toLong()!!,
|
||||
flags = currentCategory!!.flags.toLong(),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
preferences.librarySortingMode().set(flag)
|
||||
}
|
||||
@ -418,8 +439,14 @@ class LibrarySettingsSheet(
|
||||
|
||||
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0 /* SY --> */ && preferences.groupLibraryBy().get() == LibraryGroup.BY_DEFAULT /* SY <-- */) {
|
||||
currentCategory?.displayMode = flag.flag
|
||||
|
||||
db.insertCategory(currentCategory!!).executeAsBlocking()
|
||||
sheetScope.launchIO {
|
||||
updateCategory.await(
|
||||
CategoryUpdate(
|
||||
id = currentCategory!!.id?.toLong()!!,
|
||||
flags = currentCategory!!.flags.toLong(),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
preferences.libraryDisplayMode().set(flag)
|
||||
}
|
||||
@ -518,7 +545,9 @@ class LibrarySettingsSheet(
|
||||
inner class InternalGroup : Group {
|
||||
private val groupItems = mutableListOf<Item.DrawableSelection>()
|
||||
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 {
|
||||
val groupingItems = mutableListOf(
|
||||
|
@ -4,7 +4,7 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Immutable
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.interactor.MoveMangaToCategories
|
||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
||||
import eu.kanade.domain.chapter.interactor.GetMergedChapterByMangaId
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
|
||||
@ -135,7 +135,7 @@ class MangaPresenter(
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
private val deleteTrack: DeleteTrack = Injekt.get(),
|
||||
private val getTracks: GetTracks = Injekt.get(),
|
||||
private val moveMangaToCategories: MoveMangaToCategories = Injekt.get(),
|
||||
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||
private val insertTrack: InsertTrack = Injekt.get(),
|
||||
private val syncChaptersWithTrackServiceTwoWay: SyncChaptersWithTrackServiceTwoWay = Injekt.get(),
|
||||
) : BasePresenter<MangaController>() {
|
||||
@ -697,7 +697,7 @@ class MangaPresenter(
|
||||
val mangaId = manga.id ?: return
|
||||
val categoryIds = categories.mapNotNull { it.id?.toLong() }
|
||||
presenterScope.launchIO {
|
||||
moveMangaToCategories.await(mangaId, categoryIds)
|
||||
setMangaCategories.await(mangaId, categoryIds)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -94,6 +94,61 @@ AND C.date_upload > :after
|
||||
AND C.date_fetch > M.date_added
|
||||
ORDER BY C.date_upload DESC;
|
||||
|
||||
getLibrary:
|
||||
SELECT M.*, COALESCE(MC.category_id, 0) AS category
|
||||
FROM (
|
||||
SELECT mangas.*, COALESCE(C.unreadCount, 0) AS unread_count, COALESCE(R.readCount, 0) AS read_count
|
||||
FROM mangas
|
||||
LEFT JOIN (
|
||||
SELECT chapters.manga_id, COUNT(*) AS unreadCount
|
||||
FROM chapters
|
||||
WHERE chapters.read = 0
|
||||
GROUP BY chapters.manga_id
|
||||
) AS C
|
||||
ON mangas._id = C.manga_id
|
||||
LEFT JOIN (
|
||||
SELECT chapters.manga_id, COUNT(*) AS readCount
|
||||
FROM chapters
|
||||
WHERE chapters.read = 1
|
||||
GROUP BY chapters.manga_id
|
||||
) AS R
|
||||
WHERE mangas.favorite = 1
|
||||
GROUP BY mangas._id
|
||||
ORDER BY mangas.title
|
||||
) AS M
|
||||
LEFT JOIN (
|
||||
SELECT *
|
||||
FROM mangas_categories
|
||||
) AS MC
|
||||
ON M._id = MC.manga_id;
|
||||
|
||||
getLastRead:
|
||||
SELECT M.*, MAX(H.last_read) AS max
|
||||
FROM mangas M
|
||||
JOIN chapters C
|
||||
ON M._id = C.manga_id
|
||||
JOIN history H
|
||||
ON C._id = H.chapter_id
|
||||
WHERE M.favorite = 1
|
||||
GROUP BY M._id
|
||||
ORDER BY max ASC;
|
||||
|
||||
getLatestByChapterUploadDate:
|
||||
SELECT M.*, MAX(C.date_upload) AS max
|
||||
FROM mangas M
|
||||
JOIN chapters C
|
||||
ON M._id = C.manga_id
|
||||
GROUP BY M._id
|
||||
ORDER BY max ASC;
|
||||
|
||||
getLatestByChapterFetchDate:
|
||||
SELECT M.*, MAX(C.date_fetch) AS max
|
||||
FROM mangas M
|
||||
JOIN chapters C
|
||||
ON M._id = C.manga_id
|
||||
GROUP BY M._id
|
||||
ORDER BY max ASC;
|
||||
|
||||
deleteMangasNotInLibraryBySourceIds:
|
||||
DELETE FROM mangas
|
||||
WHERE favorite = 0 AND source IN :sourceIdsAND AND _id NOT IN (
|
||||
|
Loading…
x
Reference in New Issue
Block a user