diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt index c77a815..a4c6ddd 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/scale/ScaleScreens.kt @@ -306,10 +306,19 @@ private fun DeviceRow(device: ScannedDeviceInfo, onClick: (() -> Unit)?) { sealed class WeighState { data object Idle : WeighState() data class Working(val status: String) : WeighState() - data class Done(val measurement: ScaleMeasurement) : WeighState() + + /** + * Vikten är mottagen. [complete] = false medan vi fortfarande lyssnar efter + * en senare ram med kroppssammansättning (vågen skickar ofta vikten först + * och muskler/fett/vatten någon sekund senare). + */ + data class Done(val measurement: ScaleMeasurement, val complete: Boolean) : WeighState() data class Failed(val message: String) : WeighState() } +private fun ScaleMeasurement.hasComposition(): Boolean = + fat > 0f || water > 0f || muscle > 0f + class WeighViewModel( private val scaleManager: ScaleManager, private val repo: GymRepository, @@ -319,31 +328,37 @@ class WeighViewModel( val saveError = MutableStateFlow(null) val missingBodyData = MutableStateFlow(false) + private var compositionTimeout: kotlinx.coroutines.Job? = null + init { viewModelScope.launch { scaleManager.lastEvent.collect { event -> - // En färdig mätning ska inte skrivas över av senare events (t.ex. Disconnected) - if (state.value is WeighState.Done) return@collect + val current = state.value when (event) { null -> {} + is BluetoothEvent.MeasurementReceived -> onMeasurement(event.measurement) is BluetoothEvent.Listening -> - state.value = WeighState.Working("Lyssnar efter vågen — kliv på den…") + if (current !is WeighState.Done) + state.value = WeighState.Working("Lyssnar efter vågen — kliv på den…") is BluetoothEvent.Connected -> - state.value = WeighState.Working("Ansluten — ställ dig barfota på vågen") + if (current !is WeighState.Done) + state.value = WeighState.Working("Ansluten — ställ dig barfota på vågen") is BluetoothEvent.DeviceMessage -> - state.value = WeighState.Working(event.message) - is BluetoothEvent.MeasurementReceived -> { - state.value = WeighState.Done(event.measurement) - scaleManager.disconnect() - } + if (current !is WeighState.Done) + state.value = WeighState.Working(event.message) is BluetoothEvent.ConnectionFailed -> - state.value = WeighState.Failed("Kunde inte ansluta: ${event.error}") + if (current !is WeighState.Done) + state.value = WeighState.Failed("Kunde inte ansluta: ${event.error}") is BluetoothEvent.Error -> - state.value = WeighState.Failed(event.error) - is BluetoothEvent.Disconnected -> { - if (state.value is WeighState.Working) { + if (current !is WeighState.Done) + state.value = WeighState.Failed(event.error) + is BluetoothEvent.Disconnected -> when (current) { + is WeighState.Working -> state.value = WeighState.Failed("Vågen kopplade från innan mätningen blev klar") - } + // Tappade vi länken medan vi väntade på sammansättningen + // behåller vi vikten och avslutar mätningen. + is WeighState.Done -> finishMeasurement() + else -> {} } is BluetoothEvent.BroadcastComplete, is BluetoothEvent.UserInteractionRequired -> {} } @@ -351,7 +366,44 @@ class WeighViewModel( } } + /** + * Vågen kan publicera flera ramar: först bara vikt, sen en med + * muskler/fett/vatten. Vi mergar och blir klara först när sammansättningen + * kommit — eller efter en väntetid om den uteblir. + */ + private fun onMeasurement(measurement: ScaleMeasurement) { + val previous = (state.value as? WeighState.Done)?.measurement + val merged = previous?.let { measurement.mergeWith(it) } ?: measurement + + if (merged.hasComposition()) { + compositionTimeout?.cancel() + compositionTimeout = null + state.value = WeighState.Done(merged, complete = true) + scaleManager.disconnect() + } else { + state.value = WeighState.Done(merged, complete = false) + if (compositionTimeout == null) { + compositionTimeout = viewModelScope.launch { + kotlinx.coroutines.delay(20_000) + finishMeasurement() + } + } + } + } + + /** Avsluta med det vi har (vikt utan sammansättning). */ + private fun finishMeasurement() { + compositionTimeout?.cancel() + compositionTimeout = null + (state.value as? WeighState.Done)?.let { d -> + if (!d.complete) state.value = d.copy(complete = true) + } + scaleManager.disconnect() + } + fun start() { + compositionTimeout?.cancel() + compositionTimeout = null state.value = WeighState.Working("Söker efter vågen…") saved.value = false saveError.value = null @@ -523,6 +575,29 @@ fun WeighScreen( ResultRow("Benmassa", m.bone, " kg") ResultRow("Bukfett (index)", m.visceralFat, "") + if (!s.complete) { + Row(verticalAlignment = Alignment.CenterVertically) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + ) + Text( + "Väntar på muskler/fett/vatten — stå kvar på vågen…", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 10.dp), + ) + } + } else if (!m.hasComposition()) { + Text( + "Ingen kroppssammansättning togs emot. Kontrollera att " + + "längd/födelseår/kön är ifyllda i profilen och väg dig " + + "barfota med torra fötter.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + saveError?.let { Text( it,