mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	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
This commit is contained in:
		@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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(),
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user