refactor: split out searchstate
This commit is contained in:
parent
c26e522a2e
commit
c526269e5d
5 changed files with 153 additions and 94 deletions
|
|
@ -72,9 +72,9 @@ fun App(
|
|||
viewModel.bindTracker(locationTracker)
|
||||
scope.launch { locationTracker.startTracking() }
|
||||
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val infoState by viewModel.infoState.collectAsStateWithLifecycle()
|
||||
val mapState by viewModel.mapState.collectAsStateWithLifecycle()
|
||||
val searchState by viewModel.searchState.collectAsStateWithLifecycle()
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(
|
||||
|
|
@ -100,7 +100,6 @@ fun App(
|
|||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
}
|
||||
|
||||
var searchTextState by rememberSaveable { mutableStateOf("") }
|
||||
var searchExpandedState by rememberSaveable { mutableStateOf(false) }
|
||||
var sheetSwipeEnabled by rememberSaveable { mutableStateOf(true) }
|
||||
var handleHeight by remember { mutableStateOf(0.dp) }
|
||||
|
|
@ -145,16 +144,14 @@ fun App(
|
|||
polylines = mapState.polylines,
|
||||
)
|
||||
Searcher(
|
||||
routes = state.routes,
|
||||
state = searchState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
expanded = searchExpandedState,
|
||||
onExpandedChange = {
|
||||
searchExpandedState = it
|
||||
if (it)
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
},
|
||||
text = searchTextState,
|
||||
onTextChange = { searchTextState = it },
|
||||
onRouteChange = { viewModel.switchRoute(it) }
|
||||
)
|
||||
|
||||
PredictiveBackHandler(scaffoldState.bottomSheetState.currentValue != SheetValue.Hidden) { progress ->
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import kotlinx.datetime.Clock
|
|||
import kotlinx.datetime.Instant
|
||||
import moe.lava.banksia.api.ptv.PtvService
|
||||
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
||||
import moe.lava.banksia.api.ptv.structures.PtvStop
|
||||
import moe.lava.banksia.api.ptv.structures.PtvRouteType
|
||||
import moe.lava.banksia.api.ptv.structures.getProperties
|
||||
import moe.lava.banksia.log
|
||||
import moe.lava.banksia.native.maps.CameraPosition
|
||||
|
|
@ -27,19 +27,18 @@ import moe.lava.banksia.native.maps.Point
|
|||
import moe.lava.banksia.native.maps.Polyline
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
import moe.lava.banksia.ui.state.MapState
|
||||
import moe.lava.banksia.ui.state.SearchState
|
||||
import moe.lava.banksia.util.BoxedValue
|
||||
import moe.lava.banksia.util.BoxedValue.Companion.box
|
||||
|
||||
sealed class BanksiaEvent {}
|
||||
sealed class BanksiaEvent {
|
||||
data class SelectRoute(val id: Int?) : BanksiaEvent()
|
||||
data class SelectStop(val routeType: PtvRouteType, val stopId: Int?) : BanksiaEvent()
|
||||
|
||||
data class BanksiaViewState(
|
||||
val routes: List<PtvRoute> = listOf(),
|
||||
)
|
||||
data class SearchUpdate(val text: String) : BanksiaEvent()
|
||||
}
|
||||
|
||||
class BanksiaViewModel : ViewModel() {
|
||||
private val iState = MutableStateFlow(BanksiaViewState())
|
||||
val state = iState.asStateFlow()
|
||||
|
||||
private val iInfoState = MutableStateFlow<InfoPanelState>(InfoPanelState.None)
|
||||
val infoState = iInfoState.asStateFlow()
|
||||
|
||||
|
|
@ -48,17 +47,26 @@ class BanksiaViewModel : ViewModel() {
|
|||
private val iCameraChangeEmitter = MutableSharedFlow<BoxedValue<CameraPosition>>()
|
||||
val cameraChangeEmitter = iCameraChangeEmitter.asSharedFlow()
|
||||
|
||||
private val iSearchState = MutableStateFlow(SearchState())
|
||||
val searchState = iSearchState.asStateFlow()
|
||||
|
||||
private val ptvService = PtvService()
|
||||
private var locationTrackerJob: Job? = null
|
||||
private var lastKnownLocation: Point? = null
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
requestRoutes()
|
||||
}
|
||||
viewModelScope.launch { searchUpdate("") }
|
||||
}
|
||||
|
||||
fun handleEvent(event: BanksiaEvent) {}
|
||||
fun handleEvent(event: BanksiaEvent) {
|
||||
viewModelScope.launch {
|
||||
when (event) {
|
||||
is BanksiaEvent.SelectRoute -> switchRoute(event.id)
|
||||
is BanksiaEvent.SelectStop -> switchStop(event.routeType, event.stopId)
|
||||
is BanksiaEvent.SearchUpdate -> searchUpdate(event.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bindTracker(locationTracker: LocationTracker) {
|
||||
locationTrackerJob = locationTracker.getLocationsFlow()
|
||||
|
|
@ -79,43 +87,57 @@ class BanksiaViewModel : ViewModel() {
|
|||
lastKnownLocation = location
|
||||
}
|
||||
|
||||
private suspend fun requestRoutes() {
|
||||
val routes = ptvService.routes().sortedWith(
|
||||
compareBy(
|
||||
{ it.gtfsSubType()?.ordinal },
|
||||
{ it.routeNumber.toIntOrNull() },
|
||||
{ it.routeName }
|
||||
)
|
||||
)
|
||||
iState.update { it.copy(routes = routes) }
|
||||
}
|
||||
|
||||
fun switchRoute(route: PtvRoute?) {
|
||||
iMapState.update { MapState() }
|
||||
iInfoState.update {
|
||||
if (route == null)
|
||||
InfoPanelState.None
|
||||
else
|
||||
InfoPanelState.Route(
|
||||
name = route.routeName,
|
||||
type = route.routeType,
|
||||
private suspend fun searchUpdate(text: String) {
|
||||
val entries = ptvService.routes()
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it.gtfsSubType()?.ordinal },
|
||||
{ it.routeNumber.toIntOrNull() },
|
||||
{ it.routeName }
|
||||
)
|
||||
}
|
||||
)
|
||||
.filter { it.routeNumber.contains(text) || it.routeName.lowercase().contains(text.lowercase()) }
|
||||
.map { route ->
|
||||
val (main, sub) = if (route.routeNumber.isNotEmpty()) {
|
||||
route.routeNumber to route.routeName
|
||||
} else {
|
||||
route.routeName to null
|
||||
}
|
||||
|
||||
if (route != null) {
|
||||
viewModelScope.launch { buildPolylines(route) }
|
||||
viewModelScope.launch { buildStops(route) }
|
||||
// viewModelScope.launch { buildDepartures() }
|
||||
// viewModelScope.launch { buildRuns() }
|
||||
}
|
||||
SearchState.SearchEntry(main, sub, route.routeId, route.routeType)
|
||||
}
|
||||
|
||||
iSearchState.update { SearchState(entries, text) }
|
||||
}
|
||||
|
||||
// [TODO]: Cleanup
|
||||
suspend fun switchStop(stop: PtvStop?) {
|
||||
if (stop == null) {
|
||||
private suspend fun switchRoute(routeId: Int?) {
|
||||
iMapState.update { MapState() }
|
||||
if (routeId == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
return
|
||||
}
|
||||
|
||||
val route = ptvService.route(routeId)
|
||||
iInfoState.update {
|
||||
InfoPanelState.Route(
|
||||
name = route.routeName,
|
||||
type = route.routeType,
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch { buildPolylines(route) }
|
||||
viewModelScope.launch { buildStops(route) }
|
||||
// viewModelScope.launch { buildDepartures() }
|
||||
// viewModelScope.launch { buildRuns() }
|
||||
}
|
||||
|
||||
// [TODO]: Cleanup
|
||||
private suspend fun switchStop(routeType: PtvRouteType, stopId: Int?) {
|
||||
if (stopId == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
return
|
||||
}
|
||||
val stop = ptvService.stop(routeType, stopId)
|
||||
val split = stop.stopName.split("/")
|
||||
val name = split[0]
|
||||
val subname = split.getOrNull(1)
|
||||
|
|
@ -229,7 +251,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
type = MarkerType.GENERIC_STOP,
|
||||
colour = colour,
|
||||
onClick = {
|
||||
viewModelScope.launch { switchStop(stop) }
|
||||
viewModelScope.launch { switchStop(route.routeType, stop.stopId) }
|
||||
false
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -21,28 +19,20 @@ import androidx.compose.material3.SearchBarDefaults
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.api.ptv.structures.ComposableIcon
|
||||
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
||||
import kotlin.math.pow
|
||||
import moe.lava.banksia.api.ptv.structures.ComposableRouteIcon
|
||||
import moe.lava.banksia.ui.state.SearchState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Searcher(
|
||||
routes: List<PtvRoute>,
|
||||
state: SearchState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
text: String,
|
||||
onTextChange: (String) -> Unit,
|
||||
onRouteChange: (PtvRoute?) -> Unit,
|
||||
) {
|
||||
val animatedPadding by animateDpAsState(
|
||||
if (expanded) {
|
||||
|
|
@ -61,32 +51,20 @@ fun Searcher(
|
|||
.padding(horizontal = animatedPadding),
|
||||
shadowElevation = 6.dp, // Elevation level 3
|
||||
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)
|
||||
val slidePadding = when (backEdgeIsLeft) {
|
||||
true -> PaddingValues(start = slideState)
|
||||
false -> PaddingValues(end = slideState)
|
||||
null -> PaddingValues()
|
||||
}
|
||||
|
||||
SearchBarDefaults.InputField(
|
||||
modifier = Modifier
|
||||
.alpha(1f - routeInfoOpacity)
|
||||
.padding(horizontal = 20.dp - animatedPadding),
|
||||
query = text,
|
||||
onQueryChange = onTextChange,
|
||||
modifier = Modifier.padding(horizontal = 20.dp - animatedPadding),
|
||||
query = state.text,
|
||||
onQueryChange = { onEvent(BanksiaEvent.SearchUpdate(it)) },
|
||||
onSearch = {},
|
||||
expanded = expanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
leadingIcon = { Icon(Icons.Default.Search, null) },
|
||||
trailingIcon = {
|
||||
if (expanded && text.isNotEmpty())
|
||||
if (expanded && state.text.isNotEmpty())
|
||||
Icon(
|
||||
imageVector = Icons.Default.Clear,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable { onTextChange("") }
|
||||
modifier = Modifier.clickable { onEvent(BanksiaEvent.SearchUpdate("")) }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -95,27 +73,20 @@ fun Searcher(
|
|||
onExpandedChange = onExpandedChange,
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
for (route in routes) {
|
||||
if (!route.routeNumber.contains(text) &&
|
||||
!route.routeName.lowercase().contains(text.lowercase()))
|
||||
continue
|
||||
for (entry in state.entries) {
|
||||
item {
|
||||
ListItem(
|
||||
headlineContent = { Text(route.routeNumber.ifEmpty { route.routeName }) },
|
||||
supportingContent = {
|
||||
if (route.routeNumber.isNotEmpty()) {
|
||||
Text(route.routeName)
|
||||
}
|
||||
},
|
||||
leadingContent = { route.routeType.ComposableIcon() },
|
||||
headlineContent = { Text(entry.mainText) },
|
||||
supportingContent = { entry.subText?.let { Text(it) } },
|
||||
leadingContent = { ComposableRouteIcon(entry.routeType) },
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.clickable {
|
||||
onTextChange("")
|
||||
onExpandedChange(false)
|
||||
onRouteChange(route)
|
||||
onEvent(BanksiaEvent.SearchUpdate(""))
|
||||
onEvent(BanksiaEvent.SelectRoute(entry.routeId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package moe.lava.banksia.ui.state
|
||||
|
||||
import moe.lava.banksia.api.ptv.structures.PtvRouteType
|
||||
|
||||
data class SearchState(
|
||||
val entries: List<SearchEntry> = listOf(),
|
||||
val text: String = "",
|
||||
) {
|
||||
data class SearchEntry(
|
||||
val mainText: String,
|
||||
val subText: String?,
|
||||
val routeId: Int,
|
||||
val routeType: PtvRouteType,
|
||||
)
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ object Responses {
|
|||
@Serializable
|
||||
data class PtvRoutesResponse(val routes: List<PtvRoute>)
|
||||
|
||||
@Serializable
|
||||
data class PtvStopResponse(val stop: PtvStop)
|
||||
@Serializable
|
||||
data class PtvStopsResponse(val stops: List<PtvStop>)
|
||||
|
||||
|
|
@ -43,6 +45,8 @@ class PtvService {
|
|||
class PtvCache(
|
||||
private val service: PtvService,
|
||||
private val directions: HashMap<Pair<Int, Int>, PtvDirection> = HashMap(),
|
||||
private val routes: HashMap<Int, PtvRoute> = HashMap(),
|
||||
private val stops: HashMap<Int, PtvStop> = HashMap(),
|
||||
) {
|
||||
suspend fun direction(directionID: Int, routeID: Int): PtvDirection? {
|
||||
val ret = directions[Pair(directionID, routeID)]
|
||||
|
|
@ -54,6 +58,23 @@ class PtvService {
|
|||
|
||||
return ret ?: directions[Pair(directionID, routeID)]
|
||||
}
|
||||
|
||||
fun setRoutes(routes: Iterable<PtvRoute>) {
|
||||
routes.forEach {
|
||||
this.routes[it.routeId] = it
|
||||
}
|
||||
}
|
||||
|
||||
fun getRoute(routeId: Int) = routes[routeId]
|
||||
fun getRoutes() = routes.values.toList()
|
||||
|
||||
fun addStops(stops: Iterable<PtvStop>) {
|
||||
stops.forEach {
|
||||
this.stops[it.stopId] = it
|
||||
}
|
||||
}
|
||||
|
||||
fun getStop(stopId: Int) = stops[stopId]
|
||||
}
|
||||
|
||||
val cache = PtvCache(this)
|
||||
|
|
@ -83,17 +104,28 @@ class PtvService {
|
|||
}
|
||||
|
||||
suspend fun route(id: Int, includeGeopath: Boolean = false): PtvRoute {
|
||||
val cached = cache.getRoute(id)
|
||||
// TODO: im braindead so clean this up later
|
||||
if (cached != null && (!includeGeopath || (includeGeopath && cached.geopath.isNotEmpty())))
|
||||
return cached
|
||||
|
||||
val response: Responses.PtvRouteResponse = client.get("routes") {
|
||||
url {
|
||||
appendPathSegments(id.toString())
|
||||
parameters.append("include_geopath", if (includeGeopath) "true" else "false")
|
||||
}
|
||||
}.body()
|
||||
cache.setRoutes(listOf(response.route))
|
||||
return response.route
|
||||
}
|
||||
|
||||
suspend fun routes(): List<PtvRoute> {
|
||||
val cached = cache.getRoutes()
|
||||
if (cached.isNotEmpty())
|
||||
return cached
|
||||
|
||||
val response: Responses.PtvRoutesResponse = client.get("routes").body()
|
||||
cache.setRoutes(response.routes)
|
||||
return response.routes
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +140,29 @@ class PtvService {
|
|||
)
|
||||
}
|
||||
}.body()
|
||||
return response.stops
|
||||
val stops = response.stops
|
||||
cache.addStops(stops)
|
||||
return stops
|
||||
}
|
||||
|
||||
suspend fun stop(routeType: PtvRouteType, stopId: Int): PtvStop {
|
||||
val cached = cache.getStop(stopId)
|
||||
if (cached != null)
|
||||
return cached
|
||||
|
||||
val response: Responses.PtvStopResponse = client.get() {
|
||||
url {
|
||||
appendPathSegments(
|
||||
"stops",
|
||||
stopId.toString(),
|
||||
"route_type",
|
||||
routeType.ordinal.toString(),
|
||||
)
|
||||
}
|
||||
}.body()
|
||||
val stop = response.stop
|
||||
cache.addStops(listOf(stop))
|
||||
return stop
|
||||
}
|
||||
|
||||
suspend fun directionsByRoute(routeId: Int): List<PtvDirection> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue