Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bf6d68be authored by Ellen Poe's avatar Ellen Poe
Browse files

refactor: DirectionsScreen now passing complexity checks

parent 85872413
Loading
Loading
Loading
Loading
+472 −308
Original line number Diff line number Diff line
@@ -82,6 +82,7 @@ import earth.maps.cardinal.ui.core.Screen
import earth.maps.cardinal.ui.place.SearchResults
import earth.maps.cardinal.ui.saved.QuickSuggestions
import io.github.dellisd.spatialk.geojson.BoundingBox
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import uniffi.ferrostar.Route
@@ -153,11 +154,53 @@ fun DirectionsScreen(
        modifier = Modifier
            .fillMaxSize()
            .padding(horizontal = dimensionResource(dimen.padding))
    ) {
        if (!isAnyFieldFocused) {
            DirectionsScreenFullUI(
                viewModel = viewModel,
                onPeekHeightChange = onPeekHeightChange,
                onBack = onBack,
                onFullExpansionRequired = onFullExpansionRequired,
                navController = navController,
                onFieldFocusStateChange = { fieldFocusState = it },
                fieldFocusState = fieldFocusState,
                routeState = routeState,
                availableProfiles = availableProfiles,
                appPreferences = appPreferences,
                hasNotificationPermission = hasNotificationPermission,
                onRequestNotificationPermission = onRequestNotificationPermission
            )
        } else {
            DirectionsScreenFocusedField(
                viewModel = viewModel,
                fieldFocusState = fieldFocusState,
                savedPlaces = savedPlaces,
                hasLocationPermission = hasLocationPermission,
                onRequestLocationPermission = onRequestLocationPermission,
                pendingLocationRequest = pendingLocationRequest,
                coroutineScope = coroutineScope
            )
        }
    }
}

@Composable
private fun DirectionsScreenFullUI(
    viewModel: DirectionsViewModel,
    onPeekHeightChange: (Dp) -> Unit,
    onBack: () -> Unit,
    onFullExpansionRequired: () -> Job,
    navController: NavController,
    onFieldFocusStateChange: (FieldFocusState) -> Unit,
    fieldFocusState: FieldFocusState,
    routeState: RouteState,
    availableProfiles: List<RoutingProfile>,
    appPreferences: AppPreferenceRepository,
    hasNotificationPermission: Boolean,
    onRequestNotificationPermission: () -> Unit
) {
    val density = androidx.compose.ui.platform.LocalDensity.current

        // Conditionally show UI based on field focus
        if (!isAnyFieldFocused) {
    // Show full UI when no field is focused
    Column(
        modifier = Modifier
@@ -194,20 +237,10 @@ fun DirectionsScreen(
        PlaceField(
            label = stringResource(string.from),
            place = viewModel.fromPlace,
                    onCleared = {
                        viewModel.updateFromPlace(null)

                    },
                    onTextChange = {
                        viewModel.updateSearchQuery(it)
                    },
            onCleared = { viewModel.updateFromPlace(null) },
            onTextChange = { viewModel.updateSearchQuery(it) },
            onTextFieldFocusChange = {
                        fieldFocusState = if (it) {
                            onFullExpansionRequired()
                            FieldFocusState.FROM
                        } else {
                            FieldFocusState.NONE
                        }
                onFieldFocusStateChange(if (it) FieldFocusState.FROM else FieldFocusState.NONE)
            },
            isFocused = fieldFocusState == FieldFocusState.FROM,
            showRecalculateButton = viewModel.fromPlace != null && viewModel.toPlace != null,
@@ -221,19 +254,10 @@ fun DirectionsScreen(
        PlaceField(
            label = stringResource(string.to),
            place = viewModel.toPlace,
                    onCleared = {
                        viewModel.updateToPlace(null)
                    },
                    onTextChange = {
                        viewModel.updateSearchQuery(it)
                    },
            onCleared = { viewModel.updateToPlace(null) },
            onTextChange = { viewModel.updateSearchQuery(it) },
            onTextFieldFocusChange = {
                        fieldFocusState = if (it) {
                            onFullExpansionRequired()
                            FieldFocusState.TO
                        } else {
                            FieldFocusState.NONE
                        }
                onFieldFocusStateChange(if (it) FieldFocusState.TO else FieldFocusState.NONE)
            },
            isFocused = fieldFocusState == FieldFocusState.TO,
            showFlipButton = viewModel.fromPlace != null && viewModel.toPlace != null,
@@ -279,8 +303,47 @@ fun DirectionsScreen(
    }

    // Route results
            if (viewModel.selectedRoutingMode == RoutingMode.PUBLIC_TRANSPORT) {
    DirectionsRouteResults(
        viewModel = viewModel,
        routeState = routeState,
        navController = navController,
        appPreferences = appPreferences,
        hasNotificationPermission = hasNotificationPermission,
        onRequestNotificationPermission = onRequestNotificationPermission
    )
}

@Composable
private fun DirectionsRouteResults(
    viewModel: DirectionsViewModel,
    routeState: RouteState,
    navController: NavController,
    appPreferences: AppPreferenceRepository,
    hasNotificationPermission: Boolean,
    onRequestNotificationPermission: () -> Unit
) {
    val planState = viewModel.planState
    if (viewModel.selectedRoutingMode == RoutingMode.PUBLIC_TRANSPORT) {
        TransitRouteResults(planState = planState, navController = navController, viewModel = viewModel, appPreferences = appPreferences)
    } else {
        NonTransitRouteResults(
            routeState = routeState,
            viewModel = viewModel,
            navController = navController,
            appPreferences = appPreferences,
            hasNotificationPermission = hasNotificationPermission,
            onRequestNotificationPermission = onRequestNotificationPermission
        )
    }
}

@Composable
private fun TransitRouteResults(
    planState: TransitPlanState,
    navController: NavController,
    viewModel: DirectionsViewModel,
    appPreferences: AppPreferenceRepository
) {
    when {
        planState.isLoading -> {
            Text(
@@ -321,7 +384,17 @@ fun DirectionsScreen(
            )
        }
    }
            } else {
}

@Composable
private fun NonTransitRouteResults(
    routeState: RouteState,
    viewModel: DirectionsViewModel,
    navController: NavController,
    appPreferences: AppPreferenceRepository,
    hasNotificationPermission: Boolean,
    onRequestNotificationPermission: () -> Unit
) {
    when {
        routeState.isLoading -> {
            Text(
@@ -367,28 +440,31 @@ fun DirectionsScreen(
        }
    }
}
        } else {

@Composable
private fun DirectionsScreenFocusedField(
    viewModel: DirectionsViewModel,
    fieldFocusState: FieldFocusState,
    savedPlaces: List<Place>,
    hasLocationPermission: Boolean,
    onRequestLocationPermission: () -> Unit,
    pendingLocationRequest: FieldFocusState?,
    coroutineScope: CoroutineScope
) {
    // Show only the focused field and search results when a field is focused
            val currentFocusState = fieldFocusState
    PlaceField(
                label = if (currentFocusState == FieldFocusState.FROM) "From" else "To",
                place = if (currentFocusState == FieldFocusState.FROM) viewModel.fromPlace else viewModel.toPlace,
        label = if (fieldFocusState == FieldFocusState.FROM) "From" else "To",
        place = if (fieldFocusState == FieldFocusState.FROM) viewModel.fromPlace else viewModel.toPlace,
        onCleared = {
                    if (currentFocusState == FieldFocusState.FROM) {
            if (fieldFocusState == FieldFocusState.FROM) {
                viewModel.updateFromPlace(null)
            } else {
                viewModel.updateToPlace(null)
            }
        },
                onTextChange = {
                    viewModel.updateSearchQuery(it)
                },
        onTextChange = { viewModel.updateSearchQuery(it) },
        onTextFieldFocusChange = { isFocused ->
                    fieldFocusState = if (isFocused) {
                        currentFocusState
                    } else {
                        FieldFocusState.NONE
                    }

        },
        isFocused = true,
        modifier = Modifier
@@ -397,73 +473,133 @@ fun DirectionsScreen(
    )

    // Show search results or quick suggestions based on search query
            if (viewModel.isSearching) {
    FocusedFieldContent(
        viewModel = viewModel,
        fieldFocusState = fieldFocusState,
        savedPlaces = savedPlaces,
        hasLocationPermission = hasLocationPermission,
        onRequestLocationPermission = onRequestLocationPermission,
        pendingLocationRequest = pendingLocationRequest,
        coroutineScope = coroutineScope
    )
}

@Composable
private fun FocusedFieldContent(
    viewModel: DirectionsViewModel,
    fieldFocusState: FieldFocusState,
    savedPlaces: List<Place>,
    hasLocationPermission: Boolean,
    onRequestLocationPermission: () -> Unit,
    pendingLocationRequest: FieldFocusState?,
    coroutineScope: CoroutineScope
) {
    when {
        viewModel.isSearching -> {
            SearchingIndicator()
        }

        viewModel.searchQuery.isEmpty() -> {
            QuickSuggestionsContent(
                viewModel = viewModel,
                fieldFocusState = fieldFocusState,
                savedPlaces = savedPlaces,
                hasLocationPermission = hasLocationPermission,
                onRequestLocationPermission = onRequestLocationPermission,
                pendingLocationRequest = pendingLocationRequest,
                coroutineScope = coroutineScope
            )
        }

        else -> {
            SearchResultsContent(
                viewModel = viewModel,
                fieldFocusState = fieldFocusState
            )
        }
    }
}

@Composable
private fun SearchingIndicator() {
    Text(
        text = "Searching...",
        modifier = Modifier
            .fillMaxWidth()
            .padding(dimensionResource(dimen.padding))
    )
            } else if (viewModel.searchQuery.isEmpty()) {
                // Show quick suggestions when no search query
                QuickSuggestions(
                    onMyLocationSelected = {
                        // Check permissions before attempting to get location
                        if (hasLocationPermission) {
                            // Launch coroutine to get current location
                            coroutineScope.launch {
                                val myLocationPlace = viewModel.getCurrentLocationAsPlace()
                                myLocationPlace?.let { place ->
                                    // Update the appropriate place based on which field is focused
                                    if (fieldFocusState == FieldFocusState.FROM) {
                                        viewModel.updateFromPlace(place)
                                    } else {
                                        viewModel.updateToPlace(place)
                                    }
                                    // Clear focus state after selection
                                    fieldFocusState = FieldFocusState.NONE
                                }
                            }
                        } else {
                            // Set pending request for auto-retry after permission grant
                            pendingLocationRequest = fieldFocusState
                            // Request location permission
                            onRequestLocationPermission()
}
                    },

@Composable
private fun QuickSuggestionsContent(
    viewModel: DirectionsViewModel,
    fieldFocusState: FieldFocusState,
    savedPlaces: List<Place>,
    hasLocationPermission: Boolean,
    onRequestLocationPermission: () -> Unit,
    pendingLocationRequest: FieldFocusState?,
    coroutineScope: CoroutineScope
) {
    QuickSuggestions(
        onMyLocationSelected = handleMyLocationSelected(
            viewModel = viewModel,
            fieldFocusState = fieldFocusState,
            hasLocationPermission = hasLocationPermission,
            onRequestLocationPermission = onRequestLocationPermission,
            coroutineScope = coroutineScope
        ),
        savedPlaces = savedPlaces,
        onSavedPlaceSelected = { place ->
                        // Update the appropriate place based on which field is focused
                        if (fieldFocusState == FieldFocusState.FROM) {
                            viewModel.updateFromPlace(place)
                        } else {
                            viewModel.updateToPlace(place)
                        }
                        // Clear focus state after selection
                        fieldFocusState = FieldFocusState.NONE
            updatePlaceForField(viewModel, fieldFocusState, place)
        },
        isGettingLocation = viewModel.isGettingLocation,
        modifier = Modifier.fillMaxWidth()
    )
            } else {
                // Show search results when there's a query
}

@Composable
private fun SearchResultsContent(
    viewModel: DirectionsViewModel,
    fieldFocusState: FieldFocusState
) {
    SearchResults(
        viewModel = hiltViewModel(),
        geocodeResults = deduplicateSearchResults(viewModel.geocodeResults.value),
        onPlaceSelected = { place ->
                        // Update the appropriate place based on which field is focused
            updatePlaceForField(viewModel, fieldFocusState, place)
        },
        modifier = Modifier.fillMaxWidth()
    )
}

private fun updatePlaceForField(
    viewModel: DirectionsViewModel,
    fieldFocusState: FieldFocusState,
    place: Place
) {
    if (fieldFocusState == FieldFocusState.FROM) {
        viewModel.updateFromPlace(place)
    } else {
        viewModel.updateToPlace(place)
    }
                        // Clear focus state after selection
                        fieldFocusState = FieldFocusState.NONE
                    },
                    modifier = Modifier.fillMaxWidth()
                )
}

private fun handleMyLocationSelected(
    viewModel: DirectionsViewModel,
    fieldFocusState: FieldFocusState,
    hasLocationPermission: Boolean,
    onRequestLocationPermission: () -> Unit,
    coroutineScope: CoroutineScope
): () -> Unit = {
    if (hasLocationPermission) {
        coroutineScope.launch {
            val myLocationPlace = viewModel.getCurrentLocationAsPlace()
            myLocationPlace?.let { place ->
                updatePlaceForField(viewModel, fieldFocusState, place)
            }
        }
    } else {
        onRequestLocationPermission()
    }
}

@@ -836,19 +972,47 @@ private fun FerrostarRouteResults(
        }
    }

    // Profile selection dialog
    if (showProfileDialog) {
    // Dialogs
    ProfileSelectionDialog(
        showDialog = showProfileDialog,
        onDismiss = { showProfileDialog = false },
        selectedProfile = selectedProfile,
        availableProfiles = availableProfiles,
        onProfileSelected = { profile ->
            viewModel.selectRoutingProfile(profile)
            showProfileDialog = false
        }
    )

    NotificationRequestDialog(
        showDialog = showNotificationDialog,
        onDismiss = { showNotificationDialog = false },
        onConfirm = {
            showNotificationDialog = false
            pendingNavigation = true
            onRequestNotificationPermission()
        }
    )
}

@Composable
private fun ProfileSelectionDialog(
    showDialog: Boolean,
    onDismiss: () -> Unit,
    selectedProfile: RoutingProfile?,
    availableProfiles: List<RoutingProfile>,
    onProfileSelected: (RoutingProfile?) -> Unit
) {
    if (showDialog) {
        AlertDialog(
            onDismissRequest = { showProfileDialog = false },
            onDismissRequest = onDismiss,
            title = { Text(stringResource(string.select_routing_profile)) },
            text = {
                Column {
                    // Default option
                    TextButton(
                        onClick = {
                            viewModel.selectRoutingProfile(null)
                            showProfileDialog = false
                        }, modifier = Modifier.fillMaxWidth()
                        onClick = { onProfileSelected(null) },
                        modifier = Modifier.fillMaxWidth()
                    ) {
                        Text(
                            text = stringResource(string.default_profile),
@@ -859,10 +1023,8 @@ private fun FerrostarRouteResults(
                    // Custom profiles
                    availableProfiles.forEach { profile ->
                        TextButton(
                            onClick = {
                                viewModel.selectRoutingProfile(profile)
                                showProfileDialog = false
                            }, modifier = Modifier.fillMaxWidth()
                            onClick = { onProfileSelected(profile) },
                            modifier = Modifier.fillMaxWidth()
                        ) {
                            Text(
                                text = profile.name,
@@ -873,29 +1035,31 @@ private fun FerrostarRouteResults(
                }
            },
            confirmButton = {
                TextButton(onClick = { showProfileDialog = false }) {
                TextButton(onClick = onDismiss) {
                    Text(stringResource(string.cancel_change_routing_profile))
                }
            })
    }
}

    // Notification permission dialog
    if (showNotificationDialog) {
@Composable
private fun NotificationRequestDialog(
    showDialog: Boolean,
    onDismiss: () -> Unit,
    onConfirm: () -> Unit
) {
    if (showDialog) {
        AlertDialog(
            onDismissRequest = { showNotificationDialog = false },
            onDismissRequest = onDismiss,
            title = { Text(stringResource(string.notification_ask_title)) },
            text = { Text(stringResource(string.notification_ask_body)) },
            confirmButton = {
                TextButton(onClick = {
                    showNotificationDialog = false
                    pendingNavigation = true
                    onRequestNotificationPermission()
                }) {
                TextButton(onClick = onConfirm) {
                    Text(stringResource(string.got_it))
                }
            },
            dismissButton = {
                TextButton(onClick = { showNotificationDialog = false }) {
                TextButton(onClick = onDismiss) {
                    Text(stringResource(string.cancel))
                }
            })