Fix city selection vs dashboard; Map/Manage updates; workspace and API logging
Made-with: Cursor
This commit is contained in:
11
.cursor/airmq-android.code-workspace
Normal file
11
.cursor/airmq-android.code-workspace
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "../../airmq-android"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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?)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user