Quick filter request refactor
This commit is contained in:
@@ -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?
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user