diff --git a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt index 47fa75d2a..7e4697e68 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/GlobalSearchScreen.kt @@ -1,45 +1,21 @@ 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 import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import eu.kanade.presentation.browse.components.GlobalSearchCardRow -import eu.kanade.presentation.browse.components.GlobalSearchEmptyResultItem import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem 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.GlobalSearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter 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.VerticalDivider -import tachiyomi.presentation.core.components.material.padding @Composable fun GlobalSearchScreen( @@ -56,76 +32,19 @@ fun GlobalSearchScreen( ) { Scaffold( topBar = { 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), - ) { - // TODO: make this UX better; it only applies when triggering a new search - FilterChip( - selected = state.sourceFilter == SourceFilter.PinnedOnly, - onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.PushPin, - contentDescription = null, - modifier = Modifier - .size(FilterChipDefaults.IconSize), - ) - }, - label = { - Text(text = stringResource(id = R.string.pinned_sources)) - }, - ) - FilterChip( - selected = state.sourceFilter == SourceFilter.All, - onClick = { onChangeSearchFilter(SourceFilter.All) }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.DoneAll, - contentDescription = null, - modifier = Modifier - .size(FilterChipDefaults.IconSize), - ) - }, - label = { - Text(text = stringResource(id = R.string.all)) - }, - ) - - VerticalDivider() - - FilterChip( - selected = state.onlyShowHasResults, - onClick = { onToggleResults() }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.FilterList, - contentDescription = null, - modifier = Modifier - .size(FilterChipDefaults.IconSize), - ) - }, - label = { - Text(text = stringResource(id = R.string.has_results)) - }, - ) - } - - Divider() - } + GlobalSearchToolbar( + searchQuery = state.searchQuery, + progress = state.progress, + total = state.total, + navigateUp = navigateUp, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + sourceFilter = state.sourceFilter, + onChangeSearchFilter = onChangeSearchFilter, + onlyShowHasResults = state.onlyShowHasResults, + onToggleResults = onToggleResults, + scrollBehavior = scrollBehavior, + ) }, ) { paddingValues -> GlobalSearchContent( @@ -141,7 +60,7 @@ fun GlobalSearchScreen( @Composable internal fun GlobalSearchContent( - sourceId: Long? = null, + fromSourceId: Long? = null, items: Map, contentPadding: PaddingValues, getManga: @Composable (Manga) -> State, @@ -155,7 +74,7 @@ internal fun GlobalSearchContent( items.forEach { (source, result) -> item(key = source.id) { GlobalSearchResultItem( - title = sourceId?.let { "▶ ${source.name}".takeIf { source.id == sourceId } } ?: source.name, + title = fromSourceId?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name, subtitle = LocaleHelper.getDisplayName(source.lang), onClick = { onClickSource(source) }, ) { @@ -164,11 +83,6 @@ internal fun GlobalSearchContent( GlobalSearchLoadingResultItem() } is SearchItemResult.Success -> { - if (result.isEmpty) { - GlobalSearchEmptyResultItem() - return@GlobalSearchResultItem - } - GlobalSearchCardRow( titles = result.result, getManga = getManga, diff --git a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt index 1b2a04ce0..3b41222ff 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/MigrateSearchScreen.kt @@ -5,16 +5,19 @@ import androidx.compose.runtime.State import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreenModel +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import tachiyomi.domain.manga.model.Manga import tachiyomi.presentation.core.components.material.Scaffold @Composable fun MigrateSearchScreen( - navigateUp: () -> Unit, state: MigrateSearchScreenModel.State, - getManga: @Composable (Manga) -> State, + navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, onSearch: (String) -> Unit, + onChangeSearchFilter: (SourceFilter) -> Unit, + onToggleResults: () -> Unit, + getManga: @Composable (Manga) -> State, onClickSource: (CatalogueSource) -> Unit, onClickItem: (Manga) -> Unit, onLongClickItem: (Manga) -> Unit, @@ -28,13 +31,17 @@ fun MigrateSearchScreen( navigateUp = navigateUp, onChangeSearchQuery = onChangeSearchQuery, onSearch = onSearch, + sourceFilter = state.sourceFilter, + onChangeSearchFilter = onChangeSearchFilter, + onlyShowHasResults = state.onlyShowHasResults, + onToggleResults = onToggleResults, scrollBehavior = scrollBehavior, ) }, ) { paddingValues -> GlobalSearchContent( - sourceId = state.manga?.source, - items = state.items, + fromSourceId = state.manga?.source, + items = state.filteredItems, contentPadding = paddingValues, getManga = getManga, onClickSource = onClickSource, diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt index f094d6b98..f10f834f4 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchCardRow.kt @@ -3,17 +3,21 @@ package eu.kanade.presentation.browse.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import eu.kanade.presentation.library.components.CommonMangaItemDefaults import eu.kanade.presentation.library.components.MangaComfortableGridItem +import eu.kanade.tachiyomi.R import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.MangaCover import tachiyomi.domain.manga.model.asMangaCover @@ -26,13 +30,18 @@ fun GlobalSearchCardRow( onClick: (Manga) -> Unit, onLongClick: (Manga) -> Unit, ) { + if (titles.isEmpty()) { + EmptyResultItem() + return + } + LazyRow( contentPadding = PaddingValues(MaterialTheme.padding.small), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny), ) { items(titles) { val title by getManga(it) - GlobalSearchCard( + MangaItem( title = title.title, cover = title.asMangaCover(), isFavorite = title.favorite, @@ -44,7 +53,7 @@ fun GlobalSearchCardRow( } @Composable -private fun GlobalSearchCard( +private fun MangaItem( title: String, cover: MangaCover, isFavorite: Boolean, @@ -64,3 +73,15 @@ private fun GlobalSearchCard( ) } } + +@Composable +private fun EmptyResultItem() { + Text( + text = stringResource(R.string.no_results_found), + modifier = Modifier + .padding( + horizontal = MaterialTheme.padding.medium, + vertical = MaterialTheme.padding.small, + ), + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt index 8472ad657..1270c011b 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchResultItems.kt @@ -61,18 +61,6 @@ fun GlobalSearchResultItem( } } -@Composable -fun GlobalSearchEmptyResultItem() { - Text( - text = stringResource(R.string.no_results_found), - modifier = Modifier - .padding( - horizontal = MaterialTheme.padding.medium, - vertical = MaterialTheme.padding.small, - ), - ) -} - @Composable fun GlobalSearchLoadingResultItem() { Box( diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt index d45a02853..bdcb38d52 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/GlobalSearchToolbar.kt @@ -1,13 +1,36 @@ package eu.kanade.presentation.browse.components +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import eu.kanade.presentation.components.SearchToolbar +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter +import tachiyomi.presentation.core.components.material.Divider +import tachiyomi.presentation.core.components.material.VerticalDivider +import tachiyomi.presentation.core.components.material.padding @Composable fun GlobalSearchToolbar( @@ -17,24 +40,89 @@ fun GlobalSearchToolbar( navigateUp: () -> Unit, onChangeSearchQuery: (String?) -> Unit, onSearch: (String) -> Unit, + sourceFilter: SourceFilter, + onChangeSearchFilter: (SourceFilter) -> Unit, + onlyShowHasResults: Boolean, + onToggleResults: () -> Unit, scrollBehavior: TopAppBarScrollBehavior, ) { - Box { - SearchToolbar( - searchQuery = searchQuery, - onChangeSearchQuery = onChangeSearchQuery, - onSearch = onSearch, - onClickCloseSearch = navigateUp, - navigateUp = navigateUp, - scrollBehavior = scrollBehavior, - ) - if (progress in 1 until total) { - LinearProgressIndicator( - progress = progress / total.toFloat(), - modifier = Modifier - .align(Alignment.BottomStart) - .fillMaxWidth(), + Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) { + Box { + SearchToolbar( + searchQuery = searchQuery, + onChangeSearchQuery = onChangeSearchQuery, + onSearch = onSearch, + onClickCloseSearch = navigateUp, + navigateUp = navigateUp, + scrollBehavior = scrollBehavior, + ) + if (progress in 1 until total) { + LinearProgressIndicator( + progress = progress / total.toFloat(), + modifier = Modifier + .align(Alignment.BottomStart) + .fillMaxWidth(), + ) + } + } + + Row( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + .padding(horizontal = MaterialTheme.padding.small), + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + ) { + // TODO: make this UX better; it only applies when triggering a new search + FilterChip( + selected = sourceFilter == SourceFilter.PinnedOnly, + onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.PushPin, + contentDescription = null, + modifier = Modifier + .size(FilterChipDefaults.IconSize), + ) + }, + label = { + Text(text = stringResource(id = R.string.pinned_sources)) + }, + ) + FilterChip( + selected = sourceFilter == SourceFilter.All, + onClick = { onChangeSearchFilter(SourceFilter.All) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.DoneAll, + contentDescription = null, + modifier = Modifier + .size(FilterChipDefaults.IconSize), + ) + }, + label = { + Text(text = stringResource(id = R.string.all)) + }, + ) + + VerticalDivider() + + FilterChip( + selected = onlyShowHasResults, + onClick = { onToggleResults() }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.FilterList, + contentDescription = null, + modifier = Modifier + .size(FilterChipDefaults.IconSize), + ) + }, + label = { + Text(text = stringResource(id = R.string.has_results)) + }, ) } + + Divider() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt index ae89d9cb0..ac69c9f36 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreen.kt @@ -21,11 +21,13 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() { val state by screenModel.state.collectAsState() MigrateSearchScreen( - navigateUp = navigator::pop, state = state, - getManga = { screenModel.getManga(it) }, + navigateUp = navigator::pop, onChangeSearchQuery = screenModel::updateSearchQuery, onSearch = screenModel::search, + getManga = { screenModel.getManga(it) }, + onChangeSearchFilter = screenModel::setSourceFilter, + onToggleResults = screenModel::toggleFilterResults, onClickSource = { if (!screenModel.incognitoMode.get()) { screenModel.lastUsedSourceId.set(it.id) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt index 87a4e6921..c57cae057 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/MigrateSearchScreenModel.kt @@ -7,6 +7,7 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel +import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import tachiyomi.domain.manga.interactor.GetManga @@ -23,6 +24,9 @@ class MigrateSearchScreenModel( private val getManga: GetManga = Injekt.get(), ) : SearchScreenModel(State()) { + val incognitoMode = preferences.incognitoMode() + val lastUsedSourceId = sourcePreferences.lastUsedSource() + init { coroutineScope.launch { val manga = getManga.await(mangaId)!! @@ -35,15 +39,13 @@ class MigrateSearchScreenModel( } } - val incognitoMode = preferences.incognitoMode() - val lastUsedSourceId = sourcePreferences.lastUsedSource() - override fun getEnabledSources(): List { val enabledLanguages = sourcePreferences.enabledLanguages().get() val disabledSources = sourcePreferences.disabledSources().get() val pinnedSources = sourcePreferences.pinnedSources().get() return sourceManager.getCatalogueSources() + .filter { mutableState.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } .filter { it.lang in enabledLanguages } .filterNot { "${it.id}" in disabledSources } .sortedWith(compareBy({ "${it.id}" !in pinnedSources }, { "${it.name.lowercase()} (${it.lang})" })) @@ -66,6 +68,16 @@ class MigrateSearchScreenModel( return mutableState.value.items } + fun setSourceFilter(filter: SourceFilter) { + mutableState.update { it.copy(sourceFilter = filter) } + } + + fun toggleFilterResults() { + mutableState.update { + it.copy(onlyShowHasResults = !it.onlyShowHasResults) + } + } + fun setDialog(dialog: MigrateSearchDialog?) { mutableState.update { it.copy(dialog = dialog) @@ -75,12 +87,16 @@ class MigrateSearchScreenModel( @Immutable data class State( val manga: Manga? = null, - val searchQuery: String? = null, - val items: Map = emptyMap(), val dialog: MigrateSearchDialog? = null, + + val searchQuery: String? = null, + val sourceFilter: SourceFilter = SourceFilter.PinnedOnly, + val onlyShowHasResults: Boolean = false, + val items: Map = emptyMap(), ) { val progress: Int = items.count { it.value !is SearchItemResult.Loading } val total: Int = items.size + val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt index c1b649ec9..519699d2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/GlobalSearchScreenModel.kt @@ -19,6 +19,7 @@ class GlobalSearchScreenModel( val incognitoMode = preferences.incognitoMode() val lastUsedSourceId = sourcePreferences.lastUsedSource() + init { extensionFilter = initialExtensionFilter if (initialQuery.isNotBlank() || !initialExtensionFilter.isNullOrBlank()) { @@ -76,7 +77,3 @@ class GlobalSearchScreenModel( val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) } } } - -private fun SearchItemResult.isVisible(onlyShowHasResults: Boolean): Boolean { - return !onlyShowHasResults || (this is SearchItemResult.Success && !this.isEmpty) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt index 207b9561c..bcc9dc1ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/globalsearch/SearchScreenModel.kt @@ -145,4 +145,8 @@ sealed class SearchItemResult { val isEmpty: Boolean get() = result.isEmpty() } + + fun isVisible(onlyShowHasResults: Boolean): Boolean { + return !onlyShowHasResults || (this is Success && !this.isEmpty) + } }