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.my_location_24
|
||||
import moe.lava.banksia.ui.BanksiaViewModel
|
||||
import moe.lava.banksia.ui.InfoPanel
|
||||
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.ui.tooling.preview.Preview
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
|
@ -72,6 +73,7 @@ fun App(
|
|||
scope.launch { locationTracker.startTracking() }
|
||||
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val infoState by viewModel.infoState.collectAsStateWithLifecycle()
|
||||
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(
|
||||
|
|
@ -90,9 +92,8 @@ fun App(
|
|||
(getScreenHeight() - scaffoldOffset - WindowInsets.safeDrawing.getBottom(LocalDensity.current)).coerceAtLeast(0)
|
||||
} else 0
|
||||
|
||||
LaunchedEffect(state.stopState) {
|
||||
val isShown = state.stopState != null
|
||||
if (isShown)
|
||||
LaunchedEffect(infoState) {
|
||||
if (infoState !is InfoPanelState.None)
|
||||
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
|
||||
else
|
||||
scope.launch { scaffoldState.bottomSheetState.hide() }
|
||||
|
|
@ -111,9 +112,11 @@ fun App(
|
|||
sheetPeekHeight = (handleHeight + peekHeight) * peekHeightMultiplier,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
sheetContent = {
|
||||
state.stopState?.let { stopState ->
|
||||
StopInfoPanel(stopState) { peekHeight = it }
|
||||
}
|
||||
InfoPanel(
|
||||
state = infoState,
|
||||
onEvent = viewModel::handleEvent,
|
||||
onPeekHeightChange = { peekHeight = it },
|
||||
)
|
||||
},
|
||||
sheetDragHandle = {
|
||||
val density = LocalDensity.current
|
||||
|
|
@ -141,7 +144,6 @@ fun App(
|
|||
polylines = state.polylines,
|
||||
)
|
||||
Searcher(
|
||||
selectedRoute = state.routeState?.route,
|
||||
routes = state.routes,
|
||||
expanded = searchExpandedState,
|
||||
onExpandedChange = {
|
||||
|
|
|
|||
|
|
@ -42,9 +42,10 @@ fun PtvRouteType.getProperties(): RouteTypeProperties {
|
|||
}
|
||||
return RouteTypeProperties(colour, drawable, background, icon)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PtvRouteType.ComposableIcon() {
|
||||
val properties = this.getProperties()
|
||||
fun ComposableRouteIcon(routeType: PtvRouteType) {
|
||||
val properties = routeType.getProperties()
|
||||
Image(
|
||||
painter = painterResource(properties.icon),
|
||||
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 dev.icerock.moko.geo.LocationTracker
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
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.Point
|
||||
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.Companion.box
|
||||
|
||||
data class RouteState(
|
||||
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,
|
||||
)
|
||||
sealed class BanksiaEvent {}
|
||||
|
||||
data class BanksiaViewState(
|
||||
val routeState: RouteState? = null,
|
||||
val stopState: StopState? = null,
|
||||
|
||||
val routes: List<PtvRoute> = listOf(),
|
||||
|
||||
val markers: List<Marker> = listOf(),
|
||||
val polylines: List<Polyline> = listOf(),
|
||||
)
|
||||
|
||||
class BanksiaViewModel : ViewModel() {
|
||||
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 var locationTrackerJob: Job? = null
|
||||
|
|
@ -68,6 +57,8 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun handleEvent(event: BanksiaEvent) {}
|
||||
|
||||
fun bindTracker(locationTracker: LocationTracker) {
|
||||
locationTrackerJob = locationTracker.getLocationsFlow()
|
||||
.onEach { lastKnownLocation = Point(it.latitude, it.longitude) }
|
||||
|
|
@ -98,36 +89,47 @@ class BanksiaViewModel : ViewModel() {
|
|||
iState.update { it.copy(routes = routes) }
|
||||
}
|
||||
|
||||
fun switchRoute(newRoute: PtvRoute?) {
|
||||
val routeState = newRoute?.let { RouteState(it) }
|
||||
if (iState.value.routeState == routeState)
|
||||
return
|
||||
|
||||
fun switchRoute(route: PtvRoute?) {
|
||||
iState.update {
|
||||
it.copy(
|
||||
routeState = routeState,
|
||||
stopState = null,
|
||||
markers = listOf(),
|
||||
polylines = listOf(),
|
||||
)
|
||||
}
|
||||
|
||||
if (routeState != null) {
|
||||
viewModelScope.launch {
|
||||
async { buildPolylines() }
|
||||
async { buildMarkers() }
|
||||
iInfoState.update {
|
||||
if (route == null)
|
||||
InfoPanelState.None
|
||||
else
|
||||
InfoPanelState.Route(
|
||||
name = route.routeName,
|
||||
type = route.routeType,
|
||||
)
|
||||
}
|
||||
|
||||
if (route != null) {
|
||||
viewModelScope.launch { buildPolylines(route) }
|
||||
viewModelScope.launch { buildStops(route) }
|
||||
// viewModelScope.launch { buildDepartures() }
|
||||
// viewModelScope.launch { buildRuns() }
|
||||
}
|
||||
}
|
||||
|
||||
// [TODO]: Cleanup
|
||||
suspend fun switchStop(stop: PtvStop?) {
|
||||
iState.update { state ->
|
||||
state.copy(stopState = stop?.let { StopState(it) })
|
||||
}
|
||||
|
||||
if (stop == null)
|
||||
if (stop == null) {
|
||||
iInfoState.update { InfoPanelState.None }
|
||||
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)
|
||||
// Map<
|
||||
|
|
@ -155,18 +157,19 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
val departures = timetable.values.sortedBy { it.first }.map { (name, list) ->
|
||||
if (list.isEmpty())
|
||||
Pair(name, "No departures")
|
||||
InfoPanelState.Stop.Departure(name, "No departures")
|
||||
else
|
||||
Pair(name, list.joinToString(" | "))
|
||||
InfoPanelState.Stop.Departure(name, list.joinToString(" | "))
|
||||
}
|
||||
iState.update {
|
||||
it.copy(stopState = it.stopState?.copy(departures = departures))
|
||||
iInfoState.update {
|
||||
if (it !is InfoPanelState.Stop)
|
||||
it
|
||||
else
|
||||
it.copy(departures = departures)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun buildPolylines() {
|
||||
val route = iState.value.routeState?.route ?: return
|
||||
|
||||
private suspend fun buildPolylines(route: PtvRoute) {
|
||||
val routeWithGeo = if (route.geopath.isEmpty())
|
||||
ptvService.route(route.routeId, true)
|
||||
else
|
||||
|
|
@ -198,9 +201,25 @@ class BanksiaViewModel : ViewModel() {
|
|||
newCameraPosition?.let { iCameraChangeEmitter.emit(it.box()) }
|
||||
}
|
||||
|
||||
private suspend fun buildMarkers() {
|
||||
val route = iState.value.routeState?.route ?: return
|
||||
// private suspend fun buildDepartures(route: PtvRoute) {
|
||||
// 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 markers = mutableListOf<Marker>()
|
||||
val colour = route.routeType.getProperties().colour
|
||||
|
|
@ -222,12 +241,7 @@ class BanksiaViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
iState.update {
|
||||
it.copy(
|
||||
routeState = it.routeState?.copy(stops = stops),
|
||||
markers = markers
|
||||
)
|
||||
}
|
||||
iState.update { it.copy(markers = markers) }
|
||||
}
|
||||
|
||||
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.coerceAtMost
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.lava.banksia.api.ptv.structures.ComposableRouteIcon
|
||||
import moe.lava.banksia.ui.state.InfoPanelState
|
||||
|
||||
@Composable
|
||||
fun StopInfoPanel(
|
||||
stopState: StopState,
|
||||
fun InfoPanel(
|
||||
state: InfoPanelState,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
onPeekHeightChange: (Dp) -> Unit,
|
||||
) {
|
||||
if (state is InfoPanelState.None)
|
||||
return
|
||||
|
||||
val localDensity = LocalDensity.current
|
||||
val (stop, departures) = stopState
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
|
|
@ -49,26 +54,62 @@ fun StopInfoPanel(
|
|||
}
|
||||
) {
|
||||
Box {
|
||||
when (state) {
|
||||
is InfoPanelState.Route -> RouteInfoPanel(state, onEvent)
|
||||
is InfoPanelState.Stop -> StopInfoPanel(state, onEvent)
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
if (state.loading)
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.width(32.dp).align(Alignment.CenterEnd)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RouteInfoPanel(
|
||||
state: InfoPanelState.Route,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
val split = stop.stopName.split("/")
|
||||
val mainName = split[0]
|
||||
val subName = split.getOrNull(1)
|
||||
Row {
|
||||
ComposableRouteIcon(state.type)
|
||||
Text(
|
||||
mainName,
|
||||
state.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
if (subName != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StopInfoPanel(
|
||||
state: InfoPanelState.Stop,
|
||||
onEvent: (BanksiaEvent) -> Unit,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
"/ $subName",
|
||||
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
|
||||
)
|
||||
departures?.let {
|
||||
}
|
||||
state.departures?.let {
|
||||
Spacer(Modifier.height(5.dp))
|
||||
it.forEach { (name, formatted) ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
|
@ -78,11 +119,4 @@ fun StopInfoPanel(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (departures == null)
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.width(32.dp).align(Alignment.CenterEnd)
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeContent))
|
||||
}
|
||||
}
|
||||
|
|
@ -5,51 +5,38 @@ 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
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
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.structures.ComposableIcon
|
||||
import moe.lava.banksia.api.ptv.structures.PtvRoute
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.pow
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Searcher(
|
||||
selectedRoute: PtvRoute?,
|
||||
routes: List<PtvRoute>,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
|
|
@ -84,21 +71,7 @@ fun Searcher(
|
|||
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(
|
||||
enabled = selectedRoute == null,
|
||||
modifier = Modifier
|
||||
.alpha(1f - routeInfoOpacity)
|
||||
.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,
|
||||
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.serialization.kotlinx.json)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
||||
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