diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryList.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryList.kt index f70771daf8..eba47f19e0 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryList.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/GalleryList.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridS import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBarScrollBehavior import androidx.compose.material3.ShapeDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -64,6 +65,7 @@ import com.hippo.ehviewer.ui.screen.collectDetailSizeAsState import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalGrid import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid import com.hippo.ehviewer.util.displayString +import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.first @@ -90,10 +92,9 @@ fun GalleryList( detailItemContent: @Composable (LazyGridItemScope.(BaseGalleryInfo) -> Unit), thumbListState: LazyStaggeredGridState = rememberLazyStaggeredGridState(), thumbItemContent: @Composable (LazyStaggeredGridItemScope.(BaseGalleryInfo) -> Unit), - searchBarOffsetY: () -> Int, + scrollBehavior: SearchBarScrollBehavior, scrollToTopOnRefresh: Boolean = true, onRefresh: () -> Unit, - onLoading: () -> Unit, ) { val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h) val marginV = dimensionResource(id = R.dimen.gallery_list_margin_v) @@ -189,7 +190,7 @@ fun GalleryList( when (val state = data.loadState.refresh) { is LoadState.Loading -> if (!isRefreshing && scrollToTopOnRefresh) { LaunchedEffect(Unit) { - onLoading() + scrollBehavior.scrollOffset = 0f } LaunchedEffect(Unit) { if (listMode == 0) { @@ -226,8 +227,7 @@ fun GalleryList( PullToRefreshDefaults.Indicator( state = refreshState, isRefreshing = isRefreshing, - modifier = Modifier.align(Alignment.TopCenter).padding(top = contentPadding.calculateTopPadding()) - .offset { IntOffset(0, searchBarOffsetY()) }, + modifier = with(scrollBehavior) { Modifier.align(Alignment.TopCenter).padding(top = contentPadding.calculateTopPadding()).offset { IntOffset(0, scrollBehavior.scrollOffset.roundToInt()) } }, ) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt index a79119137e..e165934d37 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt @@ -48,11 +48,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.fork.SwipeToDismissBox import androidx.compose.material3.fork.SwipeToDismissBoxState +import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -123,7 +125,6 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withNonCancellableContext import eu.kanade.tachiyomi.util.lang.withUIContext -import kotlin.math.roundToInt import kotlinx.coroutines.delay import kotlinx.coroutines.launch import moe.tarsin.coroutines.onEachLatest @@ -138,15 +139,15 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) = val filterMode by Settings.downloadFilterMode.collectAsState { DownloadsFilterMode.from(it) } var filterState by rememberSaveable { mutableStateOf(DownloadsFilterState(filterMode, Settings.recentDownloadLabel.value)) } var invalidateKey by rememberSaveable { mutableStateOf(false) } - var searchBarExpanded by rememberSaveable { mutableStateOf(false) } - var searchBarOffsetY by remember { mutableIntStateOf(0) } val animateItems by Settings.animateItems.collectAsState() + val searchBarState = rememberSearchBarState() + val scrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior() var fabExpanded by remember { mutableStateOf(false) } var fabHidden by remember { mutableStateOf(false) } val checkedInfoMap = remember { mutableStateMapOf() } val selectMode by rememberUpdatedState(checkedInfoMap.isNotEmpty()) - DrawerHandle(!selectMode && !searchBarExpanded) + DrawerHandle(!selectMode && !searchBarState.expanded) val density = LocalDensity.current val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(implicit()) && EhTagDatabase.initialized @@ -210,7 +211,7 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) = } LaunchedEffect(filterState) { - searchBarOffsetY = 0 + scrollBehavior.scrollOffset = 0f } ProvideSideSheetContent { drawerState -> @@ -442,17 +443,19 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) = } } - SearchBarScreen( - onApplySearch = { filterState = filterState.copy(keyword = it) }, - expanded = searchBarExpanded, - onExpandedChange = { - searchBarExpanded = it + LaunchedEffect(searchBarState) { + snapshotFlow { searchBarState.expanded }.collect { fabHidden = it if (it) checkedInfoMap.clear() - }, + } + } + + SearchBarScreen( + onApplySearch = { filterState = filterState.copy(keyword = it) }, + searchBarState = searchBarState, + scrollBehavior = scrollBehavior, title = title, searchFieldHint = hint, - searchBarOffsetY = { searchBarOffsetY }, trailingIcon = { var expanded by remember { mutableStateOf(false) } val sideSheetState = LocalSideSheetState.current @@ -522,7 +525,6 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) = val realPadding = contentPadding + PaddingValues(dimensionResource(id = R.dimen.gallery_list_margin_h), dimensionResource(id = R.dimen.gallery_list_margin_v)) val searchBarConnection = remember { val slop = ViewConfiguration.get(implicit()).scaledTouchSlop - val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() } object : NestedScrollConnection { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { val dy = -consumed.y @@ -531,7 +533,6 @@ fun AnimatedVisibilityScope.DownloadsScreen(navigator: DestinationsNavigator) = } else if (dy <= -slop / 2) { fabHidden = false } - searchBarOffsetY = (searchBarOffsetY - dy).roundToInt().coerceIn(-topPaddingPx, 0) return Offset.Zero // We never consume it } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt index 5aeac0a790..9ae1ec0933 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt @@ -27,25 +27,26 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.currentRecomposeScope import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewModelScope import androidx.paging.Pager @@ -87,7 +88,6 @@ import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withUIContext -import kotlin.math.roundToInt import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -103,11 +103,11 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) = val cloudFavName = stringResource(R.string.cloud_favorites) val animateItems by Settings.animateItems.collectAsState() val hasSignedIn by Settings.hasSignedIn.collectAsState() + val searchBarState = rememberSearchBarState() + val scrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior() // Meta State var urlBuilder by rememberSaveable { mutableStateOf(FavListUrlBuilder(favCat = Settings.recentFavCat)) } - var searchBarExpanded by rememberSaveable { mutableStateOf(false) } - var searchBarOffsetY by remember { mutableIntStateOf(0) } // Derived State val keyword = urlBuilder.keyword @@ -115,7 +115,7 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) = val favCatName = remember(urlBuilder) { when (val favCat = urlBuilder.favCat) { in 0..9 -> Settings.favCat[favCat] - FavListUrlBuilder.FAV_CAT_LOCAL -> localFavName.also { searchBarOffsetY = 0 } + FavListUrlBuilder.FAV_CAT_LOCAL -> localFavName.also { scrollBehavior.scrollOffset = 0f } else -> cloudFavName } } @@ -124,7 +124,6 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) = } else { stringResource(R.string.favorites_title_2, favCatName, keyword) } - val density = LocalDensity.current val localFavCountFlow = rememberInVM { EhDB.localFavCount } val searchBarHint = stringResource(R.string.search_bar_hint, favCatName) val data = rememberInVM(isLocalFav) { @@ -230,20 +229,21 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) = var fabHidden by remember { mutableStateOf(false) } val checkedInfoMap = remember { mutableStateMapOf() } val selectMode = checkedInfoMap.isNotEmpty() - DrawerHandle(!selectMode && !searchBarExpanded) + DrawerHandle(!selectMode && !searchBarState.expanded) + LaunchedEffect(searchBarState) { + snapshotFlow { searchBarState.expanded }.collect { + fabHidden = it + if (it) checkedInfoMap.clear() + } + } SearchBarScreen( onApplySearch = { refresh(FavListUrlBuilder(urlBuilder.favCat, it)) }, - expanded = searchBarExpanded, - onExpandedChange = { - searchBarExpanded = it - fabHidden = it - if (it) checkedInfoMap.clear() - }, + searchBarState = searchBarState, + scrollBehavior = scrollBehavior, title = title, searchFieldHint = searchBarHint, tagNamespace = !isLocalFav, - searchBarOffsetY = { searchBarOffsetY }, trailingIcon = { val sheetState = LocalSideSheetState.current IconButton(onClick = { launch { sheetState.open() } }) { @@ -257,7 +257,6 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) = val showPages by Settings.showGalleryPages.collectAsState() val searchBarConnection = remember { val slop = ViewConfiguration.get(implicit()).scaledTouchSlop - val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() } object : NestedScrollConnection { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { val dy = -consumed.y @@ -266,7 +265,6 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) = } else if (dy <= -slop / 2) { fabHidden = false } - searchBarOffsetY = (searchBarOffsetY - dy).roundToInt().coerceIn(-topPaddingPx, 0) return Offset.Zero // We never consume it } } @@ -332,10 +330,9 @@ fun AnimatedVisibilityScope.FavouritesScreen(navigator: DestinationsNavigator) = ) } }, - searchBarOffsetY = { searchBarOffsetY }, + scrollBehavior = scrollBehavior, scrollToTopOnRefresh = urlBuilder.favCat != FavListUrlBuilder.FAV_CAT_LOCAL, onRefresh = { refresh() }, - onLoading = { searchBarOffsetY = 0 }, ) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt index a35c179dbc..00a07f5768 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt @@ -31,11 +31,13 @@ import androidx.compose.material.icons.outlined.Bookmarks import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem +import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.fork.SwipeToDismissBox import androidx.compose.material3.fork.SwipeToDismissBoxState +import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -124,7 +126,6 @@ import com.ramcosta.composedestinations.spec.Direction import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withUIContext -import kotlin.math.roundToInt import kotlin.random.Random import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -153,15 +154,15 @@ fun AnimatedVisibilityScope.ToplistScreen(navigator: DestinationsNavigator) = Ga @Composable fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = Screen(navigator) { val searchFieldState = rememberTextFieldState() + val searchBarState = rememberSearchBarState() + val scrollBehaviour = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior() var urlBuilder by rememberSaveable(lub) { mutableStateOf(lub) } - var searchBarExpanded by rememberSaveable { mutableStateOf(false) } - var searchBarOffsetY by remember { mutableIntStateOf(0) } val animateItems by Settings.animateItems.collectAsState() var category by rememberMutableStateInDataStore("SearchCategory") { EhUtils.ALL_CATEGORY } var advancedSearchOption by rememberMutableStateInDataStore("AdvancedSearchOption") { AdvancedSearchOption() } - DrawerHandle(!searchBarExpanded) + DrawerHandle(!searchBarState.expanded) LaunchedEffect(urlBuilder) { if (urlBuilder.category != EhUtils.NONE) category = urlBuilder.category @@ -460,6 +461,9 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De var fabExpanded by remember { mutableStateOf(false) } var fabHidden by remember { mutableStateOf(false) } + LaunchedEffect(searchBarState) { + snapshotFlow { searchBarState.expanded }.collect { fabHidden = it } + } val openGalleryKeyword = stringResource(R.string.gallery_list_search_bar_open_gallery) abstract class UrlSuggestion : Suggestion() { @@ -505,11 +509,6 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De SearchBarScreen( onApplySearch = ::onApplySearch, - expanded = searchBarExpanded, - onExpandedChange = { - searchBarExpanded = it - fabHidden = it - }, title = suitableTitle, searchFieldHint = searchBarHint, searchFieldState = searchFieldState, @@ -521,7 +520,6 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De } }, tagNamespace = true, - searchBarOffsetY = { searchBarOffsetY }, trailingIcon = { val sheetState = LocalSideSheetState.current IconButton(onClick = { launch { sheetState.open() } }) { @@ -544,7 +542,6 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De val showPages by Settings.showGalleryPages.collectAsState() val searchBarConnection = remember { val slop = ViewConfiguration.get(implicit()).scaledTouchSlop - val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() } object : NestedScrollConnection { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { val dy = -consumed.y @@ -553,7 +550,6 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De } else if (dy <= -slop / 2) { fabHidden = false } - searchBarOffsetY = (searchBarOffsetY - dy).roundToInt().coerceIn(-topPaddingPx, 0) return Offset.Zero // We never consume it } } @@ -582,12 +578,11 @@ fun AnimatedVisibilityScope.GalleryListScreen(lub: ListUrlBuilder, navigator: De showPages = showPages, ) }, - searchBarOffsetY = { searchBarOffsetY }, + scrollBehavior = scrollBehaviour, onRefresh = { urlBuilder.setRange(0) data.refresh() }, - onLoading = { searchBarOffsetY = 0 }, ) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/HistoryScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/HistoryScreen.kt index 2a017f2ac6..b145806206 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/HistoryScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/HistoryScreen.kt @@ -18,22 +18,16 @@ import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.material3.fork.SwipeToDismissBox import androidx.compose.material3.fork.rememberSwipeToDismissBoxState +import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -64,7 +58,6 @@ import com.hippo.ehviewer.util.FavouriteStatusRouter import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import kotlin.math.roundToInt import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -75,14 +68,11 @@ fun AnimatedVisibilityScope.HistoryScreen(navigator: DestinationsNavigator) = Sc val title = stringResource(id = R.string.history) val hint = stringResource(R.string.search_bar_hint, title) val animateItems by Settings.animateItems.collectAsState() - - var searchBarExpanded by rememberSaveable { mutableStateOf(false) } - var searchBarOffsetY by remember { mutableIntStateOf(0) } + val searchBarState = rememberSearchBarState() var keyword by rememberSaveable { mutableStateOf("") } - DrawerHandle(!searchBarExpanded) + DrawerHandle(!searchBarState.expanded) - val density = LocalDensity.current val historyData = rememberInVM { Pager(config = PagingConfig(pageSize = 20, jumpThreshold = 40)) { if (keyword.isNotEmpty()) { @@ -103,11 +93,9 @@ fun AnimatedVisibilityScope.HistoryScreen(navigator: DestinationsNavigator) = Sc keyword = it historyData.refresh() }, - expanded = searchBarExpanded, - onExpandedChange = { searchBarExpanded = it }, title = title, searchFieldHint = hint, - searchBarOffsetY = { searchBarOffsetY }, + searchBarState = searchBarState, trailingIcon = { IconButton(onClick = { launch { @@ -122,21 +110,11 @@ fun AnimatedVisibilityScope.HistoryScreen(navigator: DestinationsNavigator) = Sc } }, ) { paddingValues -> - val searchBarConnection = remember { - val topPaddingPx = with(density) { paddingValues.calculateTopPadding().roundToPx() } - object : NestedScrollConnection { - override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { - val dy = -consumed.y - searchBarOffsetY = (searchBarOffsetY - dy).roundToInt().coerceIn(-topPaddingPx, 0) - return Offset.Zero // We never consume it - } - } - } val marginH = dimensionResource(id = R.dimen.gallery_list_margin_h) val cardHeight by collectListThumbSizeAsState() val showPages by Settings.showGalleryPages.collectAsState() FastScrollLazyColumn( - modifier = Modifier.nestedScroll(searchBarConnection).fillMaxSize(), + modifier = Modifier.fillMaxSize(), contentPadding = paddingValues + PaddingValues(horizontal = marginH), verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), ) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt index 8bf1a20cbd..493757a63b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt @@ -2,26 +2,9 @@ package com.hippo.ehviewer.ui.screen import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.clickable -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.input.TextFieldState @@ -34,15 +17,20 @@ import androidx.compose.material.icons.automirrored.filled.MenuBook import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.ImageSearch import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.ExpandedFullScreenSearchBar import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.Scaffold -import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarDefaults.InputField +import androidx.compose.material3.SearchBarScrollBehavior +import androidx.compose.material3.SearchBarState +import androidx.compose.material3.SearchBarValue import androidx.compose.material3.Text +import androidx.compose.material3.TopSearchBar +import androidx.compose.material3.rememberSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -51,15 +39,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.coerceAtMost -import androidx.compose.ui.unit.dp import com.hippo.ehviewer.EhApplication.Companion.searchDatabase import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings @@ -71,13 +56,11 @@ import com.hippo.ehviewer.dao.SearchDao import com.hippo.ehviewer.ui.LocalNavDrawerState import com.hippo.ehviewer.ui.destinations.ImageSearchScreenDestination import com.hippo.ehviewer.ui.tools.DialogState -import com.hippo.ehviewer.ui.tools.rememberCompositionActiveState import com.hippo.ehviewer.ui.tools.thenIf import com.jamal.composeprefs3.ui.ifNotNullThen import com.jamal.composeprefs3.ui.ifTrueThen import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.launchUI import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest @@ -103,14 +86,13 @@ context(DialogState, DestinationsNavigator) @Composable fun SearchBarScreen( onApplySearch: (String) -> Unit, - expanded: Boolean, - onExpandedChange: (Boolean) -> Unit, title: String?, searchFieldHint: String, + searchBarState: SearchBarState = rememberSearchBarState(), + scrollBehavior: SearchBarScrollBehavior = SearchBarDefaults.enterAlwaysSearchBarScrollBehavior(), searchFieldState: TextFieldState = rememberTextFieldState(), suggestionProvider: SuggestionProvider? = null, tagNamespace: Boolean = false, - searchBarOffsetY: () -> Int = { 0 }, trailingIcon: @Composable () -> Unit = {}, filter: @Composable (() -> Unit)? = null, floatingActionButton: @Composable () -> Unit = {}, @@ -165,7 +147,7 @@ fun SearchBarScreen( mSuggestionList = mergedSuggestionFlow().toList() } - if (expanded) { + if (searchBarState.expanded) { LaunchedEffect(Unit) { snapshotFlow { searchFieldState.text }.collectLatest { updateSuggestions() @@ -173,9 +155,7 @@ fun SearchBarScreen( } } - fun hideSearchView() { - onExpandedChange(false) - } + fun hideSearchView() = scope.launch { searchBarState.animateToCollapsed() } fun onApplySearch() { // May have invalid whitespaces if pasted from clipboard, replace them with spaces @@ -200,106 +180,95 @@ fun SearchBarScreen( } } - BoxWithConstraints(modifier = Modifier.fillMaxSize()) { - Scaffold( - topBar = { - // Placeholder, fill immutable SearchBar padding - Spacer(modifier = Modifier.statusBarsPadding().height(SearchBarDefaults.InputFieldHeight + 16.dp)) + val inputField = @Composable { + InputField( + textFieldState = searchFieldState, + searchBarState = searchBarState, + onSearch = { + hideSearchView() + onApplySearch() }, - floatingActionButton = floatingActionButton, - content = content, - ) - // https://issuetracker.google.com/337191298 - // Workaround for can't exit SearchBar due to refocus in non-touch mode - Box(Modifier.size(1.dp).focusable()) - val activeState = rememberCompositionActiveState() - SearchBar( - modifier = Modifier.align(Alignment.TopCenter).thenIf(!expanded) { offset { IntOffset(0, searchBarOffsetY()) } } - .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), - inputField = { - InputField( - state = searchFieldState, - onSearch = { - hideSearchView() - onApplySearch() - }, - expanded = expanded, - onExpandedChange = onExpandedChange, - modifier = Modifier.widthIn(max = (maxWidth - SearchBarHorizontalPadding * 2).coerceAtMost(M3SearchBarMaxWidth)).fillMaxWidth(), - placeholder = { - val contentActive by activeState.state - val text = title.takeUnless { expanded || contentActive } ?: searchFieldHint - Text(text, overflow = TextOverflow.Ellipsis, maxLines = 1) - }, - leadingIcon = { - if (expanded) { - IconButton(onClick = { hideSearchView() }) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null) - } - } else { - val drawerState = LocalNavDrawerState.current - IconButton(onClick = { scope.launchUI { drawerState.open() } }) { - Icon(Icons.Default.Menu, contentDescription = null) - } - } - }, - trailingIcon = { - if (expanded) { - AnimatedContent(targetState = searchFieldState.text.isNotEmpty()) { hasText -> - if (hasText) { - IconButton(onClick = { searchFieldState.clearText() }) { - Icon(Icons.Default.Close, contentDescription = null) - } - } else { - IconButton(onClick = { navigate(ImageSearchScreenDestination) }) { - Icon(Icons.Default.ImageSearch, contentDescription = null) - } - } + placeholder = { + val text = title.takeUnless { searchBarState.expanded } ?: searchFieldHint + Text(text, overflow = TextOverflow.Ellipsis, maxLines = 1) + }, + leadingIcon = { + if (searchBarState.expanded) { + IconButton(onClick = { hideSearchView() }) { + Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = null) + } + } else { + val drawerState = LocalNavDrawerState.current + IconButton(onClick = { scope.launch { drawerState.open() } }) { + Icon(Icons.Default.Menu, contentDescription = null) + } + } + }, + trailingIcon = { + if (searchBarState.expanded) { + AnimatedContent(targetState = searchFieldState.text.isNotEmpty()) { hasText -> + if (hasText) { + IconButton(onClick = { searchFieldState.clearText() }) { + Icon(Icons.Default.Close, contentDescription = null) } } else { - Row { - trailingIcon() + IconButton(onClick = { navigate(ImageSearchScreenDestination) }) { + Icon(Icons.Default.ImageSearch, contentDescription = null) } } - }, - ) + } + } else { + Row { + trailingIcon() + } + } }, - expanded = expanded, - onExpandedChange = onExpandedChange, - ) { - activeState.Anchor() - filter?.invoke() - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom).asPaddingValues(), + ) + } + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopSearchBar( + state = searchBarState, + inputField = inputField, + scrollBehavior = scrollBehavior, + ) + ExpandedFullScreenSearchBar( + state = searchBarState, + inputField = inputField, ) { - // Workaround for prepending before the first item - item {} - items(mSuggestionList, key = { it.keyword.hashCode() * 31 + it.canDelete.hashCode() }) { - ListItem( - headlineContent = { Text(text = it.keyword) }, - supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) }, - leadingContent = it.canOpenDirectly.ifTrueThen { - Icon( - imageVector = Icons.AutoMirrored.Default.MenuBook, - contentDescription = null, - ) - }, - trailingContent = it.canDelete.ifTrueThen { - IconButton(onClick = { deleteKeyword(it.keyword) }) { + filter?.invoke() + LazyColumn(modifier = Modifier.fillMaxSize()) { + // Workaround for prepending before the first item + item {} + items(mSuggestionList, key = { it.keyword.hashCode() * 31 + it.canDelete.hashCode() }) { + ListItem( + headlineContent = { Text(text = it.keyword) }, + supportingContent = it.hint.ifNotNullThen { Text(text = it.hint!!) }, + leadingContent = it.canOpenDirectly.ifTrueThen { Icon( - imageVector = Icons.Default.Close, + imageVector = Icons.AutoMirrored.Default.MenuBook, contentDescription = null, ) - } - }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - modifier = Modifier.clickable { it.onClick() }.thenIf(animateItems) { animateItem() }, - ) + }, + trailingContent = it.canDelete.ifTrueThen { + IconButton(onClick = { deleteKeyword(it.keyword) }) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null, + ) + } + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent), + modifier = Modifier.clickable { it.onClick() }.thenIf(animateItems) { animateItem() }, + ) + } } } - } - } + }, + floatingActionButton = floatingActionButton, + content = content, + ) } fun wrapTagKeyword(keyword: String, translate: Boolean = false): String = if (keyword.endsWith(':')) { @@ -320,5 +289,6 @@ fun wrapTagKeyword(keyword: String, translate: Boolean = false): String = if (ke } private val WhitespaceRegex = Regex("\\s+") -private val SearchBarHorizontalPadding = 16.dp -private val M3SearchBarMaxWidth = 720.dp + +val SearchBarState.expanded + get() = targetValue == SearchBarValue.Expanded diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/CompositionActive.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/CompositionActive.kt deleted file mode 100644 index 81f82e62ab..0000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/CompositionActive.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.hippo.ehviewer.ui.tools - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember - -@JvmInline -value class CompositionActiveState(val state: MutableState) { - @Composable - fun Anchor() = DisposableEffect(state) { - state.value = true - onDispose { - state.value = false - } - } -} - -@Composable -fun rememberCompositionActiveState() = remember { - CompositionActiveState(mutableStateOf(false)) -}