Fix city selection vs dashboard; Map/Manage updates; workspace and API logging

Made-with: Cursor
This commit is contained in:
2026-04-06 23:49:35 +02:00
parent 34ad7e4af7
commit 0a79ee5e04
7 changed files with 50 additions and 19 deletions

View File

@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "../../airmq-android"
},
{
"path": ".."
}
],
"settings": {}
}

View File

@@ -79,7 +79,12 @@ class CityViewModel @Inject constructor(
private fun selectCity(city: org.db3.airmq.sdk.city.domain.City) { private fun selectCity(city: org.db3.airmq.sdk.city.domain.City) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
cityService.setSelectedCity(city.id) cityService.setSelectedCity(city.id)
_actions.tryEmit(CityScreenContract.Action.NavigateBack) .onSuccess { _actions.tryEmit(CityScreenContract.Action.NavigateBack) }
.onFailure {
_actions.tryEmit(
CityScreenContract.Action.ShowToast(appContext.getString(R.string.toast_error))
)
}
} }
} }
@@ -93,8 +98,8 @@ class CityViewModel @Inject constructor(
private fun enableDetectAutomaticallyWithLocation(location: Location?) { private fun enableDetectAutomaticallyWithLocation(location: Location?) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (location != null) { if (location != null) {
cityService.refreshCityFromLocation(location)
cityService.setDetectAutomatically(true) cityService.setDetectAutomatically(true)
cityService.refreshCityFromLocation(location)
val selectedCity = cityService.getSelectedCity() val selectedCity = cityService.getSelectedCity()
_uiState.value = _uiState.value.copy( _uiState.value = _uiState.value.copy(
detectAutomatically = true, detectAutomatically = true,

View File

@@ -1,5 +1,6 @@
package org.db3.airmq.features.manage package org.db3.airmq.features.manage
import android.util.Log
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -67,6 +68,7 @@ fun ManageScreen(
viewModel: ManageViewModel = hiltViewModel() viewModel: ManageViewModel = hiltViewModel()
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
Log.d("MANAGE_DEBUG", uiState.toString())
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner, viewModel) { DisposableEffect(lifecycleOwner, viewModel) {
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->

View File

@@ -301,21 +301,28 @@ private fun AirMQMap(
} }
} }
// Overlay rebuild must be keyed off composable inputs: AndroidView's `update` runs without
// recording snapshot reads when values are only read inside View.post {}, so map items can
// load without a rebuild until something else (e.g. pan → debounced rebuild) retriggers it.
LaunchedEffect(items, centerOnMarker, clusterEnabled) {
val snapshotItems = items
val snapshotCenter = centerOnMarker
val snapshotCluster = clusterEnabled
mapView.post {
rebuildAirMqMapOverlays(
map = mapView,
items = snapshotItems,
onMarkerClick = latestOnMarkerClick.value,
clusterEnabled = snapshotCluster,
centerOnMarker = snapshotCenter,
initialCameraDone = initialCameraDone
)
}
}
AndroidView( AndroidView(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
factory = { mapView }, factory = { mapView }
update = { map ->
map.post {
rebuildAirMqMapOverlays(
map = map,
items = latestItems.value,
onMarkerClick = latestOnMarkerClick.value,
clusterEnabled = latestClusterEnabled.value,
centerOnMarker = latestCenterOnMarker.value,
initialCameraDone = initialCameraDone
)
}
}
) )
} }

View File

@@ -24,6 +24,7 @@ interface CityService {
/** /**
* If [DashboardCityContext.cityId] is missing, tries to resolve it from the local city DB * If [DashboardCityContext.cityId] is missing, tries to resolve it from the local city DB
* using the stored English name and updates preferences. * using the stored English name and updates preferences.
* Does not notify [observeDashboardCityContext] (avoids re-entrant emissions during dashboard load).
*/ */
suspend fun refreshDashboardCityIdentity() suspend fun refreshDashboardCityIdentity()
@@ -80,9 +81,9 @@ interface CityService {
suspend fun setDetectAutomatically(enabled: Boolean) suspend fun setDetectAutomatically(enabled: Boolean)
/** /**
* Refreshes the selected city from location. Used when detect automatically is on. * Refreshes the selected city from location when [getDetectAutomatically] is true.
* Resolves closest city from the cities DB and updates stored city. * Resolves closest city from the cities DB and updates stored city.
* No-op if location is null or no matching city found. * No-op if auto-detect is off, location is null, or no matching city found.
*/ */
suspend fun refreshCityFromLocation(location: Location?) suspend fun refreshCityFromLocation(location: Location?)

View File

@@ -101,6 +101,7 @@ class CityServiceImpl @Inject constructor(
} }
override suspend fun refreshCityFromLocation(location: Location?) { override suspend fun refreshCityFromLocation(location: Location?) {
if (!getDetectAutomatically()) return
if (location == null) return if (location == null) return
val cities = ensureCitiesInDb() val cities = ensureCitiesInDb()
val resolvedCity = findClosestCity(cities, location.latitude, location.longitude) val resolvedCity = findClosestCity(cities, location.latitude, location.longitude)
@@ -121,7 +122,6 @@ class CityServiceImpl @Inject constructor(
val city = cityLocalDataSource.getAllCities() val city = cityLocalDataSource.getAllCities()
.find { it.nameEn.equals(nameEn, ignoreCase = true) } ?: return .find { it.nameEn.equals(nameEn, ignoreCase = true) } ?: return
prefs.edit().putString(KEY_DASHBOARD_CITY_ID, city.id).apply() prefs.edit().putString(KEY_DASHBOARD_CITY_ID, city.id).apply()
pushContextUpdate()
} }
override suspend fun getResolvedDashboardCityContext(): DashboardCityContext { override suspend fun getResolvedDashboardCityContext(): DashboardCityContext {

View File

@@ -114,7 +114,12 @@ class DashboardMetricsRepositoryImpl @Inject constructor(
} }
private fun mapLastRow(row: CityAverageLastQuery.CityAverageLast): SensorSampleRow? { private fun mapLastRow(row: CityAverageLastQuery.CityAverageLast): SensorSampleRow? {
val t = GraphqlDateTimeParser.parseToEpochMillis(row.time) ?: return null val parsedTime = GraphqlDateTimeParser.parseToEpochMillis(row.time)
val hasAnyReading = row.Temp != null || row.Hum != null || row.Press != null ||
row.PMS1 != null || row.PMS25 != null || row.PMS10 != null ||
row.radRg != null || row.PPM != null || row.IKAV != null ||
row.CO2 != null || row.VOC != null || row.AQI != null
val t = parsedTime ?: if (hasAnyReading) System.currentTimeMillis() else return null
return SensorSampleRow( return SensorSampleRow(
epochMillis = t, epochMillis = t,
temp = row.Temp?.toFloat(), temp = row.Temp?.toFloat(),