Browse Source

Add filters to Global search (#9691)

* add pinned and available filter chips to global search

* split filter predicate into seperate function

* change the global search available filter to has Results

* reordering of imports
zaghdaneh 1 year ago
parent
commit
cbcec8c4d9

+ 87 - 10
app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt

@@ -1,8 +1,22 @@
 package eu.kanade.presentation.browse
 
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.DoneAll
+import androidx.compose.material.icons.outlined.FilterList
+import androidx.compose.material.icons.outlined.PushPin
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
+import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
@@ -16,19 +30,23 @@ import eu.kanade.presentation.browse.components.GlobalSearchResultItem
 import eu.kanade.presentation.browse.components.GlobalSearchToolbar
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchFilter
 import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState
 import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
 import eu.kanade.tachiyomi.util.system.LocaleHelper
 import tachiyomi.domain.manga.model.Manga
+import tachiyomi.presentation.core.components.material.Divider
 import tachiyomi.presentation.core.components.material.Scaffold
 import tachiyomi.presentation.core.components.material.padding
 
 @Composable
 fun GlobalSearchScreen(
     state: GlobalSearchState,
+    items: Map<CatalogueSource, SearchItemResult>,
     navigateUp: () -> Unit,
     onChangeSearchQuery: (String?) -> Unit,
     onSearch: (String) -> Unit,
+    onChangeFilter: (GlobalSearchFilter) -> Unit,
     getManga: @Composable (Manga) -> State<Manga>,
     onClickSource: (CatalogueSource) -> Unit,
     onClickItem: (Manga) -> Unit,
@@ -36,19 +54,78 @@ fun GlobalSearchScreen(
 ) {
     Scaffold(
         topBar = { scrollBehavior ->
-            GlobalSearchToolbar(
-                searchQuery = state.searchQuery,
-                progress = state.progress,
-                total = state.total,
-                navigateUp = navigateUp,
-                onChangeSearchQuery = onChangeSearchQuery,
-                onSearch = onSearch,
-                scrollBehavior = scrollBehavior,
-            )
+            Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
+                GlobalSearchToolbar(
+                    searchQuery = state.searchQuery,
+                    progress = state.progress,
+                    total = state.total,
+                    navigateUp = navigateUp,
+                    onChangeSearchQuery = onChangeSearchQuery,
+                    onSearch = onSearch,
+                    scrollBehavior = scrollBehavior,
+                )
+
+                Row(
+                    modifier = Modifier
+                        .horizontalScroll(rememberScrollState())
+                        .padding(horizontal = MaterialTheme.padding.small),
+                    horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
+                ) {
+                    FilterChip(
+                        selected = state.searchFilter == GlobalSearchFilter.All,
+                        onClick = { onChangeFilter(GlobalSearchFilter.All) },
+                        leadingIcon = {
+                            Icon(
+                                imageVector = Icons.Outlined.DoneAll,
+                                contentDescription = "",
+                                modifier = Modifier
+                                    .size(FilterChipDefaults.IconSize),
+                            )
+                        },
+                        label = {
+                            Text(text = stringResource(id = R.string.all))
+                        },
+                    )
+
+                    FilterChip(
+                        selected = state.searchFilter == GlobalSearchFilter.PinnedOnly,
+                        onClick = { onChangeFilter(GlobalSearchFilter.PinnedOnly) },
+                        leadingIcon = {
+                            Icon(
+                                imageVector = Icons.Outlined.PushPin,
+                                contentDescription = "",
+                                modifier = Modifier
+                                    .size(FilterChipDefaults.IconSize),
+                            )
+                        },
+                        label = {
+                            Text(text = stringResource(id = R.string.pinned_sources))
+                        },
+                    )
+
+                    FilterChip(
+                        selected = state.searchFilter == GlobalSearchFilter.AvailableOnly,
+                        onClick = { onChangeFilter(GlobalSearchFilter.AvailableOnly) },
+                        leadingIcon = {
+                            Icon(
+                                imageVector = Icons.Outlined.FilterList,
+                                contentDescription = "",
+                                modifier = Modifier
+                                    .size(FilterChipDefaults.IconSize),
+                            )
+                        },
+                        label = {
+                            Text(text = stringResource(id = R.string.has_results))
+                        },
+                    )
+                }
+
+                Divider()
+            }
         },
     ) { paddingValues ->
         GlobalSearchContent(
-            items = state.items,
+            items = items,
             contentPadding = paddingValues,
             getManga = getManga,
             onClickSource = onClickSource,

+ 3 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreen.kt

@@ -35,6 +35,7 @@ class GlobalSearchScreen(
         var showSingleLoadingScreen by remember {
             mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1)
         }
+        val filteredSources by screenModel.searchPagerFlow.collectAsState()
 
         if (showSingleLoadingScreen) {
             LoadingScreen()
@@ -57,10 +58,12 @@ class GlobalSearchScreen(
         } else {
             GlobalSearchScreen(
                 state = state,
+                items = filteredSources,
                 navigateUp = navigator::pop,
                 onChangeSearchQuery = screenModel::updateSearchQuery,
                 onSearch = screenModel::search,
                 getManga = { screenModel.getManga(it) },
+                onChangeFilter = screenModel::setFilter,
                 onClickSource = {
                     if (!screenModel.incognitoMode.get()) {
                         screenModel.lastUsedSourceId.set(it.id)

+ 29 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt

@@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
 import androidx.compose.runtime.Immutable
 import eu.kanade.domain.base.BasePreferences
 import eu.kanade.domain.source.service.SourcePreferences
+import eu.kanade.presentation.util.ioCoroutineScope
 import eu.kanade.tachiyomi.source.CatalogueSource
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.update
 import tachiyomi.domain.source.service.SourceManager
 import uy.kohesive.injekt.Injekt
@@ -20,6 +25,13 @@ class GlobalSearchScreenModel(
     val incognitoMode = preferences.incognitoMode()
     val lastUsedSourceId = sourcePreferences.lastUsedSource()
 
+    val searchPagerFlow = state.map { Pair(it.searchFilter, it.items) }
+        .distinctUntilChanged()
+        .map { (filter, items) ->
+            items
+                .filter { (source, result) -> isSourceVisible(filter, source, result) }
+        }.stateIn(ioCoroutineScope, SharingStarted.Lazily, state.value.items)
+
     init {
         extensionFilter = initialExtensionFilter
         if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) {
@@ -38,6 +50,14 @@ class GlobalSearchScreenModel(
             .sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }))
     }
 
+    private fun isSourceVisible(filter: GlobalSearchFilter, source: CatalogueSource, result: SearchItemResult): Boolean {
+        return when (filter) {
+            GlobalSearchFilter.AvailableOnly -> result is SearchItemResult.Success && !result.isEmpty
+            GlobalSearchFilter.PinnedOnly -> "${source.id}" in sourcePreferences.pinnedSources().get()
+            GlobalSearchFilter.All -> true
+        }
+    }
+
     override fun updateSearchQuery(query: String?) {
         mutableState.update {
             it.copy(searchQuery = query)
@@ -50,14 +70,23 @@ class GlobalSearchScreenModel(
         }
     }
 
+    fun setFilter(filter: GlobalSearchFilter) {
+        mutableState.update { it.copy(searchFilter = filter) }
+    }
+
     override fun getItems(): Map<CatalogueSource, SearchItemResult> {
         return mutableState.value.items
     }
 }
 
+enum class GlobalSearchFilter {
+    All, PinnedOnly, AvailableOnly
+}
+
 @Immutable
 data class GlobalSearchState(
     val searchQuery: String? = null,
+    val searchFilter: GlobalSearchFilter = GlobalSearchFilter.All,
     val items: Map<CatalogueSource, SearchItemResult> = emptyMap(),
 ) {
 

+ 1 - 0
i18n/src/main/res/values/strings.xml

@@ -627,6 +627,7 @@
     <string name="latest">Latest</string>
     <string name="popular">Popular</string>
     <string name="browse">Browse</string>
+    <string name="has_results">Has results</string>
     <string name="local_source_help_guide">Local source guide</string>
     <string name="no_pinned_sources">You have no pinned sources</string>
     <string name="chapter_not_found">Chapter not found</string>