Banksia/composeApp/src/commonMain/kotlin/moe/lava/banksia/ui/Searcher.kt

203 lines
8 KiB
Kotlin
Raw Normal View History

2025-04-14 13:35:26 +10:00
package moe.lava.banksia.ui
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState
2025-04-14 13:35:26 +10:00
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
2025-04-14 13:35:26 +10:00
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
2025-04-14 13:35:26 +10:00
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
2025-04-14 13:35:26 +10:00
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
2025-04-14 21:07:05 +10:00
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
2025-04-14 13:35:26 +10:00
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
2025-04-14 21:07:05 +10:00
import androidx.compose.material3.Text
2025-04-14 13:35:26 +10:00
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
2025-04-14 21:07:05 +10:00
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
2025-04-14 13:35:26 +10:00
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
2025-04-14 13:35:26 +10:00
import androidx.compose.ui.Modifier
import androidx.compose.ui.backhandler.PredictiveBackHandler
import androidx.compose.ui.draw.alpha
2025-04-14 21:07:05 +10:00
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
2025-04-14 13:35:26 +10:00
import androidx.compose.ui.unit.dp
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
2025-04-14 13:35:26 +10:00
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
2025-04-14 13:35:26 +10:00
@Composable
fun Searcher(
2025-07-28 01:39:31 +10:00
selectedRoute: PtvRoute?,
routes: List<PtvRoute>,
2025-04-14 13:35:26 +10:00
expanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
text: String,
onTextChange: (String) -> Unit,
onRouteChange: (PtvRoute?) -> Unit,
2025-04-14 13:35:26 +10:00
) {
val animatedPadding by animateDpAsState(
if (expanded) {
0.dp
} else {
20.dp
},
label = "padding"
)
2025-07-28 01:39:31 +10:00
2025-04-14 13:35:26 +10:00
Box(modifier = Modifier.fillMaxSize()) {
SearchBar(
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
.padding(horizontal = animatedPadding),
shadowElevation = 6.dp, // Elevation level 3
2025-04-14 13:35:26 +10:00
inputField = {
var backProgress by remember { mutableFloatStateOf(1f) }
var backEdgeIsLeft by remember { mutableStateOf<Boolean?>(null) }
val routeInfoOpacity by animateFloatAsState((1f - backProgress).pow(3))
val slideState by animateDpAsState((50 * backProgress).dp)
2025-07-28 01:39:31 +10:00
val slidePadding = when (backEdgeIsLeft) {
true -> PaddingValues(start = slideState)
false -> PaddingValues(end = slideState)
null -> PaddingValues()
}
2025-07-28 01:39:31 +10:00
PredictiveBackHandler(enabled = selectedRoute != null) { progress ->
try {
progress.collect { backEvent ->
backProgress = backEvent.progress
backEdgeIsLeft = backEvent.swipeEdge == 0
}
backProgress = 1f
onRouteChange(null)
} catch (_: CancellationException) {
backProgress = 0f
}
backEdgeIsLeft = null
}
2025-04-14 13:35:26 +10:00
SearchBarDefaults.InputField(
2025-07-28 01:39:31 +10:00
enabled = selectedRoute == null,
modifier = Modifier
.alpha(1f - routeInfoOpacity)
.padding(horizontal = 20.dp - animatedPadding),
2025-04-14 13:35:26 +10:00
query = text,
onQueryChange = onTextChange,
onSearch = {},
expanded = expanded,
onExpandedChange = onExpandedChange,
leadingIcon = { Icon(Icons.Default.Search, null) },
trailingIcon = {
if (expanded && text.isNotEmpty())
Icon(
imageVector = Icons.Default.Clear,
contentDescription = null,
modifier = Modifier.clickable { onTextChange("") }
)
}
)
2025-07-28 01:39:31 +10:00
LaunchedEffect(selectedRoute) {
backProgress = if (selectedRoute != null) 0f else 1f;
}
2025-07-28 01:39:31 +10:00
if (selectedRoute != null)
RouteInfo(routeInfoOpacity, slidePadding, onRouteChange, selectedRoute)
2025-04-14 13:35:26 +10:00
},
expanded = expanded,
onExpandedChange = onExpandedChange,
) {
LazyColumn(modifier = Modifier.fillMaxWidth()) {
2025-04-14 21:07:05 +10:00
for (route in routes) {
if (!route.routeNumber.contains(text) &&
!route.routeName.lowercase().contains(text.lowercase()))
2025-04-14 13:35:26 +10:00
continue
item {
ListItem(
2025-04-14 21:07:05 +10:00
headlineContent = { Text(route.routeNumber.ifEmpty { route.routeName }) },
2025-04-14 13:35:26 +10:00
supportingContent = {
2025-04-14 21:07:05 +10:00
if (route.routeNumber.isNotEmpty()) {
Text(route.routeName)
2025-04-14 13:35:26 +10:00
}
},
2025-04-15 14:09:46 +10:00
leadingContent = { route.routeType.ComposableIcon() },
2025-04-14 13:35:26 +10:00
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
.clickable {
onTextChange("")
2025-04-14 21:07:05 +10:00
onExpandedChange(false)
onRouteChange(route)
2025-04-14 13:35:26 +10:00
}
)
}
2025-04-14 21:07:05 +10:00
}
2025-04-14 13:35:26 +10:00
}
}
}
2025-04-14 21:53:07 +10:00
}
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun RouteInfo(
routeInfoOpacity: Float,
slidePadding: PaddingValues,
onRouteChange: (PtvRoute?) -> Unit,
route: PtvRoute
) {
Box(
modifier = Modifier
.alpha(routeInfoOpacity)
.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,
)
}
}
}