diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/BaseSearchViewModel.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/BaseSearchViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..9ee1762a1b8efd025fd2622e94a61b863fd63e9d --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/BaseSearchViewModel.kt @@ -0,0 +1,138 @@ +/* + * Cardinal Maps + * Copyright (C) 2025 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.ui.core + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import earth.maps.cardinal.data.LatLng +import earth.maps.cardinal.data.Place +import earth.maps.cardinal.data.ViewportRepository +import earth.maps.cardinal.data.room.RecentSearchRepository +import earth.maps.cardinal.geocoding.GeocodingService +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +/** + * Base ViewModel that provides common search functionality + * to reduce duplication across ViewModels that need search capabilities + */ +@OptIn(FlowPreview::class) +abstract class BaseSearchViewModel( + protected val geocodingService: GeocodingService, + protected val viewportRepository: ViewportRepository, + protected val recentSearchRepository: RecentSearchRepository +) : ViewModel() { + + // Search query flow for debouncing + private val _searchQueryFlow = MutableStateFlow("") + protected val searchQueryFlow: StateFlow = _searchQueryFlow.asStateFlow() + + var searchQuery by mutableStateOf("") + protected set + + val geocodeResults = mutableStateOf>(emptyList()) + + var isSearching by mutableStateOf(false) + protected set + + var searchError by mutableStateOf(null) + protected set + + init { + // Set up debounced search + searchQueryFlow + .debounce(300) // 300ms delay + .distinctUntilChanged() + .onEach { query -> + if (query.isNotEmpty()) { + performSearch(query) + } else { + // Clear results when query is empty + geocodeResults.value = emptyList() + searchError = null + } + } + .launchIn(viewModelScope) + } + + /** + * Updates the search query and triggers debounced search + */ + fun updateSearchQuery(query: String) { + searchQuery = query + _searchQueryFlow.value = query + } + + /** + * Performs the actual search operation + * Can be overridden by subclasses to provide custom focus point logic + */ + protected open fun performSearch(query: String) { + viewModelScope.launch { + isSearching = true + searchError = null + try { + // Get focus point for viewport biasing - subclasses can override this + val focusPoint = getSearchFocusPoint() + geocodeResults.value = geocodingService.geocode(query, focusPoint) + isSearching = false + } catch (e: Exception) { + // Handle error + searchError = e.message ?: "An error occurred during search" + geocodeResults.value = emptyList() + isSearching = false + } + } + } + + /** + * Determines the focus point for search viewport biasing + * Subclasses can override this to provide custom logic + */ + protected open suspend fun getSearchFocusPoint(): LatLng? { + return viewportRepository.viewportCenter.value + } + + /** + * Adds a place to recent searches + */ + protected fun addRecentSearch(place: Place) { + viewModelScope.launch { + recentSearchRepository.addRecentSearch(place) + } + } + + /** + * Called when a search result is selected + * Subclasses can override this to provide custom behavior + */ + open fun onPlaceSelected(place: Place) { + addRecentSearch(place) + } +} \ No newline at end of file diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt index ae8febe5bda8d2e57e68a4918e285917d110abc7..5cd2ba1bfb2091be9cde9c6ecea71c6eec658854 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsViewModel.kt @@ -22,7 +22,6 @@ import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import dagger.hilt.android.lifecycle.HiltViewModel @@ -45,19 +44,14 @@ import earth.maps.cardinal.geocoding.GeocodingService import earth.maps.cardinal.routing.FerrostarWrapperRepository import earth.maps.cardinal.routing.RouteRepository import earth.maps.cardinal.transit.TransitousService +import earth.maps.cardinal.ui.core.BaseSearchViewModel import earth.maps.cardinal.ui.core.NavigationUtils import earth.maps.cardinal.ui.core.Screen import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uniffi.ferrostar.GeographicCoordinate @@ -68,12 +62,11 @@ import java.time.Instant import javax.inject.Inject import kotlin.time.ExperimentalTime -@OptIn(FlowPreview::class) @HiltViewModel class DirectionsViewModel @Inject constructor( - private val geocodingService: GeocodingService, + geocodingService: GeocodingService, private val ferrostarWrapperRepository: FerrostarWrapperRepository, - private val viewportRepository: ViewportRepository, + viewportRepository: ViewportRepository, private val placeDao: SavedPlaceDao, private val savedPlaceRepository: SavedPlaceRepository, private val locationRepository: LocationRepository, @@ -81,25 +74,10 @@ class DirectionsViewModel @Inject constructor( private val routeRepository: RouteRepository, private val appPreferenceRepository: AppPreferenceRepository, private val transitousService: TransitousService, - private val recentSearchRepository: RecentSearchRepository, + recentSearchRepository: RecentSearchRepository, private val routeStateRepository: RouteStateRepository, private val planStateRepository: PlanStateRepository, -) : ViewModel() { - - // Search query flow for debouncing - private val _searchQueryFlow = MutableStateFlow("") - private val searchQueryFlow: StateFlow = _searchQueryFlow.asStateFlow() - - var searchQuery by mutableStateOf("") - private set - - val geocodeResults = mutableStateOf>(emptyList()) - - var isSearching by mutableStateOf(false) - private set - - var searchError by mutableStateOf(null) - private set +) : BaseSearchViewModel(geocodingService, viewportRepository, recentSearchRepository) { // Directions state var fromPlace by mutableStateOf(null) @@ -128,22 +106,6 @@ class DirectionsViewModel @Inject constructor( private var haveManuallySetDeparture: Boolean = false - init { - // Set up debounced search - searchQueryFlow - .debounce(300) // 300ms delay - .distinctUntilChanged() - .onEach { query -> - if (query.isNotEmpty()) { - performSearch(query) - } else { - // Clear results when query is empty - geocodeResults.value = emptyList() - searchError = null - } - } - .launchIn(viewModelScope) - } suspend fun initializeRoutingMode() { // Set initial routing mode from preferences @@ -154,10 +116,6 @@ class DirectionsViewModel @Inject constructor( initializeDefaultProfileForMode(selectedRoutingMode) } - fun updateSearchQuery(query: String) { - searchQuery = query - _searchQueryFlow.value = query - } suspend fun initializeDeparture() { if (!appPreferenceRepository.continuousLocationTracking.value) { @@ -451,24 +409,10 @@ class DirectionsViewModel @Inject constructor( } } - private fun performSearch(query: String) { - viewModelScope.launch { - isSearching = true - searchError = null - try { - // Use fromPlace as focus point for viewport biasing if available, - // otherwise fall back to current viewport center - val focusPoint = fromPlace?.latLng ?: viewportRepository.viewportCenter.value - - geocodeResults.value = geocodingService.geocode(query, focusPoint) - isSearching = false - } catch (e: Exception) { - // Handle error - searchError = e.message ?: "An error occurred during search" - geocodeResults.value = emptyList() - isSearching = false - } - } + override suspend fun getSearchFocusPoint(): LatLng? { + // Use fromPlace as focus point for viewport biasing if available, + // otherwise fall back to current viewport center + return fromPlace?.latLng ?: super.getSearchFocusPoint() } /** @@ -488,9 +432,7 @@ class DirectionsViewModel @Inject constructor( * Adds the place to recent searches. */ fun onPlaceSelectedFromSearch(place: Place) { - viewModelScope.launch { - recentSearchRepository.addRecentSearch(place) - } + addRecentSearch(place) } /** diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt index ec7d0141a3aeceb12ea9a7bb60c0155aac9d97e9..9b15490e9ed1b8154b58d254643b67d1b26f1605 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -70,6 +71,7 @@ import earth.maps.cardinal.R.string import earth.maps.cardinal.data.AddressFormatter import earth.maps.cardinal.data.GeocodeResult import earth.maps.cardinal.data.Place +import earth.maps.cardinal.data.room.RecentSearch import earth.maps.cardinal.ui.core.TOOLBAR_HEIGHT_DP import earth.maps.cardinal.ui.place.SearchResultItem import kotlinx.coroutines.launch @@ -85,7 +87,7 @@ fun HomeScreen( onResultPinsChange: (List) -> Unit, onSearchEvent: () -> Unit, ) { - val searchQuery = viewModel.searchQuery + val searchQuery = viewModel.searchQueryValue Column { SearchPanelContent( @@ -99,7 +101,7 @@ fun HomeScreen( onPeekHeightChange = onPeekHeightChange, onSearchEvent = onSearchEvent, onPlaceSelected = onPlaceSelected, - homeInSearchScreen = viewModel.searchQuery.text.isNotEmpty(), + homeInSearchScreen = viewModel.searchQueryValue.text.isNotEmpty(), ) } } @@ -117,11 +119,11 @@ private fun SearchPanelContent( homeInSearchScreen: Boolean, ) { val addressFormatter = remember { AddressFormatter() } - val pinnedPlaces by viewModel.pinnedPlaces().collectAsState(emptyList()) - val geocodeResults by viewModel.geocodeResults.collectAsState(emptyList()) + val pinnedPlaces by viewModel.pinnedPlaces().collectAsState(initial = emptyList()) + val geocodeResults = viewModel.geocodeResults - LaunchedEffect(geocodeResults) { - onResultPinsChange(geocodeResults) + LaunchedEffect(geocodeResults.value) { + onResultPinsChange(geocodeResults.value) } Column( @@ -148,7 +150,7 @@ private fun SearchPanelContent( ContentBelow( homeInSearchScreen = homeInSearchScreen, - geocodePlaces = geocodeResults, + geocodePlaces = geocodeResults.value, viewModel = viewModel, onPlaceSelected = { place -> viewModel.onPlaceSelected(place) @@ -295,8 +297,16 @@ private fun ContentBelow( onPlaceSelected: (Place) -> Unit, addressFormatter: AddressFormatter, ) { - val recentSearches by viewModel.recentSearches().collectAsState(emptyList()) + val recentSearches = remember { mutableStateOf>(emptyList()) } val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + coroutineScope.launch { + viewModel.recentSearches().collect { searches -> + recentSearches.value = searches + } + } + } if (homeInSearchScreen) { LazyColumn { @@ -312,7 +322,7 @@ private fun ContentBelow( } else { LazyColumn { // Show recent searches if any. - items(recentSearches) { recentSearch -> + items(recentSearches.value) { recentSearch -> SearchResultItem( addressFormatter = addressFormatter, place = viewModel.searchToPlace(recentSearch), diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeViewModel.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeViewModel.kt index 17f35939bccacc2e2a6087d7ca42b755fc432536..8e2df921e982c7a6d0b077c3cf0c0876465aa45e 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeViewModel.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/home/HomeViewModel.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import earth.maps.cardinal.data.LocationRepository @@ -33,87 +32,40 @@ import earth.maps.cardinal.data.room.RecentSearchRepository import earth.maps.cardinal.data.room.SavedPlaceDao import earth.maps.cardinal.data.room.SavedPlaceRepository import earth.maps.cardinal.geocoding.GeocodingService -import kotlinx.coroutines.FlowPreview +import earth.maps.cardinal.ui.core.BaseSearchViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject -@OptIn(FlowPreview::class) @HiltViewModel class HomeViewModel @Inject constructor( private val placeDao: SavedPlaceDao, - private val geocodingService: GeocodingService, - private val viewportRepository: ViewportRepository, + geocodingService: GeocodingService, + viewportRepository: ViewportRepository, private val locationRepository: LocationRepository, private val savedPlaceRepository: SavedPlaceRepository, - private val recentSearchRepository: RecentSearchRepository, -) : ViewModel() { + recentSearchRepository: RecentSearchRepository, +) : BaseSearchViewModel(geocodingService, viewportRepository, recentSearchRepository) { // Whether the home screen is in a search state. private val _searchExpanded = MutableStateFlow(false) val searchExpanded: Flow = _searchExpanded - // Search query flow for debouncing - private val _searchQueryFlow = MutableStateFlow("") - private val searchQueryFlow: StateFlow = _searchQueryFlow.asStateFlow() - - var searchQuery by mutableStateOf( + // Keep selection state separately from the base viewmodel so that we can preserve it when the + // user hits the back button to return to the search screen. + var searchQueryValue by mutableStateOf( TextFieldValue() ) - - val geocodeResults = MutableStateFlow>(emptyList()) - - var isSearching by mutableStateOf(false) private set - var searchError by mutableStateOf(null) - private set - - init { - // Set up debounced search - searchQueryFlow.debounce(300) // 300ms delay - .distinctUntilChanged().onEach { query -> - if (query.isNotEmpty()) { - performSearch(query) - } else { - // Clear results when query is empty - geocodeResults.value = emptyList() - searchError = null - } - }.launchIn(viewModelScope) - } - fun updateSearchQuery(query: TextFieldValue) { - searchQuery = query - _searchQueryFlow.value = query.text - } - - - private fun performSearch(query: String) { - viewModelScope.launch { - isSearching = true - searchError = null - try { - // Use current viewport center as focus point for viewport biasing - val focusPoint = viewportRepository.viewportCenter.value - geocodeResults.value = geocodingService.geocode(query, focusPoint) - isSearching = false - } catch (e: Exception) { - // Handle error - searchError = e.message ?: "An error occurred during search" - geocodeResults.value = emptyList() - isSearching = false - } - } + searchQueryValue = query + updateSearchQuery(query.text) } fun pinnedPlaces(): Flow> { @@ -134,16 +86,14 @@ class HomeViewModel @Inject constructor( * Called when a search result is selected/tapped. * Adds the place to recent searches. */ - fun onPlaceSelected(place: Place) { - viewModelScope.launch { - recentSearchRepository.addRecentSearch(place) - } + override fun onPlaceSelected(place: Place) { + addRecentSearch(place) } /** * Gets recent searches. */ - fun recentSearches(): Flow> { + suspend fun recentSearches(): Flow> { return recentSearchRepository.getRecentSearches() } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/AccessibilitySettingsScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/AccessibilitySettingsScreen.kt index 086c6e03fdd600e96472d6fec9de911763b81bc5..217aba55ab95800f80fbd2e908d5fdc2ec24bfd4 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/AccessibilitySettingsScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/AccessibilitySettingsScreen.kt @@ -18,24 +18,8 @@ package earth.maps.cardinal.ui.settings -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.DividerDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -45,132 +29,81 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import earth.maps.cardinal.R.dimen import earth.maps.cardinal.R.string import earth.maps.cardinal.data.AppPreferences -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AccessibilitySettingsScreen( viewModel: SettingsViewModel = hiltViewModel(), onDismiss: () -> Unit ) { - val snackBarHostState = remember { SnackbarHostState() } - - Scaffold( - snackbarHost = { SnackbarHost(snackBarHostState) }, - contentWindowInsets = WindowInsets.safeDrawing, - topBar = { - TopAppBar(title = { - Text( - text = stringResource(string.accessibility_settings_title), - style = MaterialTheme.typography.headlineMedium, - fontWeight = androidx.compose.ui.text.font.FontWeight.Bold - ) - }) - }, - content = { padding -> - Box(modifier = Modifier.padding(padding)) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) + SettingsScreenScaffold( + title = stringResource(string.accessibility_settings_title), + onDismiss = onDismiss + ) { padding -> + Column(modifier = Modifier.padding(padding)) { + SettingsDivider() + SettingsScrollableContent() { + // Contrast Settings Item + SettingsItem( + title = stringResource(string.contrast_settings_title), + description = stringResource(string.contrast_settings_help_text) ) { - // Contrast Settings Item - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) - ) { - Text( - text = stringResource(string.contrast_settings_title), - style = MaterialTheme.typography.titleMedium - ) - Text( - text = stringResource(string.contrast_settings_help_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - // Contrast level selection - val currentContrastLevel by viewModel.contrastLevel.collectAsState() - var selectedContrastLevel by remember { mutableStateOf(currentContrastLevel) } + // Contrast level selection + val currentContrastLevel by viewModel.contrastLevel.collectAsState() + var selectedContrastLevel by remember { mutableStateOf(currentContrastLevel) } - // Update selected state when preference changes from outside - LaunchedEffect(currentContrastLevel) { - selectedContrastLevel = currentContrastLevel - } + // Update selected state when preference changes from outside + LaunchedEffect(currentContrastLevel) { + selectedContrastLevel = currentContrastLevel + } - PreferenceOption( - selectedValue = selectedContrastLevel, options = listOf( - AppPreferences.CONTRAST_LEVEL_STANDARD to string.contrast_standard, - AppPreferences.CONTRAST_LEVEL_MEDIUM to string.contrast_medium, - AppPreferences.CONTRAST_LEVEL_HIGH to string.contrast_high - ) - ) { newValue -> - selectedContrastLevel = newValue - viewModel.setContrastLevel(newValue) - } + PreferenceOption( + selectedValue = selectedContrastLevel, options = listOf( + AppPreferences.CONTRAST_LEVEL_STANDARD to string.contrast_standard, + AppPreferences.CONTRAST_LEVEL_MEDIUM to string.contrast_medium, + AppPreferences.CONTRAST_LEVEL_HIGH to string.contrast_high + ) + ) { newValue -> + selectedContrastLevel = newValue + viewModel.setContrastLevel(newValue) } + } - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) + SettingsDivider() - // Animation Speed Settings Item - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) - ) { - Text( - text = stringResource(string.animation_speed_title), - style = MaterialTheme.typography.titleMedium - ) - Text( - text = stringResource(string.animation_speed_help_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + // Animation Speed Settings Item + SettingsItem( + title = stringResource(string.animation_speed_title), + description = stringResource(string.animation_speed_help_text) + ) { + // Animation speed selection + val currentAnimationSpeed by viewModel.animationSpeed.collectAsState() + var selectedAnimationSpeed by remember { + mutableIntStateOf( + currentAnimationSpeed ) + } - // Animation speed selection - val currentAnimationSpeed by viewModel.animationSpeed.collectAsState() - var selectedAnimationSpeed by remember { - mutableIntStateOf( - currentAnimationSpeed - ) - } - - // Update selected state when preference changes from outside - LaunchedEffect(currentAnimationSpeed) { - selectedAnimationSpeed = currentAnimationSpeed - } + // Update selected state when preference changes from outside + LaunchedEffect(currentAnimationSpeed) { + selectedAnimationSpeed = currentAnimationSpeed + } - PreferenceOption( - selectedValue = selectedAnimationSpeed, options = listOf( - AppPreferences.ANIMATION_SPEED_SLOW to string.animation_speed_slow, - AppPreferences.ANIMATION_SPEED_NORMAL to string.animation_speed_normal, - AppPreferences.ANIMATION_SPEED_FAST to string.animation_speed_fast - ) - ) { newValue -> - selectedAnimationSpeed = newValue - viewModel.setAnimationSpeed(newValue) - } + PreferenceOption( + selectedValue = selectedAnimationSpeed, options = listOf( + AppPreferences.ANIMATION_SPEED_SLOW to string.animation_speed_slow, + AppPreferences.ANIMATION_SPEED_NORMAL to string.animation_speed_normal, + AppPreferences.ANIMATION_SPEED_FAST to string.animation_speed_fast + ) + ) { newValue -> + selectedAnimationSpeed = newValue + viewModel.setAnimationSpeed(newValue) } } } } - ) + } } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/AdvancedSettingsScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/AdvancedSettingsScreen.kt index 9e754dd4db41fe633e5ffde8ca45b136fd5226d1..bc56fcb1ea9bf34bde4db4c876f787b525fc535c 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/AdvancedSettingsScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/AdvancedSettingsScreen.kt @@ -18,31 +18,11 @@ package earth.maps.cardinal.ui.settings -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.DividerDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -50,220 +30,125 @@ import androidx.compose.runtime.getValue 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.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import earth.maps.cardinal.R.dimen import earth.maps.cardinal.R.string -import earth.maps.cardinal.ui.core.TOOLBAR_HEIGHT_DP @Composable -private fun ContinuousLocationTrackingSetting(viewModel: SettingsViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) - ) { - Text( - text = stringResource(string.continuous_location_tracking_disabled_title), - style = MaterialTheme.typography.titleMedium - ) - Text( - text = stringResource(string.continuous_location_tracking_disabled_help_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) +fun AdvancedSettingsScreen( + viewModel: SettingsViewModel = hiltViewModel() +) { + SettingsScreenScaffold( + title = stringResource(string.advanced_settings_title) + ) { padding -> + Column(modifier = Modifier.padding(padding)) { + SettingsDivider() + SettingsScrollableContent { + StatefulSwitchSetting( + title = stringResource(string.continuous_location_tracking_disabled_title), + description = stringResource(string.continuous_location_tracking_disabled_help_text), + stateFlow = viewModel.continuousLocationTracking, + onStateChanged = viewModel::setContinuousLocationTrackingEnabled + ) - val continuousLocationTracking by viewModel.continuousLocationTracking.collectAsState() + SettingsDivider() - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = if (continuousLocationTracking) stringResource(string.enabled) else stringResource( - string.disabled - ), style = MaterialTheme.typography.bodyMedium - ) - Switch( - checked = continuousLocationTracking, - onCheckedChange = { newValue -> - viewModel.setContinuousLocationTrackingEnabled(newValue) - } - ) - } - } -} + StatefulSwitchSetting( + title = stringResource(string.show_zoom_fabs_title), + description = stringResource(string.show_zoom_fabs_help_text), + stateFlow = viewModel.showZoomFabs, + onStateChanged = viewModel::setShowZoomFabsEnabled + ) -@Composable -private fun ShowZoomFabsSetting(viewModel: SettingsViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) - ) { - Text( - text = stringResource(string.show_zoom_fabs_title), - style = MaterialTheme.typography.titleMedium - ) - Text( - text = stringResource(string.show_zoom_fabs_help_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + SettingsDivider() - val showZoomFabs by viewModel.showZoomFabs.collectAsState() + // Time format setting with custom text + TimeFormatSetting(viewModel) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = if (showZoomFabs) stringResource(string.enabled) else stringResource( - string.disabled - ), style = MaterialTheme.typography.bodyMedium - ) - Switch( - checked = showZoomFabs, - onCheckedChange = { newValue -> - viewModel.setShowZoomFabsEnabled(newValue) - } - ) + SettingsDivider() + + // Distance unit setting with custom text + DistanceUnitSetting(viewModel) + + SettingsDivider() + + // API Configuration settings + PeliasBaseUrlSetting(viewModel) + + SettingsDivider() + + PeliasApiKeySetting(viewModel) + + SettingsDivider() + + ValhallaBaseUrlSetting(viewModel) + + SettingsDivider() + + ValhallaApiKeySetting(viewModel) + } } } } @Composable private fun TimeFormatSetting(viewModel: SettingsViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) - ) { - Text( - text = stringResource(string.time_format_title), - style = MaterialTheme.typography.titleMedium - ) - Text( - text = stringResource(string.time_format_help_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + val use24HourFormat by viewModel.use24HourFormat.collectAsState() + var isChecked by remember { mutableStateOf(use24HourFormat) } - val use24HourFormat by viewModel.use24HourFormat.collectAsState() - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - val formatText = if (use24HourFormat) { - "24\u2011hour" - } else { - "12\u2011hour" - } - Text( - text = formatText, - style = MaterialTheme.typography.bodyMedium - ) - Switch( - checked = use24HourFormat, - onCheckedChange = { newValue -> - viewModel.setUse24HourFormat(newValue) - } - ) - } + // Update selected state when preference changes from outside + LaunchedEffect(use24HourFormat) { + isChecked = use24HourFormat } + + SwitchSetting( + title = stringResource(string.time_format_title), + description = stringResource(string.time_format_help_text), + isChecked = isChecked, + onCheckedChange = { newValue -> + isChecked = newValue + viewModel.setUse24HourFormat(newValue) + }, + enabledText = "24\u2011hour", + disabledText = "12\u2011hour" + ) } @Composable private fun DistanceUnitSetting(viewModel: SettingsViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) - ) { - - Text( - text = stringResource(string.distance_unit_title), - style = MaterialTheme.typography.titleMedium - ) - Text( - text = stringResource(string.distance_unit_help_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - val distanceUnit by viewModel.distanceUnit.collectAsState() - val isMetric = distanceUnit == 0 + val distanceUnit by viewModel.distanceUnit.collectAsState() + val isMetric = distanceUnit == 0 + var isChecked by remember { mutableStateOf(isMetric) } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - val unitText = if (isMetric) { - stringResource(string.metric) - } else { - stringResource(string.imperial) - } - Text( - text = unitText, - style = MaterialTheme.typography.bodyMedium - ) - Switch( - checked = isMetric, - onCheckedChange = { newValue -> - val newUnit = if (newValue) 0 else 1 - viewModel.setDistanceUnit(newUnit) - } - ) - } + // Update selected state when preference changes from outside + LaunchedEffect(distanceUnit) { + isChecked = isMetric } + + SwitchSetting( + title = stringResource(string.distance_unit_title), + description = stringResource(string.distance_unit_help_text), + isChecked = isChecked, + onCheckedChange = { newValue -> + isChecked = newValue + val newUnit = if (newValue) 0 else 1 + viewModel.setDistanceUnit(newUnit) + }, + enabledText = stringResource(string.metric), + disabledText = stringResource(string.imperial) + ) } @Composable private fun PeliasBaseUrlSetting(viewModel: SettingsViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) + SettingsItem( + title = stringResource(string.pelias_base_url_title), + description = "" ) { - Text( - text = stringResource(string.pelias_base_url_title), - style = MaterialTheme.typography.titleMedium - ) - val currentPeliasConfig by viewModel.peliasApiConfig.collectAsState() var peliasBaseUrl by remember { mutableStateOf(currentPeliasConfig.baseUrl) } @@ -289,19 +174,10 @@ private fun PeliasBaseUrlSetting(viewModel: SettingsViewModel) { @Composable private fun PeliasApiKeySetting(viewModel: SettingsViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) + SettingsItem( + title = stringResource(string.pelias_api_key_title), + description = "" ) { - Text( - text = stringResource(string.pelias_api_key_title), - style = MaterialTheme.typography.titleMedium - ) - val currentPeliasConfig by viewModel.peliasApiConfig.collectAsState() var peliasApiKey by remember { mutableStateOf( @@ -332,19 +208,10 @@ private fun PeliasApiKeySetting(viewModel: SettingsViewModel) { @Composable private fun ValhallaBaseUrlSetting(viewModel: SettingsViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) + SettingsItem( + title = stringResource(string.valhalla_base_url_title), + description = "" ) { - Text( - text = stringResource(string.valhalla_base_url_title), - style = MaterialTheme.typography.titleMedium - ) - val currentValhallaConfig by viewModel.valhallaApiConfig.collectAsState() var valhallaBaseUrl by remember { mutableStateOf(currentValhallaConfig.baseUrl) } @@ -370,19 +237,10 @@ private fun ValhallaBaseUrlSetting(viewModel: SettingsViewModel) { @Composable private fun ValhallaApiKeySetting(viewModel: SettingsViewModel) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) + SettingsItem( + title = stringResource(string.valhalla_api_key_title), + description = "" ) { - Text( - text = stringResource(string.valhalla_api_key_title), - style = MaterialTheme.typography.titleMedium - ) - val currentValhallaConfig by viewModel.valhallaApiConfig.collectAsState() var valhallaApiKey by remember { mutableStateOf( @@ -410,108 +268,3 @@ private fun ValhallaApiKeySetting(viewModel: SettingsViewModel) { ) } } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AdvancedSettingsScreen( - viewModel: SettingsViewModel = hiltViewModel() -) { - val snackBarHostState = remember { SnackbarHostState() } - Scaffold( - snackbarHost = { SnackbarHost(snackBarHostState) }, - contentWindowInsets = WindowInsets.safeDrawing, - topBar = { - TopAppBar(title = { - Text( - text = stringResource(string.advanced_settings_title), - style = MaterialTheme.typography.headlineMedium, - fontWeight = androidx.compose.ui.text.font.FontWeight.Bold - ) - - }) - }, - content = { padding -> - Box(modifier = Modifier.padding(padding)) { - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) - - ContinuousLocationTrackingSetting(viewModel) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) - - ShowZoomFabsSetting(viewModel) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) - - TimeFormatSetting(viewModel) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) - - DistanceUnitSetting(viewModel) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) - - // NEW SETTINGS GO HERE. - - PeliasBaseUrlSetting(viewModel) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) - - PeliasApiKeySetting(viewModel) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) - - ValhallaBaseUrlSetting(viewModel) - - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) - - ValhallaApiKeySetting(viewModel) - - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(TOOLBAR_HEIGHT_DP) - ) - } - } - } - ) -} diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/PrivacySettingsScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/PrivacySettingsScreen.kt index bce7807da792111f628e7f49840432028025f383..e1eb7f18dbcb017f3785448ddc3eda592d0dca50 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/PrivacySettingsScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/PrivacySettingsScreen.kt @@ -18,212 +18,56 @@ package earth.maps.cardinal.ui.settings -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.DividerDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -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.res.dimensionResource -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import earth.maps.cardinal.R.dimen import earth.maps.cardinal.R.drawable import earth.maps.cardinal.R.string -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PrivacySettingsScreen( viewModel: SettingsViewModel = hiltViewModel(), onDismiss: () -> Unit, onNavigateToOfflineAreas: () -> Unit, ) { - Scaffold( - snackbarHost = { SnackbarHost(remember { SnackbarHostState() }) }, - contentWindowInsets = WindowInsets.safeDrawing, - topBar = { - TopAppBar(title = { - Text( - text = stringResource(string.privacy_settings_title), - style = MaterialTheme.typography.headlineMedium, - fontWeight = androidx.compose.ui.text.font.FontWeight.Bold + SettingsScreenScaffold( + title = stringResource(string.privacy_settings_title), + onDismiss = onDismiss + ) { padding -> + Column(modifier = Modifier.padding(padding)) { + SettingsDivider() + SettingsScrollableContent { + // Offline Areas Settings Item + SettingsItem( + title = stringResource(string.offline_areas_title), + description = stringResource(string.offline_areas_help_text), + iconResId = drawable.cloud_download_24dp, + onClick = onNavigateToOfflineAreas ) - }) - }, - content = { padding -> - Box(modifier = Modifier.padding(padding)) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - // Offline Areas Settings Item - Column( - modifier = Modifier - .fillMaxWidth() - .clickable { onNavigateToOfflineAreas() } - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(string.offline_areas_title), - style = MaterialTheme.typography.titleMedium - ) - Icon( - painter = painterResource(drawable.cloud_download_24dp), - contentDescription = null - ) - } - Text( - text = stringResource(string.offline_areas_help_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) + SettingsDivider() - // Offline Mode Settings Item - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - ) - ) { - Text( - text = stringResource(string.offline_mode_title), - style = MaterialTheme.typography.titleMedium - ) - Text( - text = stringResource(string.offline_mode_help_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - // Offline mode toggle - val currentOfflineMode by viewModel.offlineMode.collectAsState() - var isOfflineModeEnabled by remember { mutableStateOf(currentOfflineMode) } - - // Update selected state when preference changes from outside - LaunchedEffect(currentOfflineMode) { - isOfflineModeEnabled = currentOfflineMode - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = if (isOfflineModeEnabled) stringResource(string.enabled) else stringResource( - string.disabled - ), style = MaterialTheme.typography.bodyMedium - ) - Switch( - checked = isOfflineModeEnabled, onCheckedChange = { newValue -> - isOfflineModeEnabled = newValue - viewModel.setOfflineMode(newValue) - }) - } - - // Allow transit in offline mode toggle - Column( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - ) { - Text( - text = stringResource(string.allow_transit_in_offline_mode_title), - style = MaterialTheme.typography.titleMedium - ) - Text( - text = stringResource(string.allow_transit_in_offline_mode_help_text), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - val currentAllowTransitInOfflineMode by viewModel.allowTransitInOfflineMode.collectAsState() - var isAllowTransitInOfflineModeEnabled by remember { - mutableStateOf( - currentAllowTransitInOfflineMode - ) - } - - // Update selected state when preference changes from outside - LaunchedEffect(currentAllowTransitInOfflineMode) { - isAllowTransitInOfflineModeEnabled = - currentAllowTransitInOfflineMode - } + StatefulSwitchSetting( + title = stringResource(string.offline_mode_title), + description = stringResource(string.offline_mode_help_text), + stateFlow = viewModel.offlineMode, + onStateChanged = viewModel::setOfflineMode + ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = if (isAllowTransitInOfflineModeEnabled) stringResource( - string.enabled - ) else stringResource( - string.disabled - ), style = MaterialTheme.typography.bodyMedium - ) - Switch( - checked = isAllowTransitInOfflineModeEnabled, - onCheckedChange = { newValue -> - isAllowTransitInOfflineModeEnabled = newValue - viewModel.setAllowTransitInOfflineMode(newValue) - }) - } - } - } - } + SettingsDivider() + StatefulSwitchSetting( + title = stringResource(string.allow_transit_in_offline_mode_title), + description = stringResource(string.allow_transit_in_offline_mode_help_text), + stateFlow = viewModel.allowTransitInOfflineMode, + onStateChanged = viewModel::setAllowTransitInOfflineMode + ) } } - ) + } } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/RoutingProfilesScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/RoutingProfilesScreen.kt index 892bbf59da96da594f3034c48210105103de1069..f83a4aa1e085860e722b890473f48321ce29db94 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/RoutingProfilesScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/RoutingProfilesScreen.kt @@ -22,29 +22,21 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -71,48 +63,33 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -@OptIn(ExperimentalMaterial3Api::class) @Composable fun RoutingProfilesScreen( - viewModel: RoutingProfilesViewModel = hiltViewModel(), navController: NavController + viewModel: RoutingProfilesViewModel = hiltViewModel(), + navController: NavController ) { val allProfiles by viewModel.allProfiles.collectAsState() - val snackBarHostState = remember { SnackbarHostState() } - Scaffold( - snackbarHost = { SnackbarHost(snackBarHostState) }, - contentWindowInsets = WindowInsets.safeDrawing, - topBar = { - TopAppBar( - title = { - Text( - text = stringResource(string.routing_profiles), - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold - ) - } - ) - }, - content = { padding -> - Box(modifier = Modifier.padding(padding)) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(dimensionResource(dimen.padding)) - ) { + + SettingsScreenScaffold( + title = stringResource(string.routing_profiles) + ) { padding -> + Column(modifier = Modifier.padding(padding)) { + SettingsDivider() + Box { + SettingsScrollableContent { Column( - modifier = Modifier - .fillMaxSize() - .padding(top = dimensionResource(dimen.padding)) + modifier = Modifier.padding(dimensionResource(dimen.padding)) ) { if (allProfiles.isEmpty()) { Text( text = stringResource(string.no_routing_profiles_configured_yet) ) } + // Group profiles by routing mode RoutingMode.entries.forEach { mode -> - val modeProfiles = allProfiles.filter { it.routingMode == mode.value } + val modeProfiles = + allProfiles.filter { it.routingMode == mode.value } if (modeProfiles.isNotEmpty()) { RoutingModeSection( @@ -124,7 +101,8 @@ fun RoutingProfilesScreen( }, onDelete = { profile -> viewModel.deleteProfile(profile.id) - }) + } + ) } } } @@ -155,7 +133,7 @@ fun RoutingProfilesScreen( } } } - ) + } } @Composable diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/SettingsComponents.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/SettingsComponents.kt new file mode 100644 index 0000000000000000000000000000000000000000..b48adb02159e9abdb29a55d598e9a1633178d98b --- /dev/null +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/SettingsComponents.kt @@ -0,0 +1,325 @@ +/* + * Cardinal Maps + * Copyright (C) 2025 Cardinal Maps Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package earth.maps.cardinal.ui.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.DividerDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +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.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import earth.maps.cardinal.R.dimen +import earth.maps.cardinal.R.drawable +import earth.maps.cardinal.R.string +import earth.maps.cardinal.ui.core.TOOLBAR_HEIGHT_DP + +/** + * Common preference option component with radio buttons + */ +@Composable +fun PreferenceOption( + selectedValue: T, + options: List>, + onOptionSelected: (T) -> Unit +) { + Column { + options.forEach { (value, labelResId) -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onOptionSelected(value) + } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedValue == value, + onClick = { + onOptionSelected(value) + } + ) + Text( + text = stringResource(labelResId), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } +} + +/** + * Common settings screen scaffold with top bar + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreenScaffold( + title: String, + navController: NavController? = null, + onDismiss: (() -> Unit)? = null, + showCloseButton: Boolean = false, + content: @Composable (paddingValues: PaddingValues) -> Unit +) { + val snackBarHostState = remember { SnackbarHostState() } + + Scaffold( + snackbarHost = { SnackbarHost(snackBarHostState) }, + contentWindowInsets = WindowInsets.safeDrawing, + topBar = { + TopAppBar(title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold + ) + + if (showCloseButton) { + Spacer(modifier = Modifier.fillMaxWidth()) + IconButton(onClick = { + navController?.popBackStack() ?: onDismiss?.invoke() + }) { + Icon( + painter = painterResource(drawable.ic_close), + contentDescription = stringResource(string.close) + ) + } + } + } + }) + }, + content = content + ) +} + +/** + * Common settings item with title, description, and optional icon + */ +@Composable +fun SettingsItem( + title: String, + description: String, + iconResId: Int? = null, + onClick: (() -> Unit)? = null, + content: @Composable (() -> Unit)? = null +) { + Column( + modifier = Modifier + .fillMaxWidth() + .let { modifier -> + if (onClick != null) { + modifier.clickable { onClick() } + } else { + modifier + } + } + .padding( + horizontal = dimensionResource(dimen.padding), + vertical = dimensionResource(dimen.padding_minor) + ) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + if (iconResId != null) { + Icon( + painter = painterResource(iconResId), + contentDescription = null + ) + } + } + + content?.invoke() + } +} + +/** + * Common settings divider + */ +@Composable +fun SettingsDivider() { + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + thickness = DividerDefaults.Thickness, + color = MaterialTheme.colorScheme.outlineVariant + ) +} + +/** + * Common switch setting component + */ +@Composable +fun SwitchSetting( + title: String, + description: String, + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit, + enabledText: String = stringResource(string.enabled), + disabledText: String = stringResource(string.disabled) +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = dimensionResource(dimen.padding), + vertical = dimensionResource(dimen.padding_minor) + ) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = if (isChecked) enabledText else disabledText, + style = MaterialTheme.typography.bodyMedium + ) + Switch( + checked = isChecked, + onCheckedChange = onCheckedChange + ) + } + } +} + +/** + * Common switch setting with state management + */ +@Composable +fun StatefulSwitchSetting( + title: String, + description: String, + stateFlow: kotlinx.coroutines.flow.StateFlow, + onStateChanged: (Boolean) -> Unit, + enabledText: String = stringResource(string.enabled), + disabledText: String = stringResource(string.disabled) +) { + val currentState by stateFlow.collectAsState() + var isChecked by remember { mutableStateOf(currentState) } + + // Update selected state when preference changes from outside + LaunchedEffect(currentState) { + isChecked = currentState + } + + SwitchSetting( + title = title, + description = description, + isChecked = isChecked, + onCheckedChange = { newValue -> + isChecked = newValue + onStateChanged(newValue) + }, + enabledText = enabledText, + disabledText = disabledText + ) +} + +/** + * Common scrollable content wrapper for settings screens + */ +@Composable +fun SettingsScrollableContent( + content: @Composable () -> Unit, +) { + Box { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + content() + + // Add bottom padding to ensure proper spacing + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(TOOLBAR_HEIGHT_DP) + ) + } + } +} \ No newline at end of file diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/SettingsScreen.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/SettingsScreen.kt index 8191804639f53038ab357b77e25b582c27770588..66d2b609a683da5f33232054fe0e9031a99a39cd 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/SettingsScreen.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/settings/SettingsScreen.kt @@ -18,44 +18,20 @@ package earth.maps.cardinal.ui.settings -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.DividerDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -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.res.dimensionResource -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import androidx.navigation.NavController import earth.maps.cardinal.R.dimen import earth.maps.cardinal.R.drawable @@ -63,248 +39,82 @@ import earth.maps.cardinal.R.string import earth.maps.cardinal.ui.core.NavigationUtils import earth.maps.cardinal.ui.core.Screen - @Composable -fun PreferenceOption( - selectedValue: T, options: List>, onOptionSelected: (T) -> Unit +fun SettingsScreen( + navController: NavController, + viewModel: SettingsViewModel, ) { - Column { - options.forEach { (value, labelResId) -> + SettingsScreenScaffold( + title = stringResource(string.app_name_long), + navController = navController, + showCloseButton = true + ) { padding -> + Column(modifier = Modifier.padding(padding)) { + // Custom title with version Row( modifier = Modifier .fillMaxWidth() - .clickable { - onOptionSelected(value) - } - .padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) { - RadioButton( - selected = selectedValue == value, onClick = { - onOptionSelected(value) - }) + .padding(horizontal = dimensionResource(dimen.padding)), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { Text( - text = stringResource(labelResId), + modifier = Modifier.padding(start = dimensionResource(dimen.padding)), + text = viewModel.getVersionName() ?: "v?.?.?", style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(start = 8.dp) + fontStyle = FontStyle.Italic ) } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SettingsScreen( - navController: NavController, - viewModel: SettingsViewModel, -) { - Scaffold( - snackbarHost = { SnackbarHost(remember { SnackbarHostState() }) }, - contentWindowInsets = WindowInsets.safeDrawing, - topBar = { - TopAppBar(title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(string.app_name_long), - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold - ) - Text( - modifier = Modifier.padding(start = dimensionResource(dimen.padding)), - text = viewModel.getVersionName() ?: "v?.?.?", - style = MaterialTheme.typography.bodyMedium, - fontStyle = FontStyle.Italic - ) - Spacer(modifier = Modifier.fillMaxWidth()) - IconButton(onClick = { - navController.popBackStack() - }) { - Icon( - painter = painterResource(drawable.ic_close), - contentDescription = stringResource(string.close) - ) - } - } - }) - }, - content = { padding -> - Box(modifier = Modifier.padding(padding)) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) + SettingsDivider() + SettingsScrollableContent { - // Routing Profiles Settings Item - Column( - modifier = Modifier - .fillMaxWidth() - .clickable { - NavigationUtils.navigate( - navController, - Screen.RoutingProfiles - ) - } - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - )) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(string.routing_profiles), - style = MaterialTheme.typography.titleMedium - ) - Icon( - painter = painterResource(drawable.commute_icon), - contentDescription = null - ) - } - Text( - text = stringResource(string.create_and_manage_custom_routing_profiles), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + // Routing Profiles Settings Item + SettingsItem( + title = stringResource(string.routing_profiles), + description = stringResource(string.create_and_manage_custom_routing_profiles), + iconResId = drawable.commute_icon, + onClick = { + NavigationUtils.navigate(navController, Screen.RoutingProfiles) } + ) - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) + SettingsDivider() - // Privacy Settings Item - Column( - modifier = Modifier - .fillMaxWidth() - .clickable { - NavigationUtils.navigate( - navController, - Screen.OfflineSettings - ) - } - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - )) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(string.privacy_settings_title), - style = MaterialTheme.typography.titleMedium - ) - Icon( - painter = painterResource(drawable.ic_offline), - contentDescription = null - ) - } - Text( - text = stringResource(string.privacy_settings_summary), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + // Privacy Settings Item + SettingsItem( + title = stringResource(string.privacy_settings_title), + description = stringResource(string.privacy_settings_summary), + iconResId = drawable.ic_offline, + onClick = { + NavigationUtils.navigate(navController, Screen.OfflineSettings) } + ) - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) + SettingsDivider() - // Accessibility Settings Item - Column( - modifier = Modifier - .fillMaxWidth() - .clickable { - NavigationUtils.navigate( - navController, - Screen.AccessibilitySettings - ) - } - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - )) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(string.accessibility_settings_title), - style = MaterialTheme.typography.titleMedium - ) - Icon( - painter = painterResource(drawable.ic_accessiblity_settings), - contentDescription = null - ) - } - Text( - text = stringResource(string.accessibility_settings_summary), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + // Accessibility Settings Item + SettingsItem( + title = stringResource(string.accessibility_settings_title), + description = stringResource(string.accessibility_settings_summary), + iconResId = drawable.ic_accessiblity_settings, + onClick = { + NavigationUtils.navigate(navController, Screen.AccessibilitySettings) } + ) - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp), - thickness = DividerDefaults.Thickness, - color = MaterialTheme.colorScheme.outlineVariant - ) + SettingsDivider() - // Advanced Settings Item - Column( - modifier = Modifier - .fillMaxWidth() - .clickable { - NavigationUtils.navigate( - navController, - Screen.AdvancedSettings - ) - } - .padding( - horizontal = dimensionResource(dimen.padding), - vertical = dimensionResource(dimen.padding_minor) - )) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(string.advanced_settings_title), - style = MaterialTheme.typography.titleMedium - ) - Icon( - painter = painterResource(drawable.ic_settings), - contentDescription = null - ) - } - Text( - text = stringResource(string.advanced_settings_summary), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + // Advanced Settings Item + SettingsItem( + title = stringResource(string.advanced_settings_title), + description = stringResource(string.advanced_settings_summary), + iconResId = drawable.ic_settings, + onClick = { + NavigationUtils.navigate(navController, Screen.AdvancedSettings) } - - // Add some bottom padding to ensure proper spacing - Box(modifier = Modifier.padding(bottom = 8.dp)) - } + ) } - }) + } + } } diff --git a/cardinal-android/app/src/test/java/earth/maps/cardinal/ui/home/HomeViewModelTest.kt b/cardinal-android/app/src/test/java/earth/maps/cardinal/ui/home/HomeViewModelTest.kt index f2c58ebb882bd5efb26efb73e36405e8861705f7..5845908843575b5e2ada6195669967d12cfe6022 100644 --- a/cardinal-android/app/src/test/java/earth/maps/cardinal/ui/home/HomeViewModelTest.kt +++ b/cardinal-android/app/src/test/java/earth/maps/cardinal/ui/home/HomeViewModelTest.kt @@ -126,7 +126,7 @@ class HomeViewModelTest { @Test fun `initial state should be correct`() { - assertEquals(TextFieldValue(), viewModel.searchQuery) + assertEquals(TextFieldValue(), viewModel.searchQueryValue) assertEquals(emptyList(), viewModel.geocodeResults.value) assertFalse(viewModel.isSearching) assertNull(viewModel.searchError) @@ -138,7 +138,7 @@ class HomeViewModelTest { viewModel.updateSearchQuery(newQuery) - assertEquals(newQuery, viewModel.searchQuery) + assertEquals(newQuery, viewModel.searchQueryValue) } @Test