2025-04-14 13:35:26 +10:00
|
|
|
package moe.lava.banksia.ui
|
|
|
|
|
|
|
|
|
|
import androidx.compose.animation.core.animateDpAsState
|
2025-05-01 19:32:28 +10:00
|
|
|
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
|
2025-05-01 19:32:28 +10:00
|
|
|
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
|
2025-05-01 19:32:28 +10:00
|
|
|
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
|
2025-05-01 19:32:28 +10:00
|
|
|
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
|
2025-05-01 19:32:28 +10:00
|
|
|
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
|
2025-05-01 19:32:28 +10:00
|
|
|
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
|
2025-05-01 19:32:28 +10:00
|
|
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
2025-04-14 13:35:26 +10:00
|
|
|
import androidx.compose.ui.Modifier
|
2025-05-01 19:32:28 +10:00
|
|
|
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
|
2025-05-01 19:32:28 +10:00
|
|
|
import androidx.compose.ui.text.style.TextOverflow
|
2025-04-14 13:35:26 +10:00
|
|
|
import androidx.compose.ui.unit.dp
|
2025-04-15 14:19:17 +10:00
|
|
|
import moe.lava.banksia.api.ptv.structures.ComposableIcon
|
2025-04-29 20:40:45 +10:00
|
|
|
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
2025-05-01 19:32:28 +10:00
|
|
|
import kotlin.coroutines.cancellation.CancellationException
|
|
|
|
|
import kotlin.math.pow
|
2025-04-14 13:35:26 +10:00
|
|
|
|
2025-05-01 19:32:28 +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,
|
2025-05-01 19:32:28 +10:00
|
|
|
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),
|
2025-05-01 19:32:28 +10:00
|
|
|
shadowElevation = 6.dp, // Elevation level 3
|
2025-04-14 13:35:26 +10:00
|
|
|
inputField = {
|
2025-05-01 19:32:28 +10:00
|
|
|
var backProgress by remember { mutableFloatStateOf(1f) }
|
|
|
|
|
var backEdgeIsLeft by remember { mutableStateOf<Boolean?>(null) }
|
2025-05-01 19:57:35 +10:00
|
|
|
val routeInfoOpacity by animateFloatAsState((1f - backProgress).pow(3))
|
2025-05-01 19:32:28 +10:00
|
|
|
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-05-01 19:32:28 +10:00
|
|
|
|
2025-07-28 01:39:31 +10:00
|
|
|
PredictiveBackHandler(enabled = selectedRoute != null) { progress ->
|
2025-05-01 19:32:28 +10:00
|
|
|
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,
|
2025-05-01 19:32:28 +10:00
|
|
|
modifier = Modifier
|
2025-05-01 19:57:35 +10:00
|
|
|
.alpha(1f - routeInfoOpacity)
|
2025-05-01 19:32:28 +10:00
|
|
|
.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-05-01 19:32:28 +10:00
|
|
|
}
|
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 {
|
2025-05-01 19:32:28 +10:00
|
|
|
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
|
|
|
}
|
2025-05-01 19:57:35 +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,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|