From a3d18a1258981da1009e36e9fcc6a9739542f08a Mon Sep 17 00:00:00 2001 From: oliexdev Date: Fri, 15 Aug 2025 15:56:56 +0200 Subject: [PATCH] Introduces a new "Chart Settings" screen accessible from the main settings menu. --- .../core/database/UserSettingsRepository.kt | 19 ++++++ .../openscale/ui/navigation/AppNavigation.kt | 8 +++ .../health/openscale/ui/navigation/Routes.kt | 1 + .../ui/screen/components/LineChart.kt | 23 ++++++-- .../ui/screen/settings/ChartSettingsScreen.kt | 58 +++++++++++++++++++ .../ui/screen/settings/SettingsScreen.kt | 9 +++ .../app/src/main/res/values-de/strings.xml | 4 ++ .../app/src/main/res/values/strings.xml | 4 ++ 8 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 android_app/app/src/main/java/com/health/openscale/ui/screen/settings/ChartSettingsScreen.kt diff --git a/android_app/app/src/main/java/com/health/openscale/core/database/UserSettingsRepository.kt b/android_app/app/src/main/java/com/health/openscale/core/database/UserSettingsRepository.kt index 3b728eb7..dccc3aad 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/database/UserSettingsRepository.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/database/UserSettingsRepository.kt @@ -57,6 +57,9 @@ object UserPreferenceKeys { val SAVED_BLUETOOTH_SCALE_ADDRESS = stringPreferencesKey("saved_bluetooth_scale_address") val SAVED_BLUETOOTH_SCALE_NAME = stringPreferencesKey("saved_bluetooth_scale_name") + // Settings for chart + val SHOW_CHART_DATA_POINTS = booleanPreferencesKey("show_chart_data_points") + // Context strings for screen-specific settings (can be used as prefixes for dynamic keys) const val OVERVIEW_SCREEN_CONTEXT = "overview_screen" const val GRAPH_SCREEN_CONTEXT = "graph_screen" @@ -90,6 +93,9 @@ interface UserSettingsRepository { suspend fun saveBluetoothScale(address: String, name: String?) suspend fun clearSavedBluetoothScale() + val showChartDataPoints: Flow + suspend fun setShowChartDataPoints(show: Boolean) + // Generic Settings Accessors /** * Observes a setting with the given key name and default value. @@ -248,6 +254,19 @@ class UserSettingsRepositoryImpl(context: Context) : UserSettingsRepository { } } + override val showChartDataPoints: Flow = observeSetting( + UserPreferenceKeys.SHOW_CHART_DATA_POINTS.name, + true + ).catch { exception -> + LogManager.e(TAG, "Error observing showChartDataPoints", exception) + emit(true) + } + + override suspend fun setShowChartDataPoints(show: Boolean) { + LogManager.d(TAG, "Setting showChartDataPoints to: $show") + saveSetting(UserPreferenceKeys.SHOW_CHART_DATA_POINTS.name, show) + } + @Suppress("UNCHECKED_CAST") override fun observeSetting(keyName: String, defaultValue: T): Flow { LogManager.v(TAG, "Observing setting: key='$keyName', type='${defaultValue!!::class.simpleName}'") 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 e65f7057..c4160d1a 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 @@ -121,6 +121,7 @@ import com.health.openscale.ui.screen.overview.MeasurementDetailScreen import com.health.openscale.ui.screen.overview.OverviewScreen import com.health.openscale.ui.screen.settings.AboutScreen import com.health.openscale.ui.screen.settings.BluetoothScreen +import com.health.openscale.ui.screen.settings.ChartSettingsScreen import com.health.openscale.ui.screen.settings.DataManagementSettingsScreen import com.health.openscale.ui.screen.settings.GeneralSettingsScreen import com.health.openscale.ui.screen.settings.MeasurementTypeDetailScreen @@ -683,6 +684,13 @@ fun AppNavigation(sharedViewModel: SharedViewModel) { bluetoothViewModel = bluetoothViewModel ) } + composable(Routes.CHART_SETTINGS) { + ChartSettingsScreen( + navController = navController, + sharedViewModel = sharedViewModel, + settingsViewModel = settingsViewModel + ) + } composable(Routes.DATA_MANAGEMENT_SETTINGS) { DataManagementSettingsScreen( navController = navController, diff --git a/android_app/app/src/main/java/com/health/openscale/ui/navigation/Routes.kt b/android_app/app/src/main/java/com/health/openscale/ui/navigation/Routes.kt index 20b26880..eb77dd90 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/navigation/Routes.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/navigation/Routes.kt @@ -46,6 +46,7 @@ object Routes { const val MEASUREMENT_TYPES = "settings/types" const val MEASUREMENT_TYPE_DETAIL = "settings/typeDetail" const val BLUETOOTH_SETTINGS = "settings/bluetooth" + const val CHART_SETTINGS = "settings/chart" const val DATA_MANAGEMENT_SETTINGS = "settings/dataManagement" const val ABOUT_SETTINGS = "settings/about" diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/components/LineChart.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/components/LineChart.kt index a7eb0853..0d7dee79 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/components/LineChart.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/components/LineChart.kt @@ -63,6 +63,7 @@ import com.health.openscale.core.data.MeasurementTypeKey import com.health.openscale.core.data.TimeRangeFilter import com.health.openscale.core.database.UserPreferenceKeys import com.health.openscale.core.database.UserSettingsRepository +import com.health.openscale.core.database.UserSettingsRepositoryImpl import com.health.openscale.ui.screen.SharedViewModel import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent @@ -142,6 +143,8 @@ fun LineChart( val scope = rememberCoroutineScope() val userSettingsRepository = sharedViewModel.userSettingRepository + val showDataPointsSetting by userSettingsRepository.showChartDataPoints.collectAsState(initial = true) + val uiSelectedTimeRange by rememberContextualTimeRangeFilter( screenContextName = screenContextName, userSettingsRepository = userSettingsRepository @@ -503,7 +506,11 @@ fun LineChart( LineCartesianLayer.LineProvider.series( seriesEntriesForStartAxis.mapIndexedNotNull { index, _ -> if (index < typeColorsForStartAxis.size) { - createLineSpec(typeColorsForStartAxis[index], statisticsMode = targetMeasurementTypeId != null) + createLineSpec( + color = typeColorsForStartAxis[index], + statisticsMode = targetMeasurementTypeId != null, + showPoints = showDataPointsSetting + ) } else null } ) @@ -522,7 +529,11 @@ fun LineChart( LineCartesianLayer.LineProvider.series( seriesEntriesForEndAxis.mapIndexedNotNull { index, _ -> if (index < typeColorsForEndAxis.size) { - createLineSpec(typeColorsForEndAxis[index], statisticsMode = targetMeasurementTypeId != null) + createLineSpec( + color = typeColorsForEndAxis[index], + statisticsMode = targetMeasurementTypeId != null, + showPoints = showDataPointsSetting + ) } else null } ) @@ -702,9 +713,10 @@ private fun rememberXAxisValueFormatter( * @param color The color of the line and points. * @param statisticsMode If true, an area fill is added below the line, and points are hidden. * This is typically used when `targetMeasurementTypeId` is set. + * @param showPoints If true, points are displayed on the line (unless in statisticsMode). * @return A configured [LineCartesianLayer.Line]. */ -private fun createLineSpec(color: Color, statisticsMode : Boolean): LineCartesianLayer.Line { +private fun createLineSpec(color: Color, statisticsMode : Boolean, showPoints: Boolean): LineCartesianLayer.Line { val lineStroke = LineCartesianLayer.LineStroke.Continuous( thicknessDp = 2f, ) @@ -719,10 +731,11 @@ private fun createLineSpec(color: Color, statisticsMode : Boolean): LineCartesia // Area fill is shown in statistics mode (e.g., when a single type is focused) areaFill = if (statisticsMode) LineCartesianLayer.AreaFill.single(Fill(color.copy(alpha = 0.2f).toArgb())) else null, // Points on the line are shown unless in statistics mode - pointProvider = if (!statisticsMode) { + pointProvider = if (showPoints && !statisticsMode) { LineCartesianLayer.PointProvider.single( LineCartesianLayer.point(ShapeComponent(fill(color.copy(alpha = 0.7f)), CorneredShape.Pill), 6.dp) - ) } else null, + ) + } else null, // dataLabel = null, // No data labels on points pointConnector = LineCartesianLayer.PointConnector.cubic() ) diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/ChartSettingsScreen.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/ChartSettingsScreen.kt new file mode 100644 index 00000000..0e0b9c97 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/ChartSettingsScreen.kt @@ -0,0 +1,58 @@ +package com.health.openscale.ui.screen.settings + +import androidx.activity.result.launch +import androidx.navigation.NavController + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.health.openscale.R +import com.health.openscale.ui.screen.SharedViewModel +import kotlinx.coroutines.launch + +@Composable +fun ChartSettingsScreen( + navController: NavController, + sharedViewModel: SharedViewModel, + settingsViewModel: SettingsViewModel +) { + val chartSettingsScreenTitle = stringResource(R.string.settings_item_chart) + LaunchedEffect(Unit) { + sharedViewModel.setTopBarTitle(chartSettingsScreenTitle) + } + + val coroutineScope = rememberCoroutineScope() + + val showDataPoints by sharedViewModel.userSettingRepository.showChartDataPoints.collectAsState(true) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.setting_show_chart_points), + style = MaterialTheme.typography.bodyLarge + ) + Switch( + checked = showDataPoints, + onCheckedChange = { newValue -> + coroutineScope.launch { + sharedViewModel.userSettingRepository.setShowChartDataPoints(newValue) + } + } + ) + } + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsScreen.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsScreen.kt index 39a737d5..e380926f 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsScreen.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/SettingsScreen.kt @@ -23,10 +23,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ShowChart import androidx.compose.material.icons.filled.Bluetooth import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.ShowChart import androidx.compose.material.icons.filled.Storage import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.Card @@ -79,6 +81,7 @@ fun SettingsScreen( val userSettingsLabel = stringResource(R.string.settings_item_user) val measurementTypesLabel = stringResource(R.string.settings_item_measurement_types) val bluetoothLabel = stringResource(R.string.settings_item_bluetooth) + val chartSettingsLabel = stringResource(R.string.settings_item_chart) val dataManagementLabel = stringResource(R.string.settings_item_data_management) val aboutLabel = stringResource(R.string.settings_item_about) @@ -107,6 +110,12 @@ fun SettingsScreen( route = Routes.BLUETOOTH_SETTINGS, contentDescription = bluetoothLabel ), + SettingsItem( + label = chartSettingsLabel, + icon = Icons.AutoMirrored.Filled.ShowChart, + route = Routes.CHART_SETTINGS, + contentDescription = chartSettingsLabel + ), SettingsItem( label = dataManagementLabel, icon = Icons.Filled.Storage, 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 4dc6b13c..fe8cd8a9 100644 --- a/android_app/app/src/main/res/values-de/strings.xml +++ b/android_app/app/src/main/res/values-de/strings.xml @@ -281,6 +281,7 @@ Benutzer Messarten Bluetooth + Diagramm Datenverwaltung Über @@ -288,6 +289,9 @@ Allgemeine Einstellungen Sprache + + Datenpunkte anzeigen + Version: %1$s (%2$s) Projektinformationen diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 968ff822..71555ccf 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -283,6 +283,7 @@ User Measurement Types Bluetooth + Chart Data Management About @@ -290,6 +291,9 @@ General Settings Language + + Show data points + Version: %1$s (%2$s) Project Information