diff --git a/android_app/app/src/main/java/com/health/openscale/core/service/BleConnector.kt b/android_app/app/src/main/java/com/health/openscale/core/service/BleConnector.kt index b24278fb..c0489ee6 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/service/BleConnector.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/service/BleConnector.kt @@ -42,8 +42,10 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicInteger /** * Manages Bluetooth connections to scale devices, handling the connection lifecycle, @@ -102,6 +104,38 @@ class BleConnector( private var communicatorJob: Job? = null // Job for observing events from the activeCommunicator. private var disconnectTimeoutJob: Job? = null // Job for handling disconnect timeouts. + private val savedBurstSignal = MutableSharedFlow(replay = 0, extraBufferCapacity = 64) + private val pendingSavedCount = AtomicInteger(0) + @Volatile private var lastSavedArgs: List = emptyList() + + init { + scope.launch { + savedBurstSignal + .debounce(700) + .collect { + val count = pendingSavedCount.getAndSet(0) + if (count <= 0) return@collect + + if (count == 1) { + _snackbarEvents.emit( + SnackbarEvent( + messageResId = R.string.bluetooth_connector_measurement_saved, + messageFormatArgs = lastSavedArgs + ) + ) + } else { + _snackbarEvents.tryEmit( + SnackbarEvent( + messageResId = R.string.saved_measurements_message, + messageFormatArgs = listOf(count) + ) + ) + } + } + } + } + + /** * Attempts to connect to the specified Bluetooth device. * This function is suspendable and performs operations in the [scope] provided during construction. @@ -185,7 +219,6 @@ class BleConnector( // but confirm here for safety. _connectedDeviceAddress.value = connectedDeviceInfo.address _connectedDeviceName.value = connectedDeviceInfo.name - _snackbarEvents.tryEmit(SnackbarEvent(messageResId = R.string.bluetooth_connector_connected_to, messageFormatArgs = listOf(deviceDisplayName))) _connectionError.value = null // Clear any errors on successful connection. LogManager.i(TAG, "Successfully connected to $deviceDisplayName via adapter's isConnected flow.") disconnectTimeoutJob?.cancel() // Successfully connected, timeout no longer needed. @@ -429,7 +462,9 @@ class BleConnector( finalValues.forEach { databaseRepository.insertMeasurementValue(it) } LogManager.i(TAG, "Measurement from $deviceName for User $currentAppUserId saved (ID: $measurementId). Values: ${finalValues.size}") - _snackbarEvents.tryEmit(SnackbarEvent(messageResId = R.string.bluetooth_connector_measurement_saved, messageFormatArgs = listOf(measurementData.weight, deviceName))) + pendingSavedCount.incrementAndGet() + lastSavedArgs = listOf(measurementData.weight, deviceName) + savedBurstSignal.tryEmit(Unit) } catch (e: Exception) { LogManager.e(TAG, "Error saving measurement from $deviceName.", e) _snackbarEvents.tryEmit(SnackbarEvent(messageResId = R.string.bluetooth_connector_measurement_save_error, messageFormatArgs = listOf(deviceName))) diff --git a/android_app/app/src/main/java/com/health/openscale/ui/navigation/AppNavigation.kt b/android_app/app/src/main/java/com/health/openscale/ui/navigation/AppNavigation.kt index 66fbcd03..7ebc479f 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/navigation/AppNavigation.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/navigation/AppNavigation.kt @@ -130,6 +130,8 @@ import com.health.openscale.ui.screen.statistics.StatisticsScreen import com.health.openscale.ui.theme.Black import com.health.openscale.ui.theme.Blue import com.health.openscale.ui.theme.White +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch /** @@ -202,17 +204,25 @@ fun AppNavigation(sharedViewModel: SharedViewModel) { sharedViewModel.snackbarEvents, settingsViewModel.snackbarEvents, bluetoothViewModel.snackbarEvents - ).collect { evt -> + ) + .distinctUntilChanged { a, b -> + a.messageResId == b.messageResId && a.message == b.message + } + .debounce(150) + .collect { evt -> val msg = evt.message ?: context.getString( requireNotNull(evt.messageResId), *evt.messageFormatArgs.toTypedArray() ) val action = evt.actionLabel ?: evt.actionLabelResId?.let { context.getString(it) } - val res = snackbarHostState.showSnackbar( - message = msg, - actionLabel = action, - duration = evt.duration - ) + val res = snackbarHostState.run { + currentSnackbarData?.dismiss() + showSnackbar( + message = msg, + actionLabel = action, + duration = evt.duration + ) + } if (res == SnackbarResult.ActionPerformed) evt.onAction?.invoke() } } diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/overview/OverviewScreen.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/overview/OverviewScreen.kt index 0ed88534..7173c670 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/overview/OverviewScreen.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/overview/OverviewScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons @@ -120,6 +121,10 @@ import com.health.openscale.ui.screen.components.LineChart import com.health.openscale.ui.screen.components.provideFilterTopBarAction import com.health.openscale.ui.shared.TopBarAction import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.text.DateFormat import java.text.SimpleDateFormat @@ -471,6 +476,14 @@ fun OverviewScreen( ) } } else { + val topId = items.firstOrNull()?.measurementWithValues?.measurement?.id + LaunchedEffect(topId) { + if (topId != null && !listState.isScrollInProgress) { + delay(60) + listState.smartScrollTo(0) + } + } + // Chart Box(modifier = Modifier.fillMaxWidth()) { LineChart( @@ -488,9 +501,8 @@ fun OverviewScreen( ?.let { (targetIndex, mwv) -> val targetId = mwv.measurement.id scope.launch { - listState.animateScrollToItem( - index = targetIndex, - scrollOffset = 0 + listState.smartScrollTo( + index = targetIndex ) highlightedMeasurementId = targetId delay(600) @@ -542,6 +554,11 @@ fun OverviewScreen( } } +suspend fun LazyListState.smartScrollTo(index: Int) { + val dist = kotlin.math.abs(firstVisibleItemIndex - index) + if (dist > 20) scrollToItem(index) else animateScrollToItem(index) +} + /** * A Composable card displayed when no user is currently selected/active. * It prompts the user to add or select a user. diff --git a/android_app/app/src/main/java/com/health/openscale/ui/shared/SharedViewModel.kt b/android_app/app/src/main/java/com/health/openscale/ui/shared/SharedViewModel.kt index 239b1bbe..ba566678 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/shared/SharedViewModel.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/shared/SharedViewModel.kt @@ -105,6 +105,8 @@ class SharedViewModel @Inject constructor( actionLabel: String? = null, onAction: (() -> Unit)? = null ) { + if (message == null && messageResId == null) return + viewModelScope.launch { _snackbarEvents.emit( SnackbarEvent( diff --git a/android_app/app/src/main/res/values-de/strings.xml b/android_app/app/src/main/res/values-de/strings.xml index 8680f0ed..76f55e05 100644 --- a/android_app/app/src/main/res/values-de/strings.xml +++ b/android_app/app/src/main/res/values-de/strings.xml @@ -508,4 +508,6 @@ Fehler: Benutzerdaten konnten nicht korrekt geladen werden. Bitte einen Benutzer auswählen. Bitte einen gültigen Code eingeben. + %1$d Messungen gespeichert + diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 4e23ca12..56ab8dc8 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -508,4 +508,7 @@ Please select a user. Please enter a valid code. + Saved %1$d measurements + +