Quick filter for suggestions
This commit is contained in:
@@ -80,9 +80,6 @@ abstract class TrackLogsDao {
|
||||
private fun ListFilterOption.getCondition(): String = when (this) {
|
||||
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})"
|
||||
ListFilterOption.Macro.COMPLETED -> TODO()
|
||||
ListFilterOption.Macro.NEW_CHAPTERS -> TODO()
|
||||
ListFilterOption.Macro.NSFW -> TODO()
|
||||
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = track_logs.manga_id AND tag_id = ${tag.toEntity().id})"
|
||||
else -> throw IllegalArgumentException("Unsupported option $this")
|
||||
}
|
||||
|
||||
@@ -24,7 +24,9 @@ class HistoryListQuickFilter @Inject constructor(
|
||||
}
|
||||
add(ListFilterOption.Macro.COMPLETED)
|
||||
add(ListFilterOption.Macro.FAVORITE)
|
||||
add(ListFilterOption.Macro.NSFW)
|
||||
if (!settings.isNsfwContentDisabled && !settings.isHistoryExcludeNsfw) {
|
||||
add(ListFilterOption.Macro.NSFW)
|
||||
}
|
||||
repository.getPopularTags(3).mapTo(this) {
|
||||
ListFilterOption.Tag(it)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,15 @@ import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.RawQuery
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
|
||||
@Dao
|
||||
abstract class SuggestionDao {
|
||||
@@ -15,9 +21,40 @@ abstract class SuggestionDao {
|
||||
@Query("SELECT * FROM suggestions ORDER BY relevance DESC")
|
||||
abstract fun observeAll(): Flow<List<SuggestionWithManga>>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM suggestions ORDER BY relevance DESC LIMIT :limit")
|
||||
abstract fun observeAll(limit: Int): Flow<List<SuggestionWithManga>>
|
||||
fun observeAll(limit: Int, filterOptions: Collection<ListFilterOption>): Flow<List<SuggestionWithManga>> {
|
||||
val query = buildString {
|
||||
append("SELECT * FROM suggestions")
|
||||
if (filterOptions.isNotEmpty()) {
|
||||
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(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
|
||||
@Query("SELECT * FROM suggestions ORDER BY RANDOM() LIMIT 1")
|
||||
@@ -33,6 +70,9 @@ abstract class SuggestionDao {
|
||||
@Query("SELECT manga.title FROM suggestions LEFT JOIN manga ON suggestions.manga_id = manga.manga_id WHERE manga.title LIKE :query")
|
||||
abstract suspend fun getTitles(query: String): List<String>
|
||||
|
||||
@Query("SELECT tags.* FROM suggestions LEFT JOIN tags ON (tag_id IN (SELECT tag_id FROM manga_tags WHERE manga_tags.manga_id = suggestions.manga_id)) GROUP BY tag_id ORDER BY COUNT(tags.tag_id) DESC LIMIT :limit")
|
||||
abstract suspend fun getTopTags(limit: Int): List<TagEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(entity: SuggestionEntity): Long
|
||||
|
||||
@@ -48,4 +88,14 @@ abstract class SuggestionDao {
|
||||
insert(entity)
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
@RawQuery(observedEntities = [SuggestionEntity::class])
|
||||
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<SuggestionWithManga>>
|
||||
|
||||
private fun ListFilterOption.getCondition(): String = when (this) {
|
||||
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})"
|
||||
else -> throw IllegalArgumentException("Unsupported option $this")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ import org.koitharu.kotatsu.core.db.entity.toEntities
|
||||
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.toManga
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTags
|
||||
import org.koitharu.kotatsu.core.db.entity.toMangaTagsList
|
||||
import org.koitharu.kotatsu.core.util.ext.mapItems
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -22,8 +25,8 @@ class SuggestionRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun observeAll(limit: Int): Flow<List<Manga>> {
|
||||
return db.getSuggestionDao().observeAll(limit).mapItems {
|
||||
fun observeAll(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<Manga>> {
|
||||
return db.getSuggestionDao().observeAll(limit, filterOptions).mapItems {
|
||||
it.manga.toManga(it.tags.toMangaTags())
|
||||
}
|
||||
}
|
||||
@@ -48,6 +51,11 @@ class SuggestionRepository @Inject constructor(
|
||||
return db.getSuggestionDao().count() == 0
|
||||
}
|
||||
|
||||
suspend fun getTopTags(limit: Int): List<MangaTag> {
|
||||
return db.getSuggestionDao().getTopTags(limit)
|
||||
.toMangaTagsList()
|
||||
}
|
||||
|
||||
suspend fun replace(suggestions: Iterable<MangaSuggestion>) {
|
||||
db.withTransaction {
|
||||
db.getSuggestionDao().deleteAll()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.koitharu.kotatsu.suggestions.domain
|
||||
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||
import org.koitharu.kotatsu.list.domain.MangaListQuickFilter
|
||||
import javax.inject.Inject
|
||||
|
||||
class SuggestionsListQuickFilter @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val suggestionRepository: SuggestionRepository,
|
||||
) : MangaListQuickFilter(settings) {
|
||||
|
||||
override suspend fun getAvailableFilterOptions(): List<ListFilterOption> = buildList(6) {
|
||||
suggestionRepository.getTopTags(5).mapTo(this) {
|
||||
ListFilterOption.Tag(it)
|
||||
}
|
||||
if (!settings.isNsfwContentDisabled && !settings.isSuggestionsExcludeNsfw) {
|
||||
add(ListFilterOption.Macro.NSFW)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class SuggestionsFragment : MangaListFragment() {
|
||||
viewModel.updateSuggestions()
|
||||
Snackbar.make(
|
||||
requireViewBinding().recyclerView,
|
||||
R.string.feed_will_update_soon,
|
||||
R.string.suggestions_updating,
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
true
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
@@ -15,11 +16,13 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
||||
import org.koitharu.kotatsu.list.domain.QuickFilterListener
|
||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionsListQuickFilter
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -28,27 +31,44 @@ class SuggestionsViewModel @Inject constructor(
|
||||
settings: AppSettings,
|
||||
private val mangaListMapper: MangaListMapper,
|
||||
downloadScheduler: DownloadWorker.Scheduler,
|
||||
private val quickFilter: SuggestionsListQuickFilter,
|
||||
private val suggestionsScheduler: SuggestionsWorker.Scheduler,
|
||||
) : MangaListViewModel(settings, downloadScheduler) {
|
||||
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
||||
|
||||
override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_SUGGESTIONS) { suggestionsListMode }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.suggestionsListMode)
|
||||
|
||||
override val content = combine(
|
||||
repository.observeAll(),
|
||||
quickFilter.appliedOptions.flatMapLatest { repository.observeAll(0, it) },
|
||||
quickFilter.appliedOptions,
|
||||
observeListModeWithTriggers(),
|
||||
) { list, mode ->
|
||||
) { list, filters, mode ->
|
||||
when {
|
||||
list.isEmpty() -> listOf(
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_common,
|
||||
textPrimary = R.string.nothing_found,
|
||||
textSecondary = R.string.text_suggestion_holder,
|
||||
actionStringRes = 0,
|
||||
),
|
||||
)
|
||||
list.isEmpty() -> if (filters.isEmpty()) {
|
||||
listOf(
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_common,
|
||||
textPrimary = R.string.nothing_found,
|
||||
textSecondary = R.string.text_suggestion_holder,
|
||||
actionStringRes = 0,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
listOfNotNull(
|
||||
quickFilter.filterItem(filters),
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_common,
|
||||
textPrimary = R.string.nothing_found,
|
||||
textSecondary = R.string.text_empty_holder_secondary_filtered,
|
||||
actionStringRes = 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
else -> mangaListMapper.toListModelList(list, mode)
|
||||
else -> buildList(list.size + 1) {
|
||||
quickFilter.filterItem(filters)?.let(::add)
|
||||
mangaListMapper.toListModelList(this, list, mode)
|
||||
}
|
||||
}
|
||||
}.onStart {
|
||||
loadingCounter.increment()
|
||||
|
||||
@@ -97,9 +97,6 @@ abstract class TracksDao {
|
||||
private fun ListFilterOption.getCondition(): String = when (this) {
|
||||
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})"
|
||||
ListFilterOption.Macro.COMPLETED -> TODO()
|
||||
ListFilterOption.Macro.NEW_CHAPTERS -> TODO()
|
||||
ListFilterOption.Macro.NSFW -> TODO()
|
||||
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE manga_tags.manga_id = tracks.manga_id AND tag_id = ${tag.toEntity().id})"
|
||||
else -> throw IllegalArgumentException("Unsupported option $this")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user