Skip to content

Commit

Permalink
UI: Add DateTime selector screen and picker (#394)
Browse files Browse the repository at this point in the history
### TL;DR
Added a new date and time selector screen for trip planning with a custom time picker interface.

### What changed?
- Created a new `DateTimeSelectorScreen` with a vertical time picker and date selection UI
- Added navigation support for the new date/time selector screen
- Made the "Leaving Now" text in TimeTableScreen clickable to navigate to the selector
- Implemented a custom time picker with theme-aware colors and styling
- Added radio buttons for "Arrive" and "Leave" journey time options

### How to test?
1. Navigate to the TimeTableScreen
2. Tap on "Leaving Now" text
3. Verify the new DateTimeSelector screen appears
4. Test the time picker functionality
5. Verify the date selection arrows work
6. Confirm the Arrive/Leave radio buttons are functional
7. Test the back button navigation

### Why make this change?
To provide users with more control over their journey planning by allowing them to select specific dates and times for their trips, rather than being limited to current time departures only.
  • Loading branch information
ksharma-xyz authored Nov 29, 2024
1 parent 489346e commit eeada6f
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ private fun KrailTimePicker(
}

@Composable
private fun IconButton(
fun IconButton(
painter: Painter,
color: Color,
modifier: Modifier = Modifier,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package xyz.ksharma.krail.trip.planner.ui.datetimeselector

import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import org.koin.compose.viewmodel.koinViewModel
import xyz.ksharma.krail.trip.planner.ui.navigation.DateTimeSelectorRoute

internal fun NavGraphBuilder.dateTimeSelectorDestination(navController: NavHostController) {
composable<DateTimeSelectorRoute> { backStackEntry ->
val viewModel: DateTimeSelectorViewModel = koinViewModel<DateTimeSelectorViewModel>()

DateTimeSelectorScreen()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package xyz.ksharma.krail.trip.planner.ui.datetimeselector

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerColors
import androidx.compose.material3.TimePickerLayoutType
import androidx.compose.material3.TimePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import krail.feature.trip_planner.ui.generated.resources.Res
import krail.feature.trip_planner.ui.generated.resources.ic_chevron_left
import krail.feature.trip_planner.ui.generated.resources.ic_chevron_right
import org.jetbrains.compose.resources.painterResource
import xyz.ksharma.krail.core.datetime.rememberCurrentDateTime
import xyz.ksharma.krail.taj.LocalThemeColor
import xyz.ksharma.krail.taj.components.Text
import xyz.ksharma.krail.taj.components.TitleBar
import xyz.ksharma.krail.taj.theme.KrailTheme
import xyz.ksharma.krail.taj.theme.getForegroundColor
import xyz.ksharma.krail.trip.planner.ui.components.IconButton
import xyz.ksharma.krail.trip.planner.ui.components.RadioButton
import xyz.ksharma.krail.trip.planner.ui.components.hexToComposeColor
import xyz.ksharma.krail.trip.planner.ui.timetable.ActionButton

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DateTimeSelectorScreen(modifier: Modifier = Modifier, onBackClick: () -> Unit = {}) {
val themeColorHex by LocalThemeColor.current
val themeColor = remember(themeColorHex) {
themeColorHex.hexToComposeColor()
}

Column(
modifier = modifier
.fillMaxSize()
.background(color = KrailTheme.colors.surface),
) {
TitleBar(
title = { Text(text = "Plan your trip") },
navAction = {
ActionButton(
onClick = onBackClick,
contentDescription = "Back",
) {
Image(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(KrailTheme.colors.onSurface),
modifier = Modifier.size(24.dp),
)
}
},
)

LazyColumn(contentPadding = PaddingValues(vertical = 16.dp)) {
item {
JourneyTimeOptionsRow(themeColor)
}

item {
DateSelection(
themeColor = themeColor,
date = "Today, 12th July",
modifier = Modifier.padding(horizontal = 16.dp),
)
}

item {
val currentDateTime = rememberCurrentDateTime()
val timePickerState = rememberTimePickerState(
initialHour = currentDateTime.hour,
initialMinute = currentDateTime.minute,
is24Hour = false,
)
TimeSelection(
timePickerState = timePickerState,
modifier = Modifier.padding(horizontal = 16.dp),
)
}

}
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimeSelection(
timePickerState: TimePickerState,
modifier: Modifier,
) {
val themeColorHex by LocalThemeColor.current
val themeColor = themeColorHex.hexToComposeColor()
val themeContentColor = getForegroundColor(themeColor)

TimePicker(
state = timePickerState,
colors = TimePickerColors(
containerColor = KrailTheme.colors.surface,
clockDialColor = KrailTheme.colors.surface,
selectorColor = themeColor,
periodSelectorBorderColor = themeColor,
clockDialSelectedContentColor = themeContentColor,
clockDialUnselectedContentColor = KrailTheme.colors.onSurface.copy(alpha = 0.6f),
periodSelectorSelectedContainerColor = themeColor,
periodSelectorUnselectedContainerColor = KrailTheme.colors.surface,
periodSelectorSelectedContentColor = themeContentColor,
periodSelectorUnselectedContentColor = KrailTheme.colors.onSurface.copy(alpha = 0.6f),
timeSelectorSelectedContainerColor = themeColor,
timeSelectorUnselectedContainerColor = KrailTheme.colors.surface,
timeSelectorSelectedContentColor = themeContentColor,
timeSelectorUnselectedContentColor = KrailTheme.colors.onSurface.copy(alpha = 0.6f)
),
layoutType = TimePickerLayoutType.Vertical,
modifier = modifier.fillMaxWidth()
)
}

@Composable
private fun DateSelection(
themeColor: Color,
date: String,
modifier: Modifier = Modifier,
onDateSelected: () -> Unit = {},
) {
Column(modifier = modifier.fillMaxWidth().padding(vertical = 24.dp)) {
Text(
text = "Select Date",
style = KrailTheme.typography.title,
color = KrailTheme.colors.onSurface,
modifier = Modifier.padding(vertical = 12.dp)
)

Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
IconButton(
painter = painterResource(Res.drawable.ic_chevron_left),
color = themeColor,
modifier = Modifier.align(Alignment.CenterVertically)
)
Text(
text = date,
style = KrailTheme.typography.bodyLarge,
color = KrailTheme.colors.onSurface,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
)
IconButton(
painter = painterResource(Res.drawable.ic_chevron_right),
color = themeColor,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
}

@Composable
private fun JourneyTimeOptionsRow(themeColor: Color) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
RadioButton(
text = "Arrive",
selected = true,
backgroundColor = themeColor,
)
RadioButton(
text = "Leave",
backgroundColor = themeColor,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package xyz.ksharma.krail.trip.planner.ui.datetimeselector

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import xyz.ksharma.krail.core.datetime.DateTimeHelper.calculateTimeDifferenceFromNow
import xyz.ksharma.krail.core.datetime.DateTimeHelper.toGenericFormattedTimeString
import xyz.ksharma.krail.sandook.Sandook
import xyz.ksharma.krail.trip.planner.network.api.model.TripResponse
import xyz.ksharma.krail.trip.planner.network.api.ratelimit.RateLimiter
import xyz.ksharma.krail.trip.planner.network.api.service.TripPlanningService
import xyz.ksharma.krail.trip.planner.ui.state.alerts.ServiceAlert
import xyz.ksharma.krail.trip.planner.ui.state.timetable.TimeTableState
import xyz.ksharma.krail.trip.planner.ui.state.timetable.TimeTableUiEvent
import xyz.ksharma.krail.trip.planner.ui.state.timetable.Trip
import xyz.ksharma.krail.trip.planner.ui.timetable.business.buildJourneyList
import kotlin.time.Duration.Companion.seconds

class DateTimeSelectorViewModel : ViewModel() {

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import xyz.ksharma.krail.trip.planner.ui.searchstop.SearchStopViewModel
import xyz.ksharma.krail.trip.planner.ui.timetable.TimeTableViewModel
import xyz.ksharma.krail.trip.planner.ui.usualride.UsualRideViewModel
import xyz.ksharma.krail.trip.planner.ui.settings.SettingsViewModel
import xyz.ksharma.krail.trip.planner.ui.datetimeselector.DateTimeSelectorViewModel

val viewModelsModule = module {
viewModelOf(::SavedTripsViewModel)
viewModelOf(::SearchStopViewModel)
viewModelOf(::TimeTableViewModel)
viewModelOf(::UsualRideViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::DateTimeSelectorViewModel)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.navigation
import kotlinx.serialization.Serializable
import xyz.ksharma.krail.trip.planner.ui.alerts.alertsDestination
import xyz.ksharma.krail.trip.planner.ui.datetimeselector.dateTimeSelectorDestination
import xyz.ksharma.krail.trip.planner.ui.savedtrips.savedTripsDestination
import xyz.ksharma.krail.trip.planner.ui.searchstop.searchStopDestination
import xyz.ksharma.krail.trip.planner.ui.settings.settingsDestination
Expand All @@ -31,6 +32,8 @@ fun NavGraphBuilder.tripPlannerDestinations(
alertsDestination(navController)

settingsDestination(navController)

dateTimeSelectorDestination(navController)
}
}

Expand Down Expand Up @@ -80,3 +83,6 @@ internal data class ServiceAlertRoute(

@Serializable
data object SettingsRoute

@Serializable
data object DateTimeSelectorRoute
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import androidx.navigation.toRoute
import org.koin.compose.viewmodel.koinViewModel
import xyz.ksharma.krail.trip.planner.ui.navigation.DateTimeSelectorRoute
import xyz.ksharma.krail.trip.planner.ui.navigation.ServiceAlertRoute
import xyz.ksharma.krail.trip.planner.ui.navigation.TimeTableRoute
import xyz.ksharma.krail.trip.planner.ui.state.timetable.TimeTableUiEvent
Expand Down Expand Up @@ -43,6 +44,12 @@ internal fun NavGraphBuilder.timeTableDestination(navController: NavHostControll
}
}
},
dateTimeSelectorClicked = {
navController.navigate(
route = DateTimeSelectorRoute,
navOptions = NavOptions.Builder().setLaunchSingleTop(singleTop = true).build(),
)
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
Expand Down Expand Up @@ -48,6 +49,7 @@ import xyz.ksharma.krail.trip.planner.ui.components.ErrorMessage
import xyz.ksharma.krail.trip.planner.ui.components.JourneyCard
import xyz.ksharma.krail.trip.planner.ui.components.JourneyCardState
import xyz.ksharma.krail.trip.planner.ui.components.OriginDestination
import xyz.ksharma.krail.trip.planner.ui.components.hexToComposeColor
import xyz.ksharma.krail.trip.planner.ui.components.loading.LoadingEmojiAnim
import xyz.ksharma.krail.trip.planner.ui.state.TransportMode
import xyz.ksharma.krail.trip.planner.ui.state.TransportModeLine
Expand All @@ -63,7 +65,11 @@ fun TimeTableScreen(
onAlertClick: (String) -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
dateTimeSelectorClicked: () -> Unit = {},
) {
val themeColorHex by LocalThemeColor.current
val themeColor = themeColorHex.hexToComposeColor()

Column(
modifier = modifier
.fillMaxSize()
Expand Down Expand Up @@ -148,6 +154,19 @@ fun TimeTableScreen(
Spacer(modifier = Modifier.height(16.dp))
}

item {
Text(
text = "Leaving Now",
style = KrailTheme.typography.bodyLarge,
color = themeColor,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp).clickable {
dateTimeSelectorClicked()
},
)
}

if (timeTableState.isError) {
item {
ErrorMessage(
Expand Down

0 comments on commit eeada6f

Please sign in to comment.