diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt index 1636eb9ad..43f830849 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/db/dao/TrackLogsDao.kt @@ -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") } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt index aabcd94e1..bb9545bb7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/history/domain/HistoryListQuickFilter.kt @@ -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) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt index 53088f007..f11c94534 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/data/SuggestionDao.kt @@ -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> - @Transaction - @Query("SELECT * FROM suggestions ORDER BY relevance DESC LIMIT :limit") - abstract fun observeAll(limit: Int): Flow> + fun observeAll(limit: Int, filterOptions: Collection): Flow> { + 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 + @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 + @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> + + 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") + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt index 2f159ea5c..7de9f3482 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionRepository.kt @@ -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> { - return db.getSuggestionDao().observeAll(limit).mapItems { + fun observeAll(limit: Int, filterOptions: Set): Flow> { + 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 { + return db.getSuggestionDao().getTopTags(limit) + .toMangaTagsList() + } + suspend fun replace(suggestions: Iterable) { db.withTransaction { db.getSuggestionDao().deleteAll() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt new file mode 100644 index 000000000..172fa7291 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/domain/SuggestionsListQuickFilter.kt @@ -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 = buildList(6) { + suggestionRepository.getTopTags(5).mapTo(this) { + ListFilterOption.Tag(it) + } + if (!settings.isNsfwContentDisabled && !settings.isSuggestionsExcludeNsfw) { + add(ListFilterOption.Macro.NSFW) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt index 2aa161e98..4c5bedbfe 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsFragment.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt index 9d3d40eec..c8aa14887 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsViewModel.kt @@ -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() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt index 21ae5e892..9faa9b908 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/data/TracksDao.kt @@ -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") }