mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-22 19:31:48 +01:00
Use SQLDelight on Library screen (#7432)
- Uses the new `asObservable` function to change the database calls to use SQLDelight, which should make the impact minimal when it comes to bugs. - Use interactors where they already exist - The todos are for the Compose rewrite - Removed unused StorIO methods/queries - Tested loading library, move manga to new category, unfavorite multiple manga, move multiple manga from one category to another, change filter, sort and display settings (with and without per category settings), (un)mark chapters, start/delete downloads Thank Syer for asObservable Co-authored-by: jobobby04 <17078382+jobobby04@users.noreply.github.com> Co-authored-by: jobobby04 <17078382+jobobby04@users.noreply.github.com>
This commit is contained in:
parent
ff32ab09fb
commit
05085fe57f
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -15,6 +15,18 @@ class CategoryRepositoryImpl(
|
|||||||
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)
|
||||||
@ -48,12 +60,6 @@ class CategoryRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getCategoriesForManga(mangaId: Long): List<Category> {
|
|
||||||
return handler.awaitList {
|
|
||||||
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun checkDuplicateName(name: String): Boolean {
|
override suspend fun checkDuplicateName(name: String): Boolean {
|
||||||
return handler
|
return handler
|
||||||
.awaitList { categoriesQueries.getCategories() }
|
.awaitList { categoriesQueries.getCategories() }
|
||||||
|
@ -46,7 +46,7 @@ class MangaRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun moveMangaToCategories(mangaId: Long, categoryIds: List<Long>) {
|
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
|
||||||
handler.await(inTransaction = true) {
|
handler.await(inTransaction = true) {
|
||||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
|
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
|
||||||
categoryIds.map { categoryId ->
|
categoryIds.map { categoryId ->
|
||||||
@ -57,31 +57,47 @@ 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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,
|
||||||
|
mangaId = value.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,13 @@ class TrackRepositoryImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun subscribeTracksByMangaId(mangaId: Long): Flow<List<Track>> {
|
override fun getTracksAsFlow(): Flow<List<Track>> {
|
||||||
|
return handler.subscribeToList {
|
||||||
|
manga_syncQueries.getTracks(trackMapper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
|
||||||
return handler.subscribeToList {
|
return handler.subscribeToList {
|
||||||
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
|
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.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()) }
|
||||||
|
@ -12,7 +12,11 @@ class GetCategories(
|
|||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
@ -8,6 +8,10 @@ interface CategoryRepository {
|
|||||||
|
|
||||||
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 +20,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,9 @@ interface TrackRepository {
|
|||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.data.database.queries.CategoryQueries
|
|||||||
import eu.kanade.tachiyomi.data.database.queries.ChapterQueries
|
import eu.kanade.tachiyomi.data.database.queries.ChapterQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
@ -26,7 +25,7 @@ import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
|||||||
class DatabaseHelper(
|
class DatabaseHelper(
|
||||||
openHelper: SupportSQLiteOpenHelper,
|
openHelper: SupportSQLiteOpenHelper,
|
||||||
) :
|
) :
|
||||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries {
|
MangaQueries, ChapterQueries, CategoryQueries, MangaCategoryQueries {
|
||||||
|
|
||||||
override val db = DefaultStorIOSQLite.builder()
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
.sqliteOpenHelper(openHelper)
|
.sqliteOpenHelper(openHelper)
|
||||||
|
@ -28,6 +28,4 @@ interface CategoryQueries : DbProvider {
|
|||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
|
||||||
}
|
}
|
||||||
|
@ -60,8 +60,6 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
|
||||||
|
|
||||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
|
||||||
|
|
||||||
fun updateChapterFlags(manga: Manga) = db.put()
|
fun updateChapterFlags(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
||||||
@ -76,34 +74,4 @@ interface MangaQueries : DbProvider {
|
|||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||||
.prepare()
|
.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()
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.database.queries
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||||
|
|
||||||
@ -38,39 +37,6 @@ val libraryQuery =
|
|||||||
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fun getLastReadMangaQuery() =
|
|
||||||
"""
|
|
||||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
|
||||||
FROM ${Manga.TABLE}
|
|
||||||
JOIN ${Chapter.TABLE}
|
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
|
||||||
JOIN ${History.TABLE}
|
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
|
||||||
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
|
|
||||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
|
||||||
ORDER BY max DESC
|
|
||||||
"""
|
|
||||||
|
|
||||||
fun getLatestChapterMangaQuery() =
|
|
||||||
"""
|
|
||||||
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max
|
|
||||||
FROM ${Manga.TABLE}
|
|
||||||
JOIN ${Chapter.TABLE}
|
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
|
||||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
|
||||||
ORDER by max DESC
|
|
||||||
"""
|
|
||||||
|
|
||||||
fun getChapterFetchDateMangaQuery() =
|
|
||||||
"""
|
|
||||||
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
|
|
||||||
FROM ${Manga.TABLE}
|
|
||||||
JOIN ${Chapter.TABLE}
|
|
||||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
|
||||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
|
||||||
ORDER by max DESC
|
|
||||||
"""
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query to get the categories for a manga.
|
* Query to get the categories for a manga.
|
||||||
*/
|
*/
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
|
||||||
|
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
|
||||||
|
|
||||||
interface TrackQueries : DbProvider {
|
|
||||||
|
|
||||||
fun getTracks() = db.get()
|
|
||||||
.listOfObjects(Track::class.java)
|
|
||||||
.withQuery(
|
|
||||||
Query.builder()
|
|
||||||
.table(TrackTable.TABLE)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
}
|
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
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.GetChapterByMangaId
|
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||||
@ -48,7 +48,7 @@ class SearchPresenter(
|
|||||||
private val getCategories: GetCategories = Injekt.get(),
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
private val getTracks: GetTracks = Injekt.get(),
|
private val getTracks: GetTracks = Injekt.get(),
|
||||||
private val insertTrack: InsertTrack = Injekt.get(),
|
private val insertTrack: InsertTrack = Injekt.get(),
|
||||||
private val moveMangaToCategories: MoveMangaToCategories = Injekt.get(),
|
private val setMangaCategories: SetMangaCategories = Injekt.get(),
|
||||||
) : GlobalSearchPresenter(initialQuery) {
|
) : GlobalSearchPresenter(initialQuery) {
|
||||||
|
|
||||||
private val replacingMangaRelay = BehaviorRelay.create<Pair<Boolean, Manga?>>()
|
private val replacingMangaRelay = BehaviorRelay.create<Pair<Boolean, Manga?>>()
|
||||||
@ -164,7 +164,7 @@ class SearchPresenter(
|
|||||||
// Update categories
|
// Update categories
|
||||||
if (migrateCategories) {
|
if (migrateCategories) {
|
||||||
val categoryIds = getCategories.await(prevDomainManga.id).map { it.id }
|
val categoryIds = getCategories.await(prevDomainManga.id).map { it.id }
|
||||||
moveMangaToCategories.await(domainManga.id, categoryIds)
|
setMangaCategories.await(domainManga.id, categoryIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update track
|
// Update track
|
||||||
|
@ -29,6 +29,8 @@ import eu.kanade.tachiyomi.ui.base.controller.pushController
|
|||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
|
import eu.kanade.tachiyomi.ui.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.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
|
||||||
@ -36,6 +38,7 @@ import eu.kanade.tachiyomi.util.system.toast
|
|||||||
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
|
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
|
||||||
import eu.kanade.tachiyomi.widget.EmptyView
|
import eu.kanade.tachiyomi.widget.EmptyView
|
||||||
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
|
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -226,6 +229,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
|
||||||
@ -541,25 +545,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() {
|
||||||
@ -579,7 +587,7 @@ class LibraryController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,12 +2,23 @@ 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.DatabaseHandler
|
||||||
|
import eu.kanade.domain.category.interactor.GetCategories
|
||||||
|
import eu.kanade.domain.category.interactor.SetMangaCategories
|
||||||
|
import eu.kanade.domain.category.model.toDbCategory
|
||||||
|
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||||
|
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||||
|
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||||
|
import eu.kanade.domain.chapter.model.toDbChapter
|
||||||
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
|
import eu.kanade.domain.manga.model.MangaUpdate
|
||||||
|
import eu.kanade.domain.track.interactor.GetTracks
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.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.LibraryManga
|
||||||
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.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
@ -23,6 +34,8 @@ import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
|||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.util.removeCovers
|
import eu.kanade.tachiyomi.util.removeCovers
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
|
||||||
|
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
|
||||||
@ -47,7 +60,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(),
|
||||||
@ -92,6 +111,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, _ ->
|
||||||
@ -115,7 +135,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()
|
||||||
@ -252,18 +272,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++ }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sortingModes = categories.associate { category ->
|
val sortingModes = categories.associate { category ->
|
||||||
@ -366,7 +398,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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -378,7 +410,36 @@ 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 {
|
||||||
|
mangasQueries.getLibrary { _id: Long, source: Long, url: String, artist: String?, author: String?, description: String?, genre: List<String>?, title: String, status: Long, thumbnail_url: String?, favorite: Boolean, last_update: Long?, next_update: Long?, initialized: Boolean, viewer: Long, chapter_flags: Long, cover_last_modified: Long, date_added: Long, unread_count: Long, read_count: Long, category: Long ->
|
||||||
|
LibraryManga().apply {
|
||||||
|
this.id = _id
|
||||||
|
this.source = source
|
||||||
|
this.url = url
|
||||||
|
this.artist = artist
|
||||||
|
this.author = author
|
||||||
|
this.description = description
|
||||||
|
this.genre = genre?.joinToString()
|
||||||
|
this.title = title
|
||||||
|
this.status = status.toInt()
|
||||||
|
this.thumbnail_url = thumbnail_url
|
||||||
|
this.favorite = favorite
|
||||||
|
this.last_update = last_update ?: 0
|
||||||
|
this.initialized = initialized
|
||||||
|
this.viewer_flags = viewer.toInt()
|
||||||
|
this.chapter_flags = chapter_flags.toInt()
|
||||||
|
this.cover_last_modified = cover_last_modified
|
||||||
|
this.date_added = date_added
|
||||||
|
this.unreadCount = unread_count.toInt()
|
||||||
|
this.readCount = read_count.toInt()
|
||||||
|
this.category = category.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.asObservable()
|
||||||
.map { list ->
|
.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
|
||||||
@ -396,7 +457,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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,16 +466,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 ?: 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 ?: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -451,11 +516,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() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -463,9 +528,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()
|
||||||
}
|
}
|
||||||
@ -478,8 +543,9 @@ class LibraryPresenter(
|
|||||||
fun downloadUnreadChapters(mangas: List<Manga>) {
|
fun downloadUnreadChapters(mangas: List<Manga>) {
|
||||||
mangas.forEach { manga ->
|
mangas.forEach { manga ->
|
||||||
launchIO {
|
launchIO {
|
||||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
val chapters = getChapterByMangaId.await(manga.id!!)
|
||||||
.filter { !it.read }
|
.filter { !it.read }
|
||||||
|
.map { it.toDbChapter() }
|
||||||
|
|
||||||
downloadManager.downloadChapters(manga, chapters)
|
downloadManager.downloadChapters(manga, chapters)
|
||||||
}
|
}
|
||||||
@ -494,17 +560,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 = db.getChapters(manga).executeAsBlocking()
|
val chapters = getChapterByMangaId.await(manga.id!!)
|
||||||
chapters.forEach {
|
|
||||||
it.read = read
|
val toUpdate = chapters
|
||||||
if (!read) {
|
.map { chapter ->
|
||||||
it.last_page_read = 0
|
ChapterUpdate(
|
||||||
|
read = read,
|
||||||
|
lastPageRead = if (read) 0 else null,
|
||||||
|
id = chapter.id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
updateChapter.awaitAll(toUpdate)
|
||||||
db.updateChaptersProgress(chapters).executeAsBlocking()
|
|
||||||
|
|
||||||
if (read && preferences.removeAfterMarkedAsRead()) {
|
if (read && preferences.removeAfterMarkedAsRead()) {
|
||||||
deleteChapters(manga, chapters)
|
deleteChapters(manga, chapters.map { it.toDbChapter() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -519,20 +588,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) {
|
||||||
@ -547,35 +619,22 @@ class LibraryPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the given list of manga to categories.
|
* Bulk update categories of manga using old and new common categories.
|
||||||
*
|
*
|
||||||
* @param categories the selected categories.
|
* @param 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,9 @@ 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.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 +14,13 @@ 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 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,13 +28,15 @@ 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!!) {
|
||||||
|
|
||||||
val filters: Filter
|
val filters: Filter
|
||||||
private val sort: Sort
|
private val sort: Sort
|
||||||
private val display: Display
|
private val display: Display
|
||||||
private val db: DatabaseHelper by injectLazy()
|
|
||||||
|
val sheetScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
filters = Filter(router.activity!!)
|
filters = Filter(router.activity!!)
|
||||||
@ -250,8 +257,14 @@ class LibrarySettingsSheet(
|
|||||||
|
|
||||||
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
|
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -272,8 +285,14 @@ class LibrarySettingsSheet(
|
|||||||
|
|
||||||
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
|
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -361,8 +380,14 @@ class LibrarySettingsSheet(
|
|||||||
|
|
||||||
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
|
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.manga
|
|||||||
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.SyncChaptersWithSource
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
|
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
|
||||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||||
@ -90,7 +90,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>() {
|
||||||
@ -358,7 +358,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -86,6 +86,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 :sourceIds;
|
WHERE favorite = 0 AND source IN :sourceIds;
|
||||||
|
Loading…
Reference in New Issue
Block a user