Loading cardinal-android/app/src/main/java/earth/maps/cardinal/MainActivity.kt +17 −1 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue Loading @@ -54,6 +55,7 @@ import earth.maps.cardinal.tileserver.PermissionRequest import earth.maps.cardinal.tileserver.PermissionRequestManager import earth.maps.cardinal.ui.core.AppContent import earth.maps.cardinal.ui.core.MapViewModel import earth.maps.cardinal.ui.settings.ThemeModePromptBottomSheet import earth.maps.cardinal.ui.theme.AppTheme import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers Loading Loading @@ -226,7 +228,12 @@ class MainActivity : ComponentActivity() { setContent { val contrastLevel by appPreferenceRepository.contrastLevel.collectAsState() AppTheme(contrastLevel = contrastLevel) { val themeMode by appPreferenceRepository.themeMode.collectAsState() val hasPromptedThemeMode by appPreferenceRepository.hasPromptedThemeMode.collectAsState() val systemInDarkTheme = isSystemInDarkTheme() val darkTheme = themeMode.shouldUseDarkTheme(systemInDarkTheme) AppTheme(darkTheme = darkTheme, contrastLevel = contrastLevel) { val mapViewModel: MapViewModel = hiltViewModel() val navController = rememberNavController() Loading Loading @@ -259,6 +266,15 @@ class MainActivity : ComponentActivity() { requestLocationPermission() } ) if (systemInDarkTheme && !hasPromptedThemeMode) { ThemeModePromptBottomSheet( onSave = { selectedThemeMode -> appPreferenceRepository.setThemeMode(selectedThemeMode) appPreferenceRepository.setHasPromptedThemeMode(true) } ) } } } } Loading cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferenceRepository.kt +26 −0 Original line number Diff line number Diff line Loading @@ -57,6 +57,9 @@ class AppPreferenceRepository @Inject constructor( private val _distanceUnit = MutableStateFlow(AppPreferences.DISTANCE_UNIT_METRIC) val distanceUnit: StateFlow<Int> = _distanceUnit.asStateFlow() private val _themeMode = MutableStateFlow(ThemeMode.SYSTEM) val themeMode: StateFlow<ThemeMode> = _themeMode.asStateFlow() private val _allowTransitInOfflineMode = MutableStateFlow(true) val allowTransitInOfflineMode: StateFlow<Boolean> = _allowTransitInOfflineMode.asStateFlow() Loading @@ -76,6 +79,9 @@ class AppPreferenceRepository @Inject constructor( private val _hasPromptedLocation = MutableStateFlow(appPreferences.loadHasPromptedLocation()) val hasPromptedLocation: StateFlow<Boolean> = _hasPromptedLocation.asStateFlow() private val _hasPromptedThemeMode = MutableStateFlow(appPreferences.loadHasPromptedThemeMode()) val hasPromptedThemeMode: StateFlow<Boolean> = _hasPromptedThemeMode.asStateFlow() // Pelias API configuration private val _peliasApiConfig = MutableStateFlow( ApiConfiguration( Loading @@ -99,6 +105,7 @@ class AppPreferenceRepository @Inject constructor( loadAnimationSpeed() loadOfflineMode() loadDistanceUnit() loadThemeMode() loadAllowTransitInOfflineMode() loadContinuousLocationTracking() loadShowZoomFabs() Loading Loading @@ -155,6 +162,18 @@ class AppPreferenceRepository @Inject constructor( _distanceUnit.value = distanceUnit } fun setThemeMode(themeMode: ThemeMode) { _themeMode.value = themeMode viewModelScope.launch { appPreferences.saveThemeMode(themeMode) } } private fun loadThemeMode() { val themeMode = appPreferences.loadThemeMode() _themeMode.value = themeMode } private fun loadAllowTransitInOfflineMode() { val allowTransitInOfflineMode = appPreferences.loadAllowTransitInOfflineMode() _allowTransitInOfflineMode.value = allowTransitInOfflineMode Loading Loading @@ -222,6 +241,13 @@ class AppPreferenceRepository @Inject constructor( } } fun setHasPromptedThemeMode(hasPrompted: Boolean) { _hasPromptedThemeMode.value = hasPrompted viewModelScope.launch { appPreferences.saveHasPromptedThemeMode(hasPrompted) } } private fun loadApiConfigurations() { // Load Pelias configuration val peliasBaseUrl = appPreferences.loadPeliasBaseUrl() Loading cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferences.kt +36 −1 Original line number Diff line number Diff line Loading @@ -48,6 +48,8 @@ class AppPreferences(private val context: Context) { private const val KEY_USE_24_HOUR_FORMAT = "use_24_hour_format" private const val KEY_HAS_PROMPTED_LOCATION = "has_prompted_location" private const val KEY_THEME_MODE = "theme_mode" private const val KEY_HAS_PROMPTED_THEME_MODE = "has_prompted_theme_mode" // API configuration keys private const val KEY_PELIAS_BASE_URL = "pelias_base_url" Loading Loading @@ -75,6 +77,7 @@ class AppPreferences(private val context: Context) { // Distance unit constants const val DISTANCE_UNIT_METRIC = 0 const val DISTANCE_UNIT_IMPERIAL = 1 } /** Loading Loading @@ -305,6 +308,38 @@ class AppPreferences(private val context: Context) { return prefs.getBoolean(KEY_HAS_PROMPTED_LOCATION, false) } fun saveThemeMode(themeMode: ThemeMode) { prefs.edit { putString(KEY_THEME_MODE, themeMode.preferenceValue) } } fun loadThemeMode(): ThemeMode { val themeMode = runCatching { prefs.getString(KEY_THEME_MODE, null) }.getOrNull() if (themeMode != null) { return ThemeMode.fromPreferenceValue(themeMode) } val legacyThemeMode = runCatching { prefs.getInt(KEY_THEME_MODE, -1) }.getOrNull() return legacyThemeMode?.let(ThemeMode::fromLegacyPreferenceValue) ?: ThemeMode.SYSTEM } fun saveHasPromptedThemeMode(hasPrompted: Boolean) { prefs.edit { putBoolean(KEY_HAS_PROMPTED_THEME_MODE, hasPrompted) } } fun loadHasPromptedThemeMode(): Boolean { return prefs.getBoolean(KEY_HAS_PROMPTED_THEME_MODE, false) } /** * Gets the default distance unit based on the system locale. * Returns imperial for countries that use imperial system (US, Liberia, Myanmar), Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +3 −1 Original line number Diff line number Diff line Loading @@ -346,7 +346,7 @@ fun AppContent( } composable(Screen.TURN_BY_TURN) { backStackEntry -> TurnByTurnRoute(state, routeRepository, port, backStackEntry) TurnByTurnRoute(state, routeRepository, appPreferenceRepository, port, backStackEntry) } } Loading Loading @@ -1095,6 +1095,7 @@ private fun TransitItineraryDetailRoute( private fun TurnByTurnRoute( state: AppContentState, routeRepository: RouteRepository, appPreferenceRepository: AppPreferenceRepository, port: Int?, backStackEntry: NavBackStackEntry ) { Loading @@ -1120,6 +1121,7 @@ private fun TurnByTurnRoute( port = port, mode = routingMode, route = ferrostarRoute, appPreferenceRepository = appPreferenceRepository, ) } } Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/MapView.kt +9 −7 Original line number Diff line number Diff line Loading @@ -129,7 +129,9 @@ fun MapView( val pinFeatures = mapPins.map { mapViewModel.createFeatureFromPlace(it) } rememberCoroutineScope() val styleVariant = if (isSystemInDarkTheme()) "dark" else "light" val themeMode by appPreferences.themeMode.collectAsState() val useDarkTheme = themeMode.shouldUseDarkTheme(isSystemInDarkTheme()) val styleVariant = if (useDarkTheme) "dark" else "light" // Load saved viewport on initial composition LaunchedEffect(Unit) { Loading Loading @@ -186,7 +188,7 @@ fun MapView( val location by mapViewModel.locationFlow.collectAsState() val sensorHeading by mapViewModel.heading.collectAsState() val savedPlaces by mapViewModel.savedPlacesFlow.collectAsState(FeatureCollection()) FavoritesLayer(savedPlaces, mapPins, isSystemInDarkTheme()) FavoritesLayer(savedPlaces, mapPins, useDarkTheme) OfflineBoundsLayer(selectedOfflineArea) Loading @@ -194,7 +196,7 @@ fun MapView( TransitLayer(currentTransitItinerary) PinsLayer(pinFeatures, isSystemInDarkTheme()) PinsLayer(pinFeatures, useDarkTheme) location?.let { LocationPuck(it, sensorHeading) } } Loading Loading @@ -226,7 +228,7 @@ fun MapView( private fun FavoritesLayer( savedPlaces: FeatureCollection<Point, Map<String, JsonElement>>, activeMarkers: List<Place>, isSystemInDarkTheme: Boolean useDarkTheme: Boolean ) { val textColor = MaterialTheme.colorScheme.onSurface val activeMarkerIds = activeMarkers.mapNotNull { it.id } Loading @@ -235,7 +237,7 @@ private fun FavoritesLayer( source = rememberGeoJsonSource(GeoJsonData.JsonString(Json.encodeToString(savedPlaces))), iconAllowOverlap = const(true), iconImage = image( if (isSystemInDarkTheme) { if (useDarkTheme) { painterResource(drawable.ic_stars_dark) } else { painterResource(drawable.ic_stars_light) Loading Loading @@ -524,14 +526,14 @@ private fun TransitLayer(currentTransitItinerary: Itinerary?) { } @Composable private fun PinsLayer(pinFeatures: List<Feature<Point, Map<String, JsonElement>>>, isSystemInDarkTheme: Boolean) { private fun PinsLayer(pinFeatures: List<Feature<Point, Map<String, JsonElement>>>, useDarkTheme: Boolean) { SymbolLayer( id = "map_pins", source = rememberGeoJsonSource(GeoJsonData.JsonString(Json.encodeToString(FeatureCollection(features = pinFeatures)))), iconAllowOverlap = const(true), iconAnchor = const(SymbolAnchor.Bottom), iconImage = image( if (isSystemInDarkTheme) { if (useDarkTheme) { painterResource(drawable.map_pin_dark) } else { painterResource(drawable.map_pin_light) Loading Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/MainActivity.kt +17 −1 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue Loading @@ -54,6 +55,7 @@ import earth.maps.cardinal.tileserver.PermissionRequest import earth.maps.cardinal.tileserver.PermissionRequestManager import earth.maps.cardinal.ui.core.AppContent import earth.maps.cardinal.ui.core.MapViewModel import earth.maps.cardinal.ui.settings.ThemeModePromptBottomSheet import earth.maps.cardinal.ui.theme.AppTheme import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers Loading Loading @@ -226,7 +228,12 @@ class MainActivity : ComponentActivity() { setContent { val contrastLevel by appPreferenceRepository.contrastLevel.collectAsState() AppTheme(contrastLevel = contrastLevel) { val themeMode by appPreferenceRepository.themeMode.collectAsState() val hasPromptedThemeMode by appPreferenceRepository.hasPromptedThemeMode.collectAsState() val systemInDarkTheme = isSystemInDarkTheme() val darkTheme = themeMode.shouldUseDarkTheme(systemInDarkTheme) AppTheme(darkTheme = darkTheme, contrastLevel = contrastLevel) { val mapViewModel: MapViewModel = hiltViewModel() val navController = rememberNavController() Loading Loading @@ -259,6 +266,15 @@ class MainActivity : ComponentActivity() { requestLocationPermission() } ) if (systemInDarkTheme && !hasPromptedThemeMode) { ThemeModePromptBottomSheet( onSave = { selectedThemeMode -> appPreferenceRepository.setThemeMode(selectedThemeMode) appPreferenceRepository.setHasPromptedThemeMode(true) } ) } } } } Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferenceRepository.kt +26 −0 Original line number Diff line number Diff line Loading @@ -57,6 +57,9 @@ class AppPreferenceRepository @Inject constructor( private val _distanceUnit = MutableStateFlow(AppPreferences.DISTANCE_UNIT_METRIC) val distanceUnit: StateFlow<Int> = _distanceUnit.asStateFlow() private val _themeMode = MutableStateFlow(ThemeMode.SYSTEM) val themeMode: StateFlow<ThemeMode> = _themeMode.asStateFlow() private val _allowTransitInOfflineMode = MutableStateFlow(true) val allowTransitInOfflineMode: StateFlow<Boolean> = _allowTransitInOfflineMode.asStateFlow() Loading @@ -76,6 +79,9 @@ class AppPreferenceRepository @Inject constructor( private val _hasPromptedLocation = MutableStateFlow(appPreferences.loadHasPromptedLocation()) val hasPromptedLocation: StateFlow<Boolean> = _hasPromptedLocation.asStateFlow() private val _hasPromptedThemeMode = MutableStateFlow(appPreferences.loadHasPromptedThemeMode()) val hasPromptedThemeMode: StateFlow<Boolean> = _hasPromptedThemeMode.asStateFlow() // Pelias API configuration private val _peliasApiConfig = MutableStateFlow( ApiConfiguration( Loading @@ -99,6 +105,7 @@ class AppPreferenceRepository @Inject constructor( loadAnimationSpeed() loadOfflineMode() loadDistanceUnit() loadThemeMode() loadAllowTransitInOfflineMode() loadContinuousLocationTracking() loadShowZoomFabs() Loading Loading @@ -155,6 +162,18 @@ class AppPreferenceRepository @Inject constructor( _distanceUnit.value = distanceUnit } fun setThemeMode(themeMode: ThemeMode) { _themeMode.value = themeMode viewModelScope.launch { appPreferences.saveThemeMode(themeMode) } } private fun loadThemeMode() { val themeMode = appPreferences.loadThemeMode() _themeMode.value = themeMode } private fun loadAllowTransitInOfflineMode() { val allowTransitInOfflineMode = appPreferences.loadAllowTransitInOfflineMode() _allowTransitInOfflineMode.value = allowTransitInOfflineMode Loading Loading @@ -222,6 +241,13 @@ class AppPreferenceRepository @Inject constructor( } } fun setHasPromptedThemeMode(hasPrompted: Boolean) { _hasPromptedThemeMode.value = hasPrompted viewModelScope.launch { appPreferences.saveHasPromptedThemeMode(hasPrompted) } } private fun loadApiConfigurations() { // Load Pelias configuration val peliasBaseUrl = appPreferences.loadPeliasBaseUrl() Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferences.kt +36 −1 Original line number Diff line number Diff line Loading @@ -48,6 +48,8 @@ class AppPreferences(private val context: Context) { private const val KEY_USE_24_HOUR_FORMAT = "use_24_hour_format" private const val KEY_HAS_PROMPTED_LOCATION = "has_prompted_location" private const val KEY_THEME_MODE = "theme_mode" private const val KEY_HAS_PROMPTED_THEME_MODE = "has_prompted_theme_mode" // API configuration keys private const val KEY_PELIAS_BASE_URL = "pelias_base_url" Loading Loading @@ -75,6 +77,7 @@ class AppPreferences(private val context: Context) { // Distance unit constants const val DISTANCE_UNIT_METRIC = 0 const val DISTANCE_UNIT_IMPERIAL = 1 } /** Loading Loading @@ -305,6 +308,38 @@ class AppPreferences(private val context: Context) { return prefs.getBoolean(KEY_HAS_PROMPTED_LOCATION, false) } fun saveThemeMode(themeMode: ThemeMode) { prefs.edit { putString(KEY_THEME_MODE, themeMode.preferenceValue) } } fun loadThemeMode(): ThemeMode { val themeMode = runCatching { prefs.getString(KEY_THEME_MODE, null) }.getOrNull() if (themeMode != null) { return ThemeMode.fromPreferenceValue(themeMode) } val legacyThemeMode = runCatching { prefs.getInt(KEY_THEME_MODE, -1) }.getOrNull() return legacyThemeMode?.let(ThemeMode::fromLegacyPreferenceValue) ?: ThemeMode.SYSTEM } fun saveHasPromptedThemeMode(hasPrompted: Boolean) { prefs.edit { putBoolean(KEY_HAS_PROMPTED_THEME_MODE, hasPrompted) } } fun loadHasPromptedThemeMode(): Boolean { return prefs.getBoolean(KEY_HAS_PROMPTED_THEME_MODE, false) } /** * Gets the default distance unit based on the system locale. * Returns imperial for countries that use imperial system (US, Liberia, Myanmar), Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +3 −1 Original line number Diff line number Diff line Loading @@ -346,7 +346,7 @@ fun AppContent( } composable(Screen.TURN_BY_TURN) { backStackEntry -> TurnByTurnRoute(state, routeRepository, port, backStackEntry) TurnByTurnRoute(state, routeRepository, appPreferenceRepository, port, backStackEntry) } } Loading Loading @@ -1095,6 +1095,7 @@ private fun TransitItineraryDetailRoute( private fun TurnByTurnRoute( state: AppContentState, routeRepository: RouteRepository, appPreferenceRepository: AppPreferenceRepository, port: Int?, backStackEntry: NavBackStackEntry ) { Loading @@ -1120,6 +1121,7 @@ private fun TurnByTurnRoute( port = port, mode = routingMode, route = ferrostarRoute, appPreferenceRepository = appPreferenceRepository, ) } } Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/MapView.kt +9 −7 Original line number Diff line number Diff line Loading @@ -129,7 +129,9 @@ fun MapView( val pinFeatures = mapPins.map { mapViewModel.createFeatureFromPlace(it) } rememberCoroutineScope() val styleVariant = if (isSystemInDarkTheme()) "dark" else "light" val themeMode by appPreferences.themeMode.collectAsState() val useDarkTheme = themeMode.shouldUseDarkTheme(isSystemInDarkTheme()) val styleVariant = if (useDarkTheme) "dark" else "light" // Load saved viewport on initial composition LaunchedEffect(Unit) { Loading Loading @@ -186,7 +188,7 @@ fun MapView( val location by mapViewModel.locationFlow.collectAsState() val sensorHeading by mapViewModel.heading.collectAsState() val savedPlaces by mapViewModel.savedPlacesFlow.collectAsState(FeatureCollection()) FavoritesLayer(savedPlaces, mapPins, isSystemInDarkTheme()) FavoritesLayer(savedPlaces, mapPins, useDarkTheme) OfflineBoundsLayer(selectedOfflineArea) Loading @@ -194,7 +196,7 @@ fun MapView( TransitLayer(currentTransitItinerary) PinsLayer(pinFeatures, isSystemInDarkTheme()) PinsLayer(pinFeatures, useDarkTheme) location?.let { LocationPuck(it, sensorHeading) } } Loading Loading @@ -226,7 +228,7 @@ fun MapView( private fun FavoritesLayer( savedPlaces: FeatureCollection<Point, Map<String, JsonElement>>, activeMarkers: List<Place>, isSystemInDarkTheme: Boolean useDarkTheme: Boolean ) { val textColor = MaterialTheme.colorScheme.onSurface val activeMarkerIds = activeMarkers.mapNotNull { it.id } Loading @@ -235,7 +237,7 @@ private fun FavoritesLayer( source = rememberGeoJsonSource(GeoJsonData.JsonString(Json.encodeToString(savedPlaces))), iconAllowOverlap = const(true), iconImage = image( if (isSystemInDarkTheme) { if (useDarkTheme) { painterResource(drawable.ic_stars_dark) } else { painterResource(drawable.ic_stars_light) Loading Loading @@ -524,14 +526,14 @@ private fun TransitLayer(currentTransitItinerary: Itinerary?) { } @Composable private fun PinsLayer(pinFeatures: List<Feature<Point, Map<String, JsonElement>>>, isSystemInDarkTheme: Boolean) { private fun PinsLayer(pinFeatures: List<Feature<Point, Map<String, JsonElement>>>, useDarkTheme: Boolean) { SymbolLayer( id = "map_pins", source = rememberGeoJsonSource(GeoJsonData.JsonString(Json.encodeToString(FeatureCollection(features = pinFeatures)))), iconAllowOverlap = const(true), iconAnchor = const(SymbolAnchor.Bottom), iconImage = image( if (isSystemInDarkTheme) { if (useDarkTheme) { painterResource(drawable.map_pin_dark) } else { painterResource(drawable.map_pin_light) Loading