From bdd20422ce523b803e1ae06b98af7215a547d876 Mon Sep 17 00:00:00 2001 From: oliexdev Date: Sat, 30 Aug 2025 10:32:27 +0200 Subject: [PATCH] Add MeasurementType User and dialog to change user --- .../java/com/health/openscale/OpenScaleApp.kt | 3 +- .../com/health/openscale/core/data/Enums.kt | 5 +- .../core/usecase/ImportExportUseCases.kt | 5 +- .../ui/screen/dialog/UserInputDialog.kt | 115 ++++++++++++++++++ .../overview/MeasurementDetailScreen.kt | 59 +++++++-- .../ui/screen/overview/OverviewScreen.kt | 1 + .../settings/MeasurementTypeDetailScreen.kt | 6 +- .../app/src/main/res/drawable/ic_user.xml | 18 +++ .../app/src/main/res/values-de/strings.xml | 4 +- .../app/src/main/res/values/strings.xml | 6 +- 10 files changed, 199 insertions(+), 23 deletions(-) create mode 100644 android_app/app/src/main/java/com/health/openscale/ui/screen/dialog/UserInputDialog.kt create mode 100644 android_app/app/src/main/res/drawable/ic_user.xml diff --git a/android_app/app/src/main/java/com/health/openscale/OpenScaleApp.kt b/android_app/app/src/main/java/com/health/openscale/OpenScaleApp.kt index 68965569..44904125 100644 --- a/android_app/app/src/main/java/com/health/openscale/OpenScaleApp.kt +++ b/android_app/app/src/main/java/com/health/openscale/OpenScaleApp.kt @@ -72,7 +72,8 @@ fun getDefaultMeasurementTypes(): List { MeasurementType(key = MeasurementTypeKey.CALORIES, unit = UnitType.KCAL, color = 0xFF4CAF50.toInt(), icon = MeasurementTypeIcon.IC_CALORIES, isEnabled = true), MeasurementType(key = MeasurementTypeKey.COMMENT, inputType = InputFieldType.TEXT, unit = UnitType.NONE, color = 0xFFE0E0E0.toInt(), icon = MeasurementTypeIcon.IC_COMMENT, isPinned = true, isEnabled = true), MeasurementType(key = MeasurementTypeKey.DATE, inputType = InputFieldType.DATE, unit = UnitType.NONE, color = 0xFF9E9E9E.toInt(), icon = MeasurementTypeIcon.IC_DATE, isEnabled = true), - MeasurementType(key = MeasurementTypeKey.TIME, inputType = InputFieldType.TIME, unit = UnitType.NONE, color = 0xFF757575.toInt(), icon = MeasurementTypeIcon.IC_TIME, isEnabled = true) + MeasurementType(key = MeasurementTypeKey.TIME, inputType = InputFieldType.TIME, unit = UnitType.NONE, color = 0xFF757575.toInt(), icon = MeasurementTypeIcon.IC_TIME, isEnabled = true), + MeasurementType(key = MeasurementTypeKey.USER, inputType = InputFieldType.USER, unit = UnitType.NONE, color = 0xFF90A4AE.toInt(), icon = MeasurementTypeIcon.IC_USER, isEnabled = true) ) } diff --git a/android_app/app/src/main/java/com/health/openscale/core/data/Enums.kt b/android_app/app/src/main/java/com/health/openscale/core/data/Enums.kt index 5f4ea68d..b49fc29a 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/data/Enums.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/data/Enums.kt @@ -221,6 +221,7 @@ enum class MeasurementTypeIcon(val resource: IconResource) { IC_COMMENT(IconResource.PainterResource(R.drawable.ic_comment)), IC_TIME(IconResource.PainterResource(R.drawable.ic_time)), IC_DATE(IconResource.PainterResource(R.drawable.ic_date)), + IC_USER(IconResource.PainterResource(R.drawable.ic_user)), IC_M_HEIGHT(IconResource.VectorResource(Icons.Filled.Height)), IC_M_HEART_RATE(IconResource.VectorResource(Icons.Filled.Favorite)), @@ -301,6 +302,7 @@ enum class MeasurementTypeKey( DATE(24, R.string.measurement_type_date, listOf(UnitType.NONE), listOf(InputFieldType.DATE)), TIME(25, R.string.measurement_type_time, listOf(UnitType.NONE), listOf(InputFieldType.TIME)), COMMENT(26, R.string.measurement_type_comment, listOf(UnitType.NONE), listOf(InputFieldType.TEXT)), + USER(27, R.string.measurement_type_user, listOf(UnitType.NONE), listOf(InputFieldType.USER)), CUSTOM(99, R.string.measurement_type_custom_default_name, UnitType.entries.toList(), listOf(InputFieldType.FLOAT, InputFieldType.INT, InputFieldType.TEXT, InputFieldType.DATE, InputFieldType.TIME)); } @@ -325,7 +327,8 @@ enum class InputFieldType { INT, TEXT, DATE, - TIME + TIME, + USER } enum class Trend { diff --git a/android_app/app/src/main/java/com/health/openscale/core/usecase/ImportExportUseCases.kt b/android_app/app/src/main/java/com/health/openscale/core/usecase/ImportExportUseCases.kt index 7d7f0d8e..db62d41d 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/usecase/ImportExportUseCases.kt +++ b/android_app/app/src/main/java/com/health/openscale/core/usecase/ImportExportUseCases.kt @@ -96,7 +96,9 @@ class ImportExportUseCases @Inject constructor( val allAppTypes: List = repository.getAllMeasurementTypes().first() val exportableValueTypes = allAppTypes.filter { - it.key != MeasurementTypeKey.DATE && it.key != MeasurementTypeKey.TIME + it.key != MeasurementTypeKey.DATE && + it.key != MeasurementTypeKey.TIME && + it.key != MeasurementTypeKey.USER } val valueColumnKeys = exportableValueTypes.map { it.key.name }.distinct() @@ -141,6 +143,7 @@ class ImportExportUseCases @Inject constructor( InputFieldType.TIME -> value.dateValue?.let { timeFormatter.format(Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault())) } + InputFieldType.USER -> null } row[type.key.name] = s } diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/dialog/UserInputDialog.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/dialog/UserInputDialog.kt new file mode 100644 index 00000000..407df9b1 --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/dialog/UserInputDialog.kt @@ -0,0 +1,115 @@ +/* + * openScale + * Copyright (C) 2025 olie.xdev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.health.openscale.ui.screen.dialog + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.health.openscale.R +import com.health.openscale.core.data.MeasurementTypeIcon +import com.health.openscale.core.data.User +import com.health.openscale.ui.components.RoundMeasurementIcon +import java.util.Locale + +@Composable +fun UserInputDialog( + title: String, + users: List, + initialSelectedId: Int?, + measurementIcon: MeasurementTypeIcon, + iconBackgroundColor: Color, + onDismiss: () -> Unit, + onConfirm: (Int?) -> Unit +) { + var selectedId by remember(users, initialSelectedId) { mutableStateOf(initialSelectedId) } + + val usersSorted = remember(users) { + users.sortedBy { it.name.lowercase(Locale.getDefault()) } + } + + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = { onConfirm(selectedId) }) { + Text(stringResource(R.string.dialog_ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel_button)) + } + }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + RoundMeasurementIcon( + icon = measurementIcon, + backgroundTint = iconBackgroundColor + ) + Spacer(Modifier.width(12.dp)) + Text(text = title, style = MaterialTheme.typography.titleMedium) + } + }, + text = { + Box(Modifier.heightIn(max = 360.dp)) { + LazyColumn { + items(usersSorted, key = { it.id }) { user -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { selectedId = user.id } + .padding(vertical = 8.dp) + ) { + RadioButton( + selected = (selectedId == user.id), + onClick = { selectedId = user.id } + ) + Spacer(Modifier.width(12.dp)) + Text( + text = user.name, + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + } + } + ) +} diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/overview/MeasurementDetailScreen.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/overview/MeasurementDetailScreen.kt index 22507ca4..c99920a3 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/overview/MeasurementDetailScreen.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/overview/MeasurementDetailScreen.kt @@ -74,6 +74,7 @@ import com.health.openscale.ui.screen.dialog.DateInputDialog import com.health.openscale.ui.screen.dialog.NumberInputDialog import com.health.openscale.ui.screen.dialog.TextInputDialog import com.health.openscale.ui.screen.dialog.TimeInputDialog +import com.health.openscale.ui.screen.dialog.UserInputDialog import com.health.openscale.ui.screen.dialog.decrementValue import com.health.openscale.ui.screen.dialog.incrementValue import com.health.openscale.ui.shared.TopBarAction @@ -122,6 +123,10 @@ fun MeasurementDetailScreen( val dateFormat = remember { DateFormat.getDateInstance(DateFormat.DEFAULT, Locale.getDefault()) } val timeFormat = remember { DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()) } + val allUsers by sharedViewModel.allUsers.collectAsState() + var pendingUserId by remember { mutableStateOf(null) } + var showUserPicker by remember { mutableStateOf(false) } + // Show a loading indicator if navigation is pending (e.g., after saving). if (isPendingNavigation) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -146,6 +151,7 @@ fun MeasurementDetailScreen( currentMeasurementDbId = data.measurement.id currentUserIdState = data.measurement.userId // Use UserID from the loaded measurement measurementTimestampState = data.measurement.timestamp + pendingUserId = null valuesState.clear() data.values.forEach { mvWithType -> // Populate valuesState for non-date/time, enabled types. @@ -166,6 +172,7 @@ fun MeasurementDetailScreen( currentMeasurementDbId = 0 currentUserIdState = userId // Use the passed userId for a new measurement measurementTimestampState = System.currentTimeMillis() // Always use current timestamp for new + pendingUserId = null valuesState.clear() // Preload values from the user's last measurement, if available and types are loaded. @@ -206,29 +213,25 @@ fun MeasurementDetailScreen( icon = Icons.Default.Save, contentDescription = context.getString(R.string.action_save_measurement), onClick = { - if (currentUserIdState == -1) { // Ensure a user is selected. - Toast.makeText(context, R.string.toast_no_user_selected, Toast.LENGTH_SHORT) - .show() + val effectiveUserIdForSave = pendingUserId ?: currentUserIdState + + if (effectiveUserIdForSave == -1) { + Toast.makeText(context, R.string.toast_no_user_selected, Toast.LENGTH_SHORT).show() return@TopBarAction } - // Prevent saving if it's a new measurement with the exact same timestamp as the user's last one. if (currentMeasurementDbId == 0 && lastMeasurementToPreloadFrom != null && - lastMeasurementToPreloadFrom!!.measurement.userId == currentUserIdState && + lastMeasurementToPreloadFrom!!.measurement.userId == effectiveUserIdForSave && measurementTimestampState == lastMeasurementToPreloadFrom!!.measurement.timestamp ) { - Toast.makeText( - context, - R.string.toast_duplicate_timestamp, - Toast.LENGTH_LONG - ).show() + Toast.makeText(context, R.string.toast_duplicate_timestamp, Toast.LENGTH_LONG).show() return@TopBarAction } val measurementToSave = Measurement( id = currentMeasurementDbId, - userId = currentUserIdState, + userId = effectiveUserIdForSave, timestamp = measurementTimestampState ) @@ -310,6 +313,7 @@ fun MeasurementDetailScreen( if (allConversionsOk) { sharedViewModel.saveMeasurement(measurementToSave, valueList) + pendingUserId = null isPendingNavigation = true // Trigger loading indicator and navigate back. navController.popBackStack() } @@ -346,6 +350,16 @@ fun MeasurementDetailScreen( displayValue = timeFormat.format(Date(measurementTimestampState)) currentValueForIncrementDecrement = null // Not applicable } + InputFieldType.USER -> { + val effectiveUserId = pendingUserId ?: currentUserIdState + val selectedUserName = allUsers + .firstOrNull { it.id == effectiveUserId } + ?.name + ?: stringResource(R.string.placeholder_empty_value) + + displayValue = selectedUserName + currentValueForIncrementDecrement = null + } else -> { // For FLOAT, INT, TEXT displayValue = valuesState[type.id] ?: "" currentValueForIncrementDecrement = valuesState[type.id] @@ -362,6 +376,7 @@ fun MeasurementDetailScreen( when (type.inputType) { InputFieldType.DATE -> showDatePickerForMainTimestamp = true InputFieldType.TIME -> showTimePickerForMainTimestamp = true + InputFieldType.USER -> showUserPicker = true else -> dialogTargetType = type // Show generic dialog } } @@ -464,7 +479,7 @@ fun MeasurementDetailScreen( // --- Dialogs for the main measurement timestamp (measurementTimestampState) --- if (showDatePickerForMainTimestamp) { val triggeringType = allMeasurementTypes.find { it.key == MeasurementTypeKey.DATE } - val dateDialogTitle = stringResource(R.string.dialog_title_change_date, triggeringType?.getDisplayName(context) ?: stringResource(R.string.label_date)) + val dateDialogTitle = stringResource(R.string.dialog_title_change_value, triggeringType?.getDisplayName(context) ?: stringResource(R.string.label_date)) DateInputDialog( title = dateDialogTitle, initialTimestamp = measurementTimestampState, @@ -483,7 +498,7 @@ fun MeasurementDetailScreen( if (showTimePickerForMainTimestamp) { val triggeringType = allMeasurementTypes.find { it.key == MeasurementTypeKey.TIME } - val timeDialogTitle = stringResource(R.string.dialog_title_change_time, triggeringType?.getDisplayName(context) ?: stringResource(R.string.label_time)) + val timeDialogTitle = stringResource(R.string.dialog_title_change_value, triggeringType?.getDisplayName(context) ?: stringResource(R.string.label_time)) TimeInputDialog( title = timeDialogTitle, initialTimestamp = measurementTimestampState, @@ -500,6 +515,24 @@ fun MeasurementDetailScreen( } ) } + + if (showUserPicker) { + val triggeringType = allMeasurementTypes.find { it.key == MeasurementTypeKey.USER } + val userDialogTitle = stringResource(R.string.dialog_title_change_value, triggeringType?.getDisplayName(context) ?: stringResource(R.string.measurement_type_user)) + + UserInputDialog( + title = userDialogTitle, + users = allUsers, + initialSelectedId = pendingUserId ?: currentUserIdState, + measurementIcon = triggeringType?.icon ?: MeasurementTypeIcon.IC_USER, + iconBackgroundColor = triggeringType?.let { Color(it.color) } ?: MaterialTheme.colorScheme.primary, + onDismiss = { showUserPicker = false }, + onConfirm = { id -> + pendingUserId = id + showUserPicker = false + } + ) + } } /** 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 7173c670..5b7adda1 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 @@ -930,6 +930,7 @@ fun MeasurementValueRow( InputFieldType.TIME -> originalValue.dateValue?.let { DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(Date(it)) } + InputFieldType.USER -> null } ?: "-" val context = LocalContext.current diff --git a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/MeasurementTypeDetailScreen.kt b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/MeasurementTypeDetailScreen.kt index c1a8e4bd..2fe67830 100644 --- a/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/MeasurementTypeDetailScreen.kt +++ b/android_app/app/src/main/java/com/health/openscale/ui/screen/settings/MeasurementTypeDetailScreen.kt @@ -523,8 +523,10 @@ fun MeasurementTypeDetailScreen( OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_pinned)) { Switch(checked = isPinned, onCheckedChange = { isPinned = it }) } - OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_on_right_y_axis)) { - Switch(checked = isOnRightYAxis, onCheckedChange = { isOnRightYAxis = it }) + if (selectedInputType == InputFieldType.FLOAT || selectedInputType == InputFieldType.INT) { + OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_on_right_y_axis)) { + Switch(checked = isOnRightYAxis, onCheckedChange = { isOnRightYAxis = it }) + } } } diff --git a/android_app/app/src/main/res/drawable/ic_user.xml b/android_app/app/src/main/res/drawable/ic_user.xml new file mode 100644 index 00000000..7363b00f --- /dev/null +++ b/android_app/app/src/main/res/drawable/ic_user.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file 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 81c10c0d..8c93de3d 100644 --- a/android_app/app/src/main/res/values-de/strings.xml +++ b/android_app/app/src/main/res/values-de/strings.xml @@ -102,8 +102,7 @@ %1$s bearbeiten - %1$s ändern - %1$s ändern + %1$s ändern Farbe auswählen Symbol auswählen Wert eingeben @@ -153,6 +152,7 @@ Kommentar Datum Uhrzeit + Benutzer Benutzerdefinierter Typ diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 184a16d9..bb0e4b00 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -102,9 +102,8 @@ Please enter valid data. - Edit %1$s - Change %1$s - Change %1$s + Edit %1$s + Change %1$s Select color Select icon Input value @@ -155,6 +154,7 @@ Comment Date Time + User Custom Type