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