diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt index d731c16..f981b3b 100644 --- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt +++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/App.kt @@ -15,6 +15,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState @@ -126,13 +127,13 @@ fun App() { LaunchedEffect(route) { val route = route + polylines.clear() if (route == null) return@LaunchedEffect val geoRoute = ptvService.route(route.routeId, true) val colour = route.routeType.getProperties().colour val allPoints = mutableListOf() - polylines.clear() geoRoute.geopath.forEach { pp -> // TODO: use gtfs colours pp.paths.forEach { sp -> @@ -159,6 +160,7 @@ fun App() { var stop by remember { mutableStateOf(null) } var markers by remember { mutableStateOf(listOf()) } LaunchedEffect(route) { + markers = listOf() route?.let { route -> markers = buildStops(ptvService, route) { stop = it @@ -196,7 +198,9 @@ fun App() { modifier = Modifier.fillMaxSize(), newCameraPosition = newCameraPosition, cameraPositionUpdated = { newCameraPosition = null }, - extInsets = WindowInsets(top = with(LocalDensity.current) { 56.dp.roundToPx() }, bottom = extInsets), + extInsets = WindowInsets(top = with(LocalDensity.current) { + SearchBarDefaults.InputFieldHeight.roundToPx() + }, bottom = extInsets), markers = markers, polylines = polylines, ) @@ -208,6 +212,7 @@ fun App() { if (it) scope.launch { scaffoldState.bottomSheetState.hide() } }, + route = route, text = searchTextState, onTextChange = { searchTextState = it }, onRouteChange = { route = it } diff --git a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/Searcher.kt b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/Searcher.kt index 2e88300..cfbc8f4 100644 --- a/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/Searcher.kt +++ b/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/Searcher.kt @@ -1,17 +1,25 @@ package moe.lava.banksia.ui import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme @@ -21,26 +29,34 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.backhandler.PredictiveBackHandler +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import moe.lava.banksia.api.ptv.PtvService import moe.lava.banksia.api.ptv.structures.ComposableIcon import moe.lava.banksia.api.ptv.structures.PtvRoute +import kotlin.coroutines.cancellation.CancellationException +import kotlin.math.pow -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun Searcher( ptvService: PtvService, expanded: Boolean, onExpandedChange: (Boolean) -> Unit, + route: PtvRoute?, text: String, onTextChange: (String) -> Unit, - onRouteChange: (PtvRoute) -> Unit, + onRouteChange: (PtvRoute?) -> Unit, ) { val animatedPadding by animateDpAsState( if (expanded) { @@ -63,13 +79,41 @@ fun Searcher( ) } SearchBar( - colors = SearchBarDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainer), modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() .padding(horizontal = animatedPadding), + shadowElevation = 6.dp, // Elevation level 3 inputField = { + var backProgress by remember { mutableFloatStateOf(1f) } + var backEdgeIsLeft by remember { mutableStateOf(null) } + val boxOpacityState by animateFloatAsState((1f - backProgress).pow(3)) + val slideState by animateDpAsState((50 * backProgress).dp) + val slidePadding = if (backEdgeIsLeft == true) + PaddingValues(start = slideState) + else if (backEdgeIsLeft == false) + PaddingValues(end = slideState) + else + PaddingValues() + + PredictiveBackHandler(enabled = route != null) { progress -> + try { + progress.collect { backEvent -> + backProgress = backEvent.progress + backEdgeIsLeft = backEvent.swipeEdge == 0 + } + backProgress = 1f + onRouteChange(null) + } catch (_: CancellationException) { + backProgress = 0f + } + backEdgeIsLeft = null + } SearchBarDefaults.InputField( + enabled = route == null, + modifier = Modifier + .alpha(1f - boxOpacityState) + .padding(horizontal = 20.dp - animatedPadding), query = text, onQueryChange = onTextChange, onSearch = {}, @@ -85,6 +129,45 @@ fun Searcher( ) } ) + LaunchedEffect(route) { + backProgress = if (route != null) 0f else 1f; + } + if (route != null) { + Box( + modifier = Modifier + .alpha(boxOpacityState) + .sizeIn( + minHeight = SearchBarDefaults.InputFieldHeight, + maxHeight = SearchBarDefaults.InputFieldHeight, + ) + .fillMaxWidth() + .padding(slidePadding) + ) { + IconButton( + modifier = Modifier.align(Alignment.CenterStart), + onClick = { + onRouteChange(null) + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, "Clear route") + } + Row( + modifier = Modifier + .padding(horizontal = 50.dp) + .align(Alignment.Center), + verticalAlignment = Alignment.CenterVertically, + ) { + route.routeType.ComposableIcon() + Spacer(Modifier.width(15.dp)) + Text( + route.routeNumber.ifEmpty { route.routeName }, + style = MaterialTheme.typography.headlineSmall, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } + } }, expanded = expanded, onExpandedChange = onExpandedChange, @@ -108,7 +191,7 @@ fun Searcher( .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 4.dp) .clickable { - onTextChange(route.getShortFullName()) + onTextChange("") onExpandedChange(false) onRouteChange(route) }