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:
parent
2f05f7b91f
commit
cbcec8c4d9
4 changed files with 120 additions and 10 deletions
|
@ -1,8 +1,22 @@
|
||||||
package eu.kanade.presentation.browse
|
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.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.presentation.browse.components.GlobalSearchToolbar
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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.GlobalSearchState
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.manga.model.Manga
|
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.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchScreen(
|
fun GlobalSearchScreen(
|
||||||
state: GlobalSearchState,
|
state: GlobalSearchState,
|
||||||
|
items: Map<CatalogueSource, SearchItemResult>,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
|
onChangeFilter: (GlobalSearchFilter) -> Unit,
|
||||||
getManga: @Composable (Manga) -> State<Manga>,
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (CatalogueSource) -> Unit,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
|
@ -36,19 +54,78 @@ fun GlobalSearchScreen(
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
GlobalSearchToolbar(
|
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||||
searchQuery = state.searchQuery,
|
GlobalSearchToolbar(
|
||||||
progress = state.progress,
|
searchQuery = state.searchQuery,
|
||||||
total = state.total,
|
progress = state.progress,
|
||||||
navigateUp = navigateUp,
|
total = state.total,
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
navigateUp = navigateUp,
|
||||||
onSearch = onSearch,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
scrollBehavior = scrollBehavior,
|
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 ->
|
) { paddingValues ->
|
||||||
GlobalSearchContent(
|
GlobalSearchContent(
|
||||||
items = state.items,
|
items = items,
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
getManga = getManga,
|
getManga = getManga,
|
||||||
onClickSource = onClickSource,
|
onClickSource = onClickSource,
|
||||||
|
|
|
@ -35,6 +35,7 @@ class GlobalSearchScreen(
|
||||||
var showSingleLoadingScreen by remember {
|
var showSingleLoadingScreen by remember {
|
||||||
mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1)
|
mutableStateOf(searchQuery.isNotEmpty() && extensionFilter.isNotEmpty() && state.total == 1)
|
||||||
}
|
}
|
||||||
|
val filteredSources by screenModel.searchPagerFlow.collectAsState()
|
||||||
|
|
||||||
if (showSingleLoadingScreen) {
|
if (showSingleLoadingScreen) {
|
||||||
LoadingScreen()
|
LoadingScreen()
|
||||||
|
@ -57,10 +58,12 @@ class GlobalSearchScreen(
|
||||||
} else {
|
} else {
|
||||||
GlobalSearchScreen(
|
GlobalSearchScreen(
|
||||||
state = state,
|
state = state,
|
||||||
|
items = filteredSources,
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
onChangeSearchQuery = screenModel::updateSearchQuery,
|
onChangeSearchQuery = screenModel::updateSearchQuery,
|
||||||
onSearch = screenModel::search,
|
onSearch = screenModel::search,
|
||||||
getManga = { screenModel.getManga(it) },
|
getManga = { screenModel.getManga(it) },
|
||||||
|
onChangeFilter = screenModel::setFilter,
|
||||||
onClickSource = {
|
onClickSource = {
|
||||||
if (!screenModel.incognitoMode.get()) {
|
if (!screenModel.incognitoMode.get()) {
|
||||||
screenModel.lastUsedSourceId.set(it.id)
|
screenModel.lastUsedSourceId.set(it.id)
|
||||||
|
|
|
@ -3,7 +3,12 @@ package eu.kanade.tachiyomi.ui.browse.source.globalsearch
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
import eu.kanade.presentation.util.ioCoroutineScope
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
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 kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -20,6 +25,13 @@ class GlobalSearchScreenModel(
|
||||||
val incognitoMode = preferences.incognitoMode()
|
val incognitoMode = preferences.incognitoMode()
|
||||||
val lastUsedSourceId = sourcePreferences.lastUsedSource()
|
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 {
|
init {
|
||||||
extensionFilter = initialExtensionFilter
|
extensionFilter = initialExtensionFilter
|
||||||
if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) {
|
if (initialQuery.isNotBlank() || initialExtensionFilter.isNotBlank()) {
|
||||||
|
@ -38,6 +50,14 @@ class GlobalSearchScreenModel(
|
||||||
.sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" }))
|
.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?) {
|
override fun updateSearchQuery(query: String?) {
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(searchQuery = query)
|
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> {
|
override fun getItems(): Map<CatalogueSource, SearchItemResult> {
|
||||||
return mutableState.value.items
|
return mutableState.value.items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class GlobalSearchFilter {
|
||||||
|
All, PinnedOnly, AvailableOnly
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class GlobalSearchState(
|
data class GlobalSearchState(
|
||||||
val searchQuery: String? = null,
|
val searchQuery: String? = null,
|
||||||
|
val searchFilter: GlobalSearchFilter = GlobalSearchFilter.All,
|
||||||
val items: Map<CatalogueSource, SearchItemResult> = emptyMap(),
|
val items: Map<CatalogueSource, SearchItemResult> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
@ -627,6 +627,7 @@
|
||||||
<string name="latest">Latest</string>
|
<string name="latest">Latest</string>
|
||||||
<string name="popular">Popular</string>
|
<string name="popular">Popular</string>
|
||||||
<string name="browse">Browse</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="local_source_help_guide">Local source guide</string>
|
||||||
<string name="no_pinned_sources">You have no pinned sources</string>
|
<string name="no_pinned_sources">You have no pinned sources</string>
|
||||||
<string name="chapter_not_found">Chapter not found</string>
|
<string name="chapter_not_found">Chapter not found</string>
|
||||||
|
|
Reference in a new issue