Quick filter for suggestions

This commit is contained in:
Koitharu
2024-08-18 16:03:00 +03:00
parent 65abef1282
commit d06b396aec
8 changed files with 120 additions and 25 deletions

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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")
}
}

View File

@@ -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()

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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")
}