refactor: split up state for info panel
This commit is contained in:
parent
1fa2a9bc10
commit
b376e7da5b
8 changed files with 205 additions and 168 deletions
|
|
@ -48,8 +48,9 @@ import moe.lava.banksia.native.maps.getScreenHeight
|
||||||
import moe.lava.banksia.resources.Res
|
import moe.lava.banksia.resources.Res
|
||||||
import moe.lava.banksia.resources.my_location_24
|
import moe.lava.banksia.resources.my_location_24
|
||||||
import moe.lava.banksia.ui.BanksiaViewModel
|
import moe.lava.banksia.ui.BanksiaViewModel
|
||||||
|
import moe.lava.banksia.ui.InfoPanel
|
||||||
import moe.lava.banksia.ui.Searcher
|
import moe.lava.banksia.ui.Searcher
|
||||||
import moe.lava.banksia.ui.StopInfoPanel
|
import moe.lava.banksia.ui.state.InfoPanelState
|
||||||
import org.jetbrains.compose.resources.painterResource
|
import org.jetbrains.compose.resources.painterResource
|
||||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
@ -72,6 +73,7 @@ fun App(
|
||||||
scope.launch { locationTracker.startTracking() }
|
scope.launch { locationTracker.startTracking() }
|
||||||
|
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val infoState by viewModel.infoState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||||
bottomSheetState = rememberStandardBottomSheetState(
|
bottomSheetState = rememberStandardBottomSheetState(
|
||||||
|
|
@ -90,9 +92,8 @@ fun App(
|
||||||
(getScreenHeight() - scaffoldOffset - WindowInsets.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0)
|
(getScreenHeight() - scaffoldOffset - WindowInsets.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0)
|
||||||
} else 0
|
} else 0
|
||||||
|
|
||||||
LaunchedEffect(state.stopState) {
|
LaunchedEffect(infoState) {
|
||||||
val isShown = state.stopState != null
|
if (infoState !is InfoPanelState.None)
|
||||||
if (isShown)
|
|
||||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||||
else
|
else
|
||||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||||
|
|
@ -111,9 +112,11 @@ fun App(
|
||||||
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
sheetContent = {
|
sheetContent = {
|
||||||
state.stopState?.let { stopState ->
|
InfoPanel(
|
||||||
StopInfoPanel(stopState) { peekHeight = it }
|
state = infoState,
|
||||||
}
|
onEvent = viewModel::handleEvent,
|
||||||
|
onPeekHeightChange = { peekHeight = it },
|
||||||
|
)
|
||||||
},
|
},
|
||||||
sheetDragHandle = {
|
sheetDragHandle = {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
@ -141,7 +144,6 @@ fun App(
|
||||||
polylines = state.polylines,
|
polylines = state.polylines,
|
||||||
)
|
)
|
||||||
Searcher(
|
Searcher(
|
||||||
selectedRoute = state.routeState?.route,
|
|
||||||
routes = state.routes,
|
routes = state.routes,
|
||||||
expanded = searchExpandedState,
|
expanded = searchExpandedState,
|
||||||
onExpandedChange = {
|
onExpandedChange = {
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,10 @@ fun PtvRouteType.getProperties(): RouteTypeProperties {
|
||||||
}
|
}
|
||||||
return RouteTypeProperties(colour, drawable, background, icon)
|
return RouteTypeProperties(colour, drawable, background, icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PtvRouteType.ComposableIcon() {
|
fun ComposableRouteIcon(routeType: PtvRouteType) {
|
||||||
val properties = this.getProperties()
|
val properties = routeType.getProperties()
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(properties.icon),
|
painter = painterResource(properties.icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|
@ -54,3 +55,6 @@ fun PtvRouteType.ComposableIcon() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
inline fun PtvRouteType.ComposableIcon() = ComposableRouteIcon(this)
|
||||||
|
|
@ -4,10 +4,8 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dev.icerock.moko.geo.LocationTracker
|
import dev.icerock.moko.geo.LocationTracker
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -27,33 +25,24 @@ import moe.lava.banksia.native.maps.Marker
|
||||||
import moe.lava.banksia.native.maps.MarkerType
|
import moe.lava.banksia.native.maps.MarkerType
|
||||||
import moe.lava.banksia.native.maps.Point
|
import moe.lava.banksia.native.maps.Point
|
||||||
import moe.lava.banksia.native.maps.Polyline
|
import moe.lava.banksia.native.maps.Polyline
|
||||||
|
import moe.lava.banksia.ui.state.InfoPanelState
|
||||||
import moe.lava.banksia.util.BoxedValue
|
import moe.lava.banksia.util.BoxedValue
|
||||||
import moe.lava.banksia.util.BoxedValue.Companion.box
|
import moe.lava.banksia.util.BoxedValue.Companion.box
|
||||||
|
|
||||||
data class RouteState(
|
sealed class BanksiaEvent {}
|
||||||
val route: PtvRoute,
|
|
||||||
val stops: List<PtvStop>? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class StopState(
|
|
||||||
val stop: PtvStop,
|
|
||||||
// val departures: List<PtvDeparture>? = null,
|
|
||||||
val departures: List<Pair<String, String>>? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class BanksiaViewState(
|
data class BanksiaViewState(
|
||||||
val routeState: RouteState? = null,
|
|
||||||
val stopState: StopState? = null,
|
|
||||||
|
|
||||||
val routes: List<PtvRoute> = listOf(),
|
val routes: List<PtvRoute> = listOf(),
|
||||||
|
|
||||||
val markers: List<Marker> = listOf(),
|
val markers: List<Marker> = listOf(),
|
||||||
val polylines: List<Polyline> = listOf(),
|
val polylines: List<Polyline> = listOf(),
|
||||||
)
|
)
|
||||||
|
|
||||||
class BanksiaViewModel : ViewModel() {
|
class BanksiaViewModel : ViewModel() {
|
||||||
private val iState = MutableStateFlow(BanksiaViewState())
|
private val iState = MutableStateFlow(BanksiaViewState())
|
||||||
val state: StateFlow<BanksiaViewState> = iState.asStateFlow()
|
val state = iState.asStateFlow()
|
||||||
|
|
||||||
|
private val iInfoState = MutableStateFlow<InfoPanelState>(InfoPanelState.None)
|
||||||
|
val infoState = iInfoState.asStateFlow()
|
||||||
|
|
||||||
private val ptvService = PtvService()
|
private val ptvService = PtvService()
|
||||||
private var locationTrackerJob: Job? = null
|
private var locationTrackerJob: Job? = null
|
||||||
|
|
@ -68,6 +57,8 @@ class BanksiaViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleEvent(event: BanksiaEvent) {}
|
||||||
|
|
||||||
fun bindTracker(locationTracker: LocationTracker) {
|
fun bindTracker(locationTracker: LocationTracker) {
|
||||||
locationTrackerJob = locationTracker.getLocationsFlow()
|
locationTrackerJob = locationTracker.getLocationsFlow()
|
||||||
.onEach { lastKnownLocation = Point(it.latitude, it.longitude) }
|
.onEach { lastKnownLocation = Point(it.latitude, it.longitude) }
|
||||||
|
|
@ -98,36 +89,47 @@ class BanksiaViewModel : ViewModel() {
|
||||||
iState.update { it.copy(routes = routes) }
|
iState.update { it.copy(routes = routes) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchRoute(newRoute: PtvRoute?) {
|
fun switchRoute(route: PtvRoute?) {
|
||||||
val routeState = newRoute?.let { RouteState(it) }
|
|
||||||
if (iState.value.routeState == routeState)
|
|
||||||
return
|
|
||||||
|
|
||||||
iState.update {
|
iState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
routeState = routeState,
|
|
||||||
stopState = null,
|
|
||||||
markers = listOf(),
|
markers = listOf(),
|
||||||
polylines = listOf(),
|
polylines = listOf(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
iInfoState.update {
|
||||||
|
if (route == null)
|
||||||
|
InfoPanelState.None
|
||||||
|
else
|
||||||
|
InfoPanelState.Route(
|
||||||
|
name = route.routeName,
|
||||||
|
type = route.routeType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (routeState != null) {
|
if (route != null) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch { buildPolylines(route) }
|
||||||
async { buildPolylines() }
|
viewModelScope.launch { buildStops(route) }
|
||||||
async { buildMarkers() }
|
// viewModelScope.launch { buildDepartures() }
|
||||||
}
|
// viewModelScope.launch { buildRuns() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// [TODO]: Cleanup
|
// [TODO]: Cleanup
|
||||||
suspend fun switchStop(stop: PtvStop?) {
|
suspend fun switchStop(stop: PtvStop?) {
|
||||||
iState.update { state ->
|
if (stop == null) {
|
||||||
state.copy(stopState = stop?.let { StopState(it) })
|
iInfoState.update { InfoPanelState.None }
|
||||||
}
|
|
||||||
|
|
||||||
if (stop == null)
|
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
val split = stop.stopName.split("/")
|
||||||
|
val name = split[0]
|
||||||
|
val subname = split.getOrNull(1)
|
||||||
|
iInfoState.update {
|
||||||
|
InfoPanelState.Stop(
|
||||||
|
id = stop.stopId,
|
||||||
|
name = name,
|
||||||
|
subname = subname,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val res = ptvService.departures(stop.routeType, stop.stopId)
|
val res = ptvService.departures(stop.routeType, stop.stopId)
|
||||||
// Map<
|
// Map<
|
||||||
|
|
@ -155,18 +157,19 @@ class BanksiaViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
val departures = timetable.values.sortedBy { it.first }.map { (name, list) ->
|
val departures = timetable.values.sortedBy { it.first }.map { (name, list) ->
|
||||||
if (list.isEmpty())
|
if (list.isEmpty())
|
||||||
Pair(name, "No departures")
|
InfoPanelState.Stop.Departure(name, "No departures")
|
||||||
else
|
else
|
||||||
Pair(name, list.joinToString(" | "))
|
InfoPanelState.Stop.Departure(name, list.joinToString(" | "))
|
||||||
}
|
}
|
||||||
iState.update {
|
iInfoState.update {
|
||||||
it.copy(stopState = it.stopState?.copy(departures = departures))
|
if (it !is InfoPanelState.Stop)
|
||||||
|
it
|
||||||
|
else
|
||||||
|
it.copy(departures = departures)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildPolylines() {
|
private suspend fun buildPolylines(route: PtvRoute) {
|
||||||
val route = iState.value.routeState?.route ?: return
|
|
||||||
|
|
||||||
val routeWithGeo = if (route.geopath.isEmpty())
|
val routeWithGeo = if (route.geopath.isEmpty())
|
||||||
ptvService.route(route.routeId, true)
|
ptvService.route(route.routeId, true)
|
||||||
else
|
else
|
||||||
|
|
@ -198,9 +201,25 @@ class BanksiaViewModel : ViewModel() {
|
||||||
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun buildMarkers() {
|
// private suspend fun buildDepartures(route: PtvRoute) {
|
||||||
val route = iState.value.routeState?.route ?: return
|
// val directions = ptvService.directionsByRoute(route.routeId)
|
||||||
|
//
|
||||||
|
// iState.update {
|
||||||
|
// it.copy(routeState = it.routeState?.copy(directions = directions))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private suspend fun buildRuns() {
|
||||||
|
// val route = iState.value.routeState?.route ?: return
|
||||||
|
//
|
||||||
|
// val directions = ptvService.directionsByRoute(route.routeId)
|
||||||
|
//
|
||||||
|
// iState.update {
|
||||||
|
// it.copy(routeState = it.routeState?.copy(directions = directions))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
private suspend fun buildStops(route: PtvRoute) {
|
||||||
val stops = ptvService.stopsByRoute(route.routeId, route.routeType)
|
val stops = ptvService.stopsByRoute(route.routeId, route.routeType)
|
||||||
val markers = mutableListOf<Marker>()
|
val markers = mutableListOf<Marker>()
|
||||||
val colour = route.routeType.getProperties().colour
|
val colour = route.routeType.getProperties().colour
|
||||||
|
|
@ -222,12 +241,7 @@ class BanksiaViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
iState.update {
|
iState.update { it.copy(markers = markers) }
|
||||||
it.copy(
|
|
||||||
routeState = it.routeState?.copy(stops = stops),
|
|
||||||
markers = markers
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildBounds(points: List<Point>): CameraPositionBounds {
|
private fun buildBounds(points: List<Point>): CameraPositionBounds {
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,19 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.coerceAtMost
|
import androidx.compose.ui.unit.coerceAtMost
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import moe.lava.banksia.api.ptv.structures.ComposableRouteIcon
|
||||||
|
import moe.lava.banksia.ui.state.InfoPanelState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StopInfoPanel(
|
fun InfoPanel(
|
||||||
stopState: StopState,
|
state: InfoPanelState,
|
||||||
|
onEvent: (BanksiaEvent) -> Unit,
|
||||||
onPeekHeightChange: (Dp) -> Unit,
|
onPeekHeightChange: (Dp) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
if (state is InfoPanelState.None)
|
||||||
|
return
|
||||||
|
|
||||||
val localDensity = LocalDensity.current
|
val localDensity = LocalDensity.current
|
||||||
val (stop, departures) = stopState
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -49,36 +54,13 @@ fun StopInfoPanel(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
Column(Modifier.fillMaxWidth()) {
|
when (state) {
|
||||||
val split = stop.stopName.split("/")
|
is InfoPanelState.Route -> RouteInfoPanel(state, onEvent)
|
||||||
val mainName = split[0]
|
is InfoPanelState.Stop -> StopInfoPanel(state, onEvent)
|
||||||
val subName = split.getOrNull(1)
|
else -> throw UnsupportedOperationException()
|
||||||
Text(
|
|
||||||
mainName,
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
textAlign = TextAlign.Start
|
|
||||||
)
|
|
||||||
if (subName != null)
|
|
||||||
Text(
|
|
||||||
"/ $subName",
|
|
||||||
modifier = Modifier.padding(start = 5.dp),
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
color = Color.Gray,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
textAlign = TextAlign.Start
|
|
||||||
)
|
|
||||||
departures?.let {
|
|
||||||
Spacer(Modifier.height(5.dp))
|
|
||||||
it.forEach { (name, formatted) ->
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
|
||||||
Text(formatted, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(horizontal = 5.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (departures == null)
|
|
||||||
|
if (state.loading)
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.width(32.dp).align(Alignment.CenterEnd)
|
modifier = Modifier.width(32.dp).align(Alignment.CenterEnd)
|
||||||
)
|
)
|
||||||
|
|
@ -86,3 +68,55 @@ fun StopInfoPanel(
|
||||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent))
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RouteInfoPanel(
|
||||||
|
state: InfoPanelState.Route,
|
||||||
|
onEvent: (BanksiaEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxWidth()) {
|
||||||
|
Row {
|
||||||
|
ComposableRouteIcon(state.type)
|
||||||
|
Text(
|
||||||
|
state.name,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StopInfoPanel(
|
||||||
|
state: InfoPanelState.Stop,
|
||||||
|
onEvent: (BanksiaEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
state.name,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
state.subname?.let {
|
||||||
|
Text(
|
||||||
|
"/ $it",
|
||||||
|
modifier = Modifier.padding(start = 5.dp),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = Color.Gray,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
|
state.departures?.let {
|
||||||
|
Spacer(Modifier.height(5.dp))
|
||||||
|
it.forEach { (name, formatted) ->
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||||
|
Text(formatted, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(horizontal = 5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,51 +5,38 @@ import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
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.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material.icons.Icons
|
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.Clear
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.SearchBar
|
import androidx.compose.material3.SearchBar
|
||||||
import androidx.compose.material3.SearchBarDefaults
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.backhandler.PredictiveBackHandler
|
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import moe.lava.banksia.api.ptv.structures.ComposableIcon
|
import moe.lava.banksia.api.ptv.structures.ComposableIcon
|
||||||
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Searcher(
|
fun Searcher(
|
||||||
selectedRoute: PtvRoute?,
|
|
||||||
routes: List<PtvRoute>,
|
routes: List<PtvRoute>,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onExpandedChange: (Boolean) -> Unit,
|
onExpandedChange: (Boolean) -> Unit,
|
||||||
|
|
@ -84,21 +71,7 @@ fun Searcher(
|
||||||
null -> PaddingValues()
|
null -> PaddingValues()
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
SearchBarDefaults.InputField(
|
SearchBarDefaults.InputField(
|
||||||
enabled = selectedRoute == null,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.alpha(1f - routeInfoOpacity)
|
.alpha(1f - routeInfoOpacity)
|
||||||
.padding(horizontal = 20.dp - animatedPadding),
|
.padding(horizontal = 20.dp - animatedPadding),
|
||||||
|
|
@ -117,11 +90,6 @@ fun Searcher(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
LaunchedEffect(selectedRoute) {
|
|
||||||
backProgress = if (selectedRoute != null) 0f else 1f;
|
|
||||||
}
|
|
||||||
if (selectedRoute != null)
|
|
||||||
RouteInfo(routeInfoOpacity, slidePadding, onRouteChange, selectedRoute)
|
|
||||||
},
|
},
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onExpandedChange = onExpandedChange,
|
onExpandedChange = onExpandedChange,
|
||||||
|
|
@ -156,47 +124,3 @@ fun Searcher(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package moe.lava.banksia.ui.state
|
||||||
|
|
||||||
|
import moe.lava.banksia.api.ptv.structures.PtvRouteType
|
||||||
|
|
||||||
|
sealed class InfoPanelState {
|
||||||
|
abstract val loading: Boolean
|
||||||
|
|
||||||
|
data object None : InfoPanelState() {
|
||||||
|
override val loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Route(
|
||||||
|
val name: String,
|
||||||
|
val type: PtvRouteType,
|
||||||
|
) : InfoPanelState() {
|
||||||
|
override val loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Stop(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val subname: String? = null,
|
||||||
|
val departures: List<Departure>? = null,
|
||||||
|
) : InfoPanelState() {
|
||||||
|
override val loading: Boolean
|
||||||
|
get() = departures == null
|
||||||
|
|
||||||
|
data class Departure(val directionName: String, val formattedTimes: String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,7 @@ kotlin {
|
||||||
implementation(libs.ktor.client.contentnegotiation)
|
implementation(libs.ktor.client.contentnegotiation)
|
||||||
implementation(libs.ktor.serialization.kotlinx.json)
|
implementation(libs.ktor.serialization.kotlinx.json)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
}
|
}
|
||||||
iosMain.dependencies {
|
iosMain.dependencies {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package moe.lava.banksia.api.ptv.structures
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
data class PtvVehiclePosition(
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val easting: Double?,
|
||||||
|
val northing: Double?,
|
||||||
|
val direction: String?,
|
||||||
|
val bearing: Double?,
|
||||||
|
val supplier: String?,
|
||||||
|
@SerialName("datetime_utc") val datetimeUtc: Instant?,
|
||||||
|
@SerialName("expiry_time") val expiryTime: Instant?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PtvRun(
|
||||||
|
@SerialName("run_ref") val runRef: String,
|
||||||
|
@SerialName("route_id") val routeId: Int,
|
||||||
|
@SerialName("route_type") val routeType: PtvRouteType,
|
||||||
|
@SerialName("final_stop_id") val finalStopId: Int,
|
||||||
|
@SerialName("destination_name") val destinationName: String,
|
||||||
|
@SerialName("direction_id") val directionId: Int,
|
||||||
|
@SerialName("status") val status: String,
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue