Skip to content

Commit a6b75db

Browse files
authored
Merge pull request #195 from ellenhp/ellenhp/localization_settings
Distance unit preference and 12/24 hour time
2 parents 8626709 + 293a1d8 commit a6b75db

File tree

10 files changed

+314
-97
lines changed

10 files changed

+314
-97
lines changed

cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferenceRepository.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package earth.maps.cardinal.data
1818

1919
import android.content.Context
20+
import android.text.format.DateFormat
2021
import kotlinx.coroutines.CoroutineScope
2122
import kotlinx.coroutines.Dispatchers
2223
import kotlinx.coroutines.SupervisorJob
@@ -64,6 +65,9 @@ class AppPreferenceRepository @Inject constructor(
6465
private val _showZoomFabs = MutableStateFlow(true)
6566
val showZoomFabs: StateFlow<Boolean> = _showZoomFabs.asStateFlow()
6667

68+
private val _use24HourFormat = MutableStateFlow(DateFormat.is24HourFormat(context))
69+
val use24HourFormat: StateFlow<Boolean> = _use24HourFormat.asStateFlow()
70+
6771
private val _lastRoutingMode = MutableStateFlow(appPreferences.loadLastRoutingMode())
6872
val lastRoutingMode: StateFlow<String> = _lastRoutingMode.asStateFlow()
6973

@@ -93,6 +97,7 @@ class AppPreferenceRepository @Inject constructor(
9397
loadAllowTransitInOfflineMode()
9498
loadContinuousLocationTracking()
9599
loadShowZoomFabs()
100+
loadUse24HourFormat()
96101
loadLastRoutingMode()
97102
loadApiConfigurations()
98103
}
@@ -160,6 +165,11 @@ class AppPreferenceRepository @Inject constructor(
160165
_showZoomFabs.value = show
161166
}
162167

168+
private fun loadUse24HourFormat() {
169+
val use24Hour = appPreferences.loadUse24HourFormat()
170+
_use24HourFormat.value = use24Hour
171+
}
172+
163173
fun setAllowTransitInOfflineMode(allowTransitInOfflineMode: Boolean) {
164174
_allowTransitInOfflineMode.value = allowTransitInOfflineMode
165175
viewModelScope.launch {
@@ -181,6 +191,13 @@ class AppPreferenceRepository @Inject constructor(
181191
}
182192
}
183193

194+
fun setUse24HourFormat(use24Hour: Boolean) {
195+
_use24HourFormat.value = use24Hour
196+
viewModelScope.launch {
197+
appPreferences.saveUse24HourFormat(use24Hour)
198+
}
199+
}
200+
184201
private fun loadLastRoutingMode() {
185202
val mode = appPreferences.loadLastRoutingMode()
186203
_lastRoutingMode.value = mode

cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferences.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ package earth.maps.cardinal.data
1818

1919
import android.content.Context
2020
import android.content.SharedPreferences
21+
import android.text.format.DateFormat
2122
import androidx.core.content.edit
2223
import java.util.Locale
2324

2425
/**
2526
* Helper class to save and load app preferences using SharedPreferences.
2627
*/
27-
class AppPreferences(context: Context) {
28+
class AppPreferences(private val context: Context) {
2829
private val prefs: SharedPreferences =
2930
context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
3031

@@ -41,6 +42,8 @@ class AppPreferences(context: Context) {
4142

4243
private const val KEY_LAST_ROUTING_MODE = "last_routing_mode"
4344

45+
private const val KEY_USE_24_HOUR_FORMAT = "use_24_hour_format"
46+
4447
// API configuration keys
4548
private const val KEY_PELIAS_BASE_URL = "pelias_base_url"
4649
private const val KEY_PELIAS_API_KEY = "pelias_api_key"
@@ -263,6 +266,25 @@ class AppPreferences(context: Context) {
263266
?: DEFAULT_LAST_ROUTING_MODE
264267
}
265268

269+
/**
270+
* Saves the use 24-hour format preference.
271+
*/
272+
fun saveUse24HourFormat(use24Hour: Boolean) {
273+
prefs.edit {
274+
putBoolean(KEY_USE_24_HOUR_FORMAT, use24Hour)
275+
}
276+
}
277+
278+
/**
279+
* Loads the saved use 24-hour format preference.
280+
* Returns the system default as default value.
281+
*/
282+
fun loadUse24HourFormat(): Boolean {
283+
val systemDefault = DateFormat.is24HourFormat(context)
284+
// Note: we're using the system default as the fallback, but storing user preference
285+
return prefs.getBoolean(KEY_USE_24_HOUR_FORMAT, systemDefault)
286+
}
287+
266288
/**
267289
* Gets the default distance unit based on the system locale.
268290
* Returns imperial for countries that use imperial system (US, Liberia, Myanmar),

cardinal-android/app/src/main/java/earth/maps/cardinal/data/GeoUtils.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ object GeoUtils {
3232
private const val METERS_TO_MILES = 1609.34
3333
private const val METERS_TO_FEET = 3.28084
3434

35+
private const val SHORT_DISTANCE_THRESHOLD_METERS = 200.0
36+
3537
/**
3638
* Formats a distance in meters to a human-readable string based on the unit preference.
3739
*
@@ -98,6 +100,9 @@ object GeoUtils {
98100
* @return Formatted short distance string (e.g., "150 m" or "490 ft")
99101
*/
100102
fun formatShortDistance(meters: Double, unitPreference: Int): String {
103+
if (meters > SHORT_DISTANCE_THRESHOLD_METERS) {
104+
return formatDistance(meters, unitPreference)
105+
}
101106
return when (unitPreference) {
102107
AppPreferences.DISTANCE_UNIT_METRIC -> "${meters.roundToInt()} m"
103108
AppPreferences.DISTANCE_UNIT_IMPERIAL -> {
@@ -147,7 +152,8 @@ object GeoUtils {
147152

148153
// Calculate the approximate delta in degrees for the given radius
149154
val latDelta = Math.toDegrees(radiusMeters / earthRadius)
150-
val lonDelta = Math.toDegrees(radiusMeters / (earthRadius * cos(Math.toRadians(center.latitude))))
155+
val lonDelta =
156+
Math.toDegrees(radiusMeters / (earthRadius * cos(Math.toRadians(center.latitude))))
151157

152158
// Create the bounding box with north, south, east, west boundaries
153159
return BoundingBox(

cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -737,8 +737,7 @@ fun AppContent(
737737
try {
738738
val positions =
739739
earth.maps.cardinal.data.PolylineUtils.decodePolyline(
740-
geometry.points,
741-
geometry.precision
740+
geometry.points, geometry.precision
742741
)
743742
allPositions.addAll(positions)
744743
} catch (e: Exception) {
@@ -789,10 +788,9 @@ fun AppContent(
789788
peekHeight = peekHeight,
790789
content = {
791790
earth.maps.cardinal.ui.directions.TransitItineraryDetailScreen(
792-
itinerary = itinerary,
793-
onBack = {
791+
itinerary = itinerary, onBack = {
794792
navController.popBackStack()
795-
}
793+
}, appPreferences = appPreferenceRepository
796794
)
797795
},
798796
showToolbar = false,
@@ -842,12 +840,10 @@ fun AppContent(
842840
modifier = Modifier.align(Alignment.BottomCenter),
843841
visible = showToolbar,
844842
enter = slideInVertically(
845-
initialOffsetY = { it },
846-
animationSpec = tween(300)
843+
initialOffsetY = { it }, animationSpec = tween(300)
847844
),
848845
exit = slideOutVertically(
849-
targetOffsetY = { it },
850-
animationSpec = tween(300)
846+
targetOffsetY = { it }, animationSpec = tween(300)
851847
),
852848
) {
853849
CardinalToolbar(navController, onSearchDoublePress = { homeViewModel.expandSearch() })
@@ -858,15 +854,12 @@ fun AppContent(
858854
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
859855
@Composable
860856
private fun CardinalToolbar(
861-
navController: NavController,
862-
onSearchDoublePress: (() -> Unit)? = null
857+
navController: NavController, onSearchDoublePress: (() -> Unit)? = null
863858
) {
864859
FlexibleBottomAppBar {
865860
IconButton(onClick = {
866861
NavigationUtils.navigate(
867-
navController,
868-
screen = Screen.ManagePlaces(null),
869-
popUpToHome = true
862+
navController, screen = Screen.ManagePlaces(null), popUpToHome = true
870863
)
871864
}) {
872865
Icon(
@@ -888,9 +881,7 @@ private fun CardinalToolbar(
888881
onSearchDoublePress?.invoke()
889882
} else {
890883
NavigationUtils.navigate(
891-
navController,
892-
screen = Screen.HomeSearch,
893-
popUpToHome = true
884+
navController, screen = Screen.HomeSearch, popUpToHome = true
894885
)
895886
}
896887
}) {
@@ -901,9 +892,7 @@ private fun CardinalToolbar(
901892
}
902893
IconButton(onClick = {
903894
NavigationUtils.navigate(
904-
navController,
905-
screen = Screen.NearbyTransit,
906-
popUpToHome = true
895+
navController, screen = Screen.NearbyTransit, popUpToHome = true
907896
)
908897
}) {
909898
Icon(
@@ -915,9 +904,7 @@ private fun CardinalToolbar(
915904
}
916905
IconButton(onClick = {
917906
NavigationUtils.navigate(
918-
navController,
919-
screen = Screen.OfflineAreas,
920-
popUpToHome = true
907+
navController, screen = Screen.OfflineAreas, popUpToHome = true
921908
)
922909
}) {
923910
Icon(

cardinal-android/app/src/main/java/earth/maps/cardinal/ui/directions/DirectionsScreen.kt

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,11 @@ fun DirectionsScreen(
301301

302302
planState.planResponse != null -> {
303303
TransitDirectionsScreen(
304-
viewModel = viewModel,
305-
onItineraryClick = { itinerary ->
304+
viewModel = viewModel, onItineraryClick = { itinerary ->
306305
NavigationUtils.navigate(
307-
navController,
308-
Screen.TransitItineraryDetail(itinerary)
306+
navController, Screen.TransitItineraryDetail(itinerary)
309307
)
310-
}
308+
}, appPreferences = appPreferences
311309
)
312310
}
313311

@@ -408,29 +406,29 @@ fun DirectionsScreen(
408406
// Show quick suggestions when no search query
409407
QuickSuggestions(
410408
onMyLocationSelected = {
411-
// Check permissions before attempting to get location
412-
if (hasLocationPermission) {
413-
// Launch coroutine to get current location
414-
coroutineScope.launch {
415-
val myLocationPlace = viewModel.getCurrentLocationAsPlace()
416-
myLocationPlace?.let { place ->
417-
// Update the appropriate place based on which field is focused
418-
if (fieldFocusState == FieldFocusState.FROM) {
419-
viewModel.updateFromPlace(place)
420-
} else {
421-
viewModel.updateToPlace(place)
422-
}
423-
// Clear focus state after selection
424-
fieldFocusState = FieldFocusState.NONE
409+
// Check permissions before attempting to get location
410+
if (hasLocationPermission) {
411+
// Launch coroutine to get current location
412+
coroutineScope.launch {
413+
val myLocationPlace = viewModel.getCurrentLocationAsPlace()
414+
myLocationPlace?.let { place ->
415+
// Update the appropriate place based on which field is focused
416+
if (fieldFocusState == FieldFocusState.FROM) {
417+
viewModel.updateFromPlace(place)
418+
} else {
419+
viewModel.updateToPlace(place)
425420
}
421+
// Clear focus state after selection
422+
fieldFocusState = FieldFocusState.NONE
426423
}
427-
} else {
428-
// Set pending request for auto-retry after permission grant
429-
pendingLocationRequest = fieldFocusState
430-
// Request location permission
431-
onRequestLocationPermission()
432424
}
433-
},
425+
} else {
426+
// Set pending request for auto-retry after permission grant
427+
pendingLocationRequest = fieldFocusState
428+
// Request location permission
429+
onRequestLocationPermission()
430+
}
431+
},
434432
savedPlaces = savedPlaces,
435433
onSavedPlaceSelected = { place ->
436434
// Update the appropriate place based on which field is focused
@@ -682,8 +680,7 @@ private fun RoutingProfileSelector(
682680
val profileOptions = listOf(null) + availableProfiles
683681

684682
SingleChoiceSegmentedButtonRow(
685-
modifier = modifier
686-
.fillMaxWidth()
683+
modifier = modifier.fillMaxWidth()
687684
) {
688685
profileOptions.forEach { profile ->
689686
val isSelected = selectedProfile?.id == profile?.id

0 commit comments

Comments
 (0)