Quick filter for suggestions

master
Koitharu 2 years ago
parent 65abef1282
commit d06b396aec
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -80,9 +80,6 @@ abstract class TrackLogsDao {
private fun ListFilterOption.getCondition(): String = when (this) { private fun ListFilterOption.getCondition(): String = when (this) {
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 = ${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})" 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") else -> throw IllegalArgumentException("Unsupported option $this")
} }

@ -24,7 +24,9 @@ class HistoryListQuickFilter @Inject constructor(
} }
add(ListFilterOption.Macro.COMPLETED) add(ListFilterOption.Macro.COMPLETED)
add(ListFilterOption.Macro.FAVORITE) add(ListFilterOption.Macro.FAVORITE)
add(ListFilterOption.Macro.NSFW) if (!settings.isNsfwContentDisabled && !settings.isHistoryExcludeNsfw) {
add(ListFilterOption.Macro.NSFW)
}
repository.getPopularTags(3).mapTo(this) { repository.getPopularTags(3).mapTo(this) {
ListFilterOption.Tag(it) ListFilterOption.Tag(it)
} }

@ -4,9 +4,15 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
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 kotlinx.coroutines.flow.Flow 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 @Dao
abstract class SuggestionDao { abstract class SuggestionDao {
@ -15,9 +21,40 @@ abstract class SuggestionDao {
@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>>
@Transaction fun observeAll(limit: Int, filterOptions: Collection<ListFilterOption>): Flow<List<SuggestionWithManga>> {
@Query("SELECT * FROM suggestions ORDER BY relevance DESC LIMIT :limit") val query = buildString {
abstract fun observeAll(limit: Int): Flow<List<SuggestionWithManga>> 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 @Transaction
@Query("SELECT * FROM suggestions ORDER BY RANDOM() LIMIT 1") @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") @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> 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) @Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: SuggestionEntity): Long abstract suspend fun insert(entity: SuggestionEntity): Long
@ -48,4 +88,14 @@ abstract class SuggestionDao {
insert(entity) 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.toEntity
import org.koitharu.kotatsu.core.db.entity.toManga import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags 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.core.util.ext.mapItems
import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import javax.inject.Inject import javax.inject.Inject
@ -22,8 +25,8 @@ class SuggestionRepository @Inject constructor(
} }
} }
fun observeAll(limit: Int): Flow<List<Manga>> { fun observeAll(limit: Int, filterOptions: Set<ListFilterOption>): Flow<List<Manga>> {
return db.getSuggestionDao().observeAll(limit).mapItems { return db.getSuggestionDao().observeAll(limit, filterOptions).mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.manga.toManga(it.tags.toMangaTags())
} }
} }
@ -48,6 +51,11 @@ class SuggestionRepository @Inject constructor(
return db.getSuggestionDao().count() == 0 return db.getSuggestionDao().count() == 0
} }
suspend fun getTopTags(limit: Int): List<MangaTag> {
return db.getSuggestionDao().getTopTags(limit)
.toMangaTagsList()
}
suspend fun replace(suggestions: Iterable<MangaSuggestion>) { suspend fun replace(suggestions: Iterable<MangaSuggestion>) {
db.withTransaction { db.withTransaction {
db.getSuggestionDao().deleteAll() 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() viewModel.updateSuggestions()
Snackbar.make( Snackbar.make(
requireViewBinding().recyclerView, requireViewBinding().recyclerView,
R.string.feed_will_update_soon, R.string.suggestions_updating,
Snackbar.LENGTH_LONG, Snackbar.LENGTH_LONG,
).show() ).show()
true true

@ -6,6 +6,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus 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.core.util.ext.onFirst
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.domain.MangaListMapper 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.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import org.koitharu.kotatsu.suggestions.domain.SuggestionsListQuickFilter
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -28,27 +31,44 @@ class SuggestionsViewModel @Inject constructor(
settings: AppSettings, settings: AppSettings,
private val mangaListMapper: MangaListMapper, private val mangaListMapper: MangaListMapper,
downloadScheduler: DownloadWorker.Scheduler, downloadScheduler: DownloadWorker.Scheduler,
private val quickFilter: SuggestionsListQuickFilter,
private val suggestionsScheduler: SuggestionsWorker.Scheduler, 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 } override val listMode = settings.observeAsFlow(AppSettings.KEY_LIST_MODE_SUGGESTIONS) { suggestionsListMode }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.suggestionsListMode) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, settings.suggestionsListMode)
override val content = combine( override val content = combine(
repository.observeAll(), quickFilter.appliedOptions.flatMapLatest { repository.observeAll(0, it) },
quickFilter.appliedOptions,
observeListModeWithTriggers(), observeListModeWithTriggers(),
) { list, mode -> ) { list, filters, mode ->
when { when {
list.isEmpty() -> listOf( list.isEmpty() -> if (filters.isEmpty()) {
EmptyState( listOf(
icon = R.drawable.ic_empty_common, EmptyState(
textPrimary = R.string.nothing_found, icon = R.drawable.ic_empty_common,
textSecondary = R.string.text_suggestion_holder, textPrimary = R.string.nothing_found,
actionStringRes = 0, 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 { }.onStart {
loadingCounter.increment() loadingCounter.increment()

@ -97,9 +97,6 @@ abstract class TracksDao {
private fun ListFilterOption.getCondition(): String = when (this) { private fun ListFilterOption.getCondition(): String = when (this) {
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 = ${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})" 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") else -> throw IllegalArgumentException("Unsupported option $this")
} }

Loading…
Cancel
Save