Quick filter request refactor

This commit is contained in:
Koitharu
2024-08-18 16:41:31 +03:00
parent d06b396aec
commit 9e49b28ac3
7 changed files with 207 additions and 203 deletions

View File

@@ -0,0 +1,109 @@
package org.koitharu.kotatsu.core.db
import androidx.sqlite.db.SimpleSQLiteQuery
import org.koitharu.kotatsu.list.domain.ListFilterOption
import java.util.LinkedList
class MangaQueryBuilder(
private val table: String,
private val conditionCallback: ConditionCallback
) {
private var filterOptions: Collection<ListFilterOption> = emptyList()
private var whereConditions = LinkedList<String>()
private var orderBy: String? = null
private var groupBy: String? = null
private var extraJoins: String? = null
private var limit: Int = 0
fun filters(options: Collection<ListFilterOption>) = apply {
filterOptions = options
}
fun where(condition: String) = apply {
whereConditions.add(condition)
}
fun orderBy(orderBy: String?) = apply {
this@MangaQueryBuilder.orderBy = orderBy
}
fun groupBy(groupBy: String?) = apply {
this@MangaQueryBuilder.groupBy = groupBy
}
fun limit(limit: Int) = apply {
this@MangaQueryBuilder.limit = limit
}
fun join(join: String?) = apply {
extraJoins = join
}
fun build() = buildString {
append("SELECT * FROM ")
append(table)
extraJoins?.let {
append(' ')
append(it)
}
if (whereConditions.isNotEmpty()) {
whereConditions.joinTo(
buffer = this,
prefix = " WHERE ",
separator = " AND ",
)
}
if (filterOptions.isNotEmpty()) {
if (whereConditions.isEmpty()) {
append(" WHERE")
}
var isFirst = true
val groupedOptions = filterOptions.groupBy { it.groupKey }
for ((_, group) in groupedOptions) {
if (group.isEmpty()) {
continue
}
if (isFirst) {
isFirst = false
append(' ')
} else {
append(" AND ")
}
if (group.size > 1) {
group.joinTo(
buffer = this,
separator = " OR ",
prefix = "(",
postfix = ")",
transform = ::getConditionOrThrow,
)
} else {
append(getConditionOrThrow(group.single()))
}
}
}
groupBy?.let {
append(" GROUP BY ")
append(it)
}
orderBy?.let {
append(" ORDER BY ")
append(it)
}
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}.let { SimpleSQLiteQuery(it) }
private fun getConditionOrThrow(option: ListFilterOption): String =
requireNotNull(conditionCallback.getCondition(option)) {
"Unsupported filter option $option"
}
fun interface ConditionCallback {
fun getCondition(option: ListFilterOption): String?
}
}

View File

@@ -6,51 +6,26 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.RawQuery import androidx.room.RawQuery
import androidx.room.Transaction import androidx.room.Transaction
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.MangaQueryBuilder
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TrackLogWithManga import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
@Dao @Dao
abstract class TrackLogsDao { abstract class TrackLogsDao : MangaQueryBuilder.ConditionCallback {
fun observeAll(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<TrackLogWithManga>> { fun observeAll(
val query = buildString { limit: Int,
append("SELECT * FROM track_logs") filterOptions: Set<ListFilterOption>,
if (filterOptions.isNotEmpty()) { ): Flow<List<TrackLogWithManga>> = observeAllImpl(
append(" WHERE") MangaQueryBuilder("track_logs", this)
var isFirst = true .filters(filterOptions)
val groupedOptions = filterOptions.groupBy { it.groupKey } .limit(limit)
for ((_, group) in groupedOptions) { .orderBy("created_at DESC")
if (group.isEmpty()) { .build(),
continue )
}
if (isFirst) {
isFirst = false
append(' ')
} else {
append(" AND ")
}
if (group.size > 1) {
group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") {
it.getCondition()
}
} else {
append(group.single().getCondition())
}
}
}
append(" ORDER BY created_at DESC")
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
}
@Query("SELECT COUNT(*) FROM track_logs WHERE unread = 1") @Query("SELECT COUNT(*) FROM track_logs WHERE unread = 1")
abstract fun observeUnreadCount(): Flow<Int> abstract fun observeUnreadCount(): Flow<Int>
@@ -77,10 +52,10 @@ abstract class TrackLogsDao {
@RawQuery(observedEntities = [TrackLogEntity::class]) @RawQuery(observedEntities = [TrackLogEntity::class])
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<TrackLogWithManga>> protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<TrackLogWithManga>>
private fun ListFilterOption.getCondition(): String = when (this) { override fun getCondition(option: ListFilterOption): String? = when (option) {
ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = track_logs.manga_id)" ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = track_logs.manga_id)"
is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = track_logs.manga_id AND favourites.category_id = ${category.id})" is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = track_logs.manga_id AND favourites.category_id = ${option.category.id})"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = track_logs.manga_id AND tag_id = ${tag.toEntity().id})" is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = track_logs.manga_id AND tag_id = ${option.tagId})"
else -> throw IllegalArgumentException("Unsupported option $this") else -> null
} }
} }

View File

@@ -11,14 +11,15 @@ import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.MangaQueryBuilder
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import org.koitharu.kotatsu.favourites.domain.model.Cover import org.koitharu.kotatsu.favourites.domain.model.Cover
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED
@Dao @Dao
abstract class FavouritesDao { abstract class FavouritesDao : MangaQueryBuilder.ConditionCallback {
/** SELECT **/ /** SELECT **/
@@ -55,41 +56,17 @@ abstract class FavouritesDao {
order: ListSortOrder, order: ListSortOrder,
filterOptions: Set<ListFilterOption>, filterOptions: Set<ListFilterOption>,
limit: Int limit: Int
): Flow<List<FavouriteManga>> { ): Flow<List<FavouriteManga>> = observeAllImpl(
val orderBy = getOrderBy(order) MangaQueryBuilder(TABLE_FAVOURITES, this)
val query = buildString { .join("LEFT JOIN manga ON favourites.manga_id = manga.manga_id")
append( .where("deleted_at = 0")
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + .run { if (categoryId != 0L) where("category_id = $categoryId") else this }
"WHERE deleted_at = 0", .filters(filterOptions)
) .groupBy("favourites.manga_id")
if (categoryId != 0L) { .orderBy(getOrderBy(order))
append(" AND category_id = ") .limit(limit)
append(categoryId) .build(),
} )
val groupedOptions = filterOptions.groupBy { it.groupKey }
for ((_, group) in groupedOptions) {
if (group.isEmpty()) {
continue
}
append(" AND ")
if (group.size > 1) {
group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") {
it.getCondition()
}
} else {
append(group.single().getCondition())
}
}
append(" GROUP BY favourites.manga_id ORDER BY ")
append(orderBy)
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
}
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> { suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {
val orderBy = getOrderBy(order) val orderBy = getOrderBy(order)
@@ -213,13 +190,13 @@ abstract class FavouritesDao {
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported") else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
} }
private fun ListFilterOption.getCondition(): String = when (this) { override fun getCondition(option: ListFilterOption): String? = when (option) {
ListFilterOption.Macro.COMPLETED -> "EXISTS(SELECT * FROM history WHERE history.manga_id = favourites.manga_id AND history.percent >= $PROGRESS_COMPLETED)" ListFilterOption.Macro.COMPLETED -> "EXISTS(SELECT * FROM history WHERE history.manga_id = favourites.manga_id AND history.percent >= $PROGRESS_COMPLETED)"
ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0" ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1" ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${tag.toEntity().id})" is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
ListFilterOption.Downloaded, ListFilterOption.Downloaded,
is ListFilterOption.Favorite, is ListFilterOption.Favorite,
ListFilterOption.Macro.FAVORITE -> throw IllegalArgumentException("Unsupported option $this") ListFilterOption.Macro.FAVORITE -> null
} }
} }

View File

@@ -6,17 +6,17 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.RawQuery import androidx.room.RawQuery
import androidx.room.Transaction import androidx.room.Transaction
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.MangaQueryBuilder
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED import org.koitharu.kotatsu.list.domain.ReadingProgress.Companion.PROGRESS_COMPLETED
@Dao @Dao
abstract class HistoryDao { abstract class HistoryDao : MangaQueryBuilder.ConditionCallback {
@Transaction @Transaction
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit OFFSET :offset") @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit OFFSET :offset")
@@ -34,48 +34,30 @@ abstract class HistoryDao {
order: ListSortOrder, order: ListSortOrder,
filterOptions: Set<ListFilterOption>, filterOptions: Set<ListFilterOption>,
limit: Int limit: Int
): Flow<List<HistoryWithManga>> { ): Flow<List<HistoryWithManga>> = observeAllImpl(
val orderBy = when (order) { MangaQueryBuilder(TABLE_HISTORY, this)
ListSortOrder.LAST_READ -> "history.updated_at DESC" .join("LEFT JOIN manga ON history.manga_id = manga.manga_id")
ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC" .where("history.deleted_at = 0")
ListSortOrder.NEWEST -> "history.created_at DESC" .filters(filterOptions)
ListSortOrder.OLDEST -> "history.created_at ASC" .orderBy(
ListSortOrder.PROGRESS -> "history.percent DESC" orderBy = when (order) {
ListSortOrder.UNREAD -> "history.percent ASC" ListSortOrder.LAST_READ -> "history.updated_at DESC"
ListSortOrder.ALPHABETIC -> "manga.title" ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC"
ListSortOrder.ALPHABETIC_REVERSE -> "manga.title DESC" ListSortOrder.NEWEST -> "history.created_at DESC"
ListSortOrder.NEW_CHAPTERS -> "IFNULL((SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC" ListSortOrder.OLDEST -> "history.created_at ASC"
ListSortOrder.UPDATED -> "IFNULL((SELECT last_chapter_date FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC" ListSortOrder.PROGRESS -> "history.percent DESC"
else -> throw IllegalArgumentException("Sort order $order is not supported") ListSortOrder.UNREAD -> "history.percent ASC"
} ListSortOrder.ALPHABETIC -> "manga.title"
val query = buildString { ListSortOrder.ALPHABETIC_REVERSE -> "manga.title DESC"
append( ListSortOrder.NEW_CHAPTERS -> "IFNULL((SELECT chapters_new FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC"
"SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " + ListSortOrder.UPDATED -> "IFNULL((SELECT last_chapter_date FROM tracks WHERE tracks.manga_id = manga.manga_id), 0) DESC"
"WHERE history.deleted_at = 0", else -> throw IllegalArgumentException("Sort order $order is not supported")
},
) )
val groupedOptions = filterOptions.groupBy { it.groupKey } .groupBy("history.manga_id")
for ((_, group) in groupedOptions) { .limit(limit)
if (group.isEmpty()) { .build(),
continue )
}
append(" AND ")
if (group.size > 1) {
group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") {
it.getCondition()
}
} else {
append(group.single().getCondition())
}
}
append(" GROUP BY history.manga_id ORDER BY ")
append(orderBy)
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
}
@Query("SELECT manga_id FROM history WHERE deleted_at = 0") @Query("SELECT manga_id FROM history WHERE deleted_at = 0")
abstract suspend fun findAllIds(): LongArray abstract suspend fun findAllIds(): LongArray
@@ -170,13 +152,13 @@ abstract class HistoryDao {
@RawQuery(observedEntities = [HistoryEntity::class]) @RawQuery(observedEntities = [HistoryEntity::class])
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<HistoryWithManga>> protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<HistoryWithManga>>
private fun ListFilterOption.getCondition(): String = when (this) { override fun getCondition(option: ListFilterOption): String? = when (option) {
ListFilterOption.Downloaded -> throw IllegalArgumentException("Unsupported option $this") ListFilterOption.Downloaded -> null
is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id AND category_id = ${category.id})" is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id AND category_id = ${option.category.id})"
ListFilterOption.Macro.COMPLETED -> "percent >= $PROGRESS_COMPLETED" ListFilterOption.Macro.COMPLETED -> "percent >= $PROGRESS_COMPLETED"
ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = history.manga_id) > 0" ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = history.manga_id) > 0"
ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)" ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)"
ListFilterOption.Macro.NSFW -> "manga.nsfw = 1" ListFilterOption.Macro.NSFW -> "manga.nsfw = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${tag.toEntity().id})" is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE history.manga_id = manga_tags.manga_id AND tag_id = ${option.tagId})"
} }
} }

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.list.domain
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -55,6 +56,8 @@ sealed interface ListFilterOption {
val tag: MangaTag val tag: MangaTag
) : ListFilterOption { ) : ListFilterOption {
val tagId: Long = tag.toEntity().id
override val titleResId: Int override val titleResId: Int
get() = 0 get() = 0

View File

@@ -7,54 +7,29 @@ import androidx.room.Query
import androidx.room.RawQuery import androidx.room.RawQuery
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.MangaQueryBuilder
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.db.entity.toEntity
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
@Dao @Dao
abstract class SuggestionDao { abstract class SuggestionDao : MangaQueryBuilder.ConditionCallback {
@Transaction @Transaction
@Query("SELECT * FROM suggestions ORDER BY relevance DESC") @Query("SELECT * FROM suggestions ORDER BY relevance DESC")
abstract fun observeAll(): Flow<List<SuggestionWithManga>> abstract fun observeAll(): Flow<List<SuggestionWithManga>>
fun observeAll(limit: Int, filterOptions: Collection<ListFilterOption>): Flow<List<SuggestionWithManga>> { fun observeAll(
val query = buildString { limit: Int,
append("SELECT * FROM suggestions") filterOptions: Collection<ListFilterOption>
if (filterOptions.isNotEmpty()) { ): Flow<List<SuggestionWithManga>> = observeAllImpl(
append(" WHERE") MangaQueryBuilder("suggestions", this)
var isFirst = true .filters(filterOptions)
val groupedOptions = filterOptions.groupBy { it.groupKey } .orderBy("relevance DESC")
for ((_, group) in groupedOptions) { .limit(limit)
if (group.isEmpty()) { .build(),
continue )
}
if (isFirst) {
isFirst = false
append(' ')
} else {
append(" AND ")
}
if (group.size > 1) {
group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") {
it.getCondition()
}
} else {
append(group.single().getCondition())
}
}
}
append(" ORDER BY relevance DESC")
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeAllImpl(SimpleSQLiteQuery(query))
}
@Transaction @Transaction
@Query("SELECT * FROM suggestions ORDER BY RANDOM() LIMIT 1") @Query("SELECT * FROM suggestions ORDER BY RANDOM() LIMIT 1")
@@ -93,9 +68,9 @@ abstract class SuggestionDao {
@RawQuery(observedEntities = [SuggestionEntity::class]) @RawQuery(observedEntities = [SuggestionEntity::class])
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<SuggestionWithManga>> protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<SuggestionWithManga>>
private fun ListFilterOption.getCondition(): String = when (this) { override fun getCondition(option: ListFilterOption): String? = when (option) {
ListFilterOption.Macro.NSFW -> "(SELECT nsfw FROM manga WHERE manga.manga_id = suggestions.manga_id) = 1" ListFilterOption.Macro.NSFW -> "(SELECT nsfw FROM manga WHERE manga.manga_id = suggestions.manga_id) = 1"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = suggestions.manga_id AND tag_id = ${tag.toEntity().id})" is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = suggestions.manga_id AND tag_id = ${option.tagId})"
else -> throw IllegalArgumentException("Unsupported option $this") else -> null
} }
} }

View File

@@ -5,14 +5,13 @@ import androidx.room.Query
import androidx.room.RawQuery import androidx.room.RawQuery
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Upsert import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.toEntity import org.koitharu.kotatsu.core.db.MangaQueryBuilder
import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.ListFilterOption
@Dao @Dao
abstract class TracksDao { abstract class TracksDao : MangaQueryBuilder.ConditionCallback {
@Transaction @Transaction
@Query("SELECT * FROM tracks ORDER BY last_check_time ASC LIMIT :limit OFFSET :offset") @Query("SELECT * FROM tracks ORDER BY last_check_time ASC LIMIT :limit OFFSET :offset")
@@ -44,33 +43,17 @@ abstract class TracksDao {
@Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC") @Query("SELECT * FROM tracks WHERE chapters_new > 0 ORDER BY last_chapter_date DESC")
abstract fun observeUpdatedManga(): Flow<List<MangaWithTrack>> abstract fun observeUpdatedManga(): Flow<List<MangaWithTrack>>
fun observeUpdatedManga(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<MangaWithTrack>> { fun observeUpdatedManga(
val query = buildString { limit: Int,
append("SELECT * FROM tracks WHERE chapters_new > 0") filterOptions: Set<ListFilterOption>,
if (filterOptions.isNotEmpty()) { ): Flow<List<MangaWithTrack>> = observeMangaImpl(
val groupedOptions = filterOptions.groupBy { it.groupKey } MangaQueryBuilder("tracks", this)
for ((_, group) in groupedOptions) { .where("chapters_new > 0")
if (group.isEmpty()) { .filters(filterOptions)
continue .limit(limit)
} .orderBy("last_chapter_date DESC")
append(" AND ") .build(),
if (group.size > 1) { )
group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") {
it.getCondition()
}
} else {
append(group.single().getCondition())
}
}
}
append(" ORDER BY last_chapter_date DESC")
if (limit > 0) {
append(" LIMIT ")
append(limit)
}
}
return observeMangaImpl(SimpleSQLiteQuery(query))
}
@Query("DELETE FROM tracks") @Query("DELETE FROM tracks")
abstract suspend fun clear() abstract suspend fun clear()
@@ -94,10 +77,10 @@ abstract class TracksDao {
@RawQuery(observedEntities = [TrackEntity::class]) @RawQuery(observedEntities = [TrackEntity::class])
protected abstract fun observeMangaImpl(query: SupportSQLiteQuery): Flow<List<MangaWithTrack>> protected abstract fun observeMangaImpl(query: SupportSQLiteQuery): Flow<List<MangaWithTrack>>
private fun ListFilterOption.getCondition(): String = when (this) { override fun getCondition(option: ListFilterOption): String? = when (option) {
ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = tracks.manga_id)" ListFilterOption.Macro.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = tracks.manga_id)"
is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = tracks.manga_id AND favourites.category_id = ${category.id})" is ListFilterOption.Favorite -> "EXISTS(SELECT * FROM favourites WHERE favourites.manga_id = tracks.manga_id AND favourites.category_id = ${option.category.id})"
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = tracks.manga_id AND tag_id = ${tag.toEntity().id})" is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = tracks.manga_id AND tag_id = ${option.tagId})"
else -> throw IllegalArgumentException("Unsupported option $this") else -> null
} }
} }