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) {
viewModelScope.launch(Dispatchers.IO) {
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?) {
viewModelScope.launch(Dispatchers.IO) {
if (location != null) {
cityService.refreshCityFromLocation(location)
cityService.setDetectAutomatically(true)
cityService.refreshCityFromLocation(location)
val selectedCity = cityService.getSelectedCity()
_uiState.value = _uiState.value.copy(
detectAutomatically = true,

View File

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

View File

@@ -301,21 +301,28 @@ private fun AirMQMap(
}
}
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { mapView },
update = { map ->
map.post {
// 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 = map,
items = latestItems.value,
map = mapView,
items = snapshotItems,
onMarkerClick = latestOnMarkerClick.value,
clusterEnabled = latestClusterEnabled.value,
centerOnMarker = latestCenterOnMarker.value,
clusterEnabled = snapshotCluster,
centerOnMarker = snapshotCenter,
initialCameraDone = initialCameraDone
)
}
}
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { mapView }
)
}

View File

@@ -24,6 +24,7 @@ interface CityService {
/**
* If [DashboardCityContext.cityId] is missing, tries to resolve it from the local city DB
* using the stored English name and updates preferences.
* Does not notify [observeDashboardCityContext] (avoids re-entrant emissions during dashboard load).
*/
suspend fun refreshDashboardCityIdentity()
@@ -80,9 +81,9 @@ interface CityService {
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.
* 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?)

View File

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

View File

@@ -114,7 +114,12 @@ class DashboardMetricsRepositoryImpl @Inject constructor(
}
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(
epochMillis = t,
temp = row.Temp?.toFloat(),