1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-09-02 12:54:10 +02:00

Add MeasurementType User and dialog to change user

This commit is contained in:
oliexdev
2025-08-30 10:32:27 +02:00
parent 7be6b9575c
commit bdd20422ce
10 changed files with 199 additions and 23 deletions

View File

@@ -72,7 +72,8 @@ fun getDefaultMeasurementTypes(): List<MeasurementType> {
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)
)
}

View File

@@ -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 {

View File

@@ -96,7 +96,9 @@ class ImportExportUseCases @Inject constructor(
val allAppTypes: List<MeasurementType> = 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
}

View File

@@ -0,0 +1,115 @@
/*
* openScale
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
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<User>,
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
)
}
}
}
}
}
)
}

View File

@@ -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<Int?>(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
}
)
}
}
/**

View File

@@ -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

View File

@@ -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 })
}
}
}

View File

@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="350"
android:viewportHeight="350">
<path
android:fillColor="#FF000000"
android:pathData="M175,171.173c38.914,0 70.463,-38.318 70.463,-85.586C245.463,38.318 235.105,0 175,0s-70.465,38.318 -70.465,85.587C104.535,132.855 136.084,171.173 175,171.173z"/>
<path
android:fillColor="#FF000000"
android:pathData="M41.909,301.853C41.897,298.971 41.885,301.041 41.909,301.853L41.909,301.853z"/>
<path
android:fillColor="#FF000000"
android:pathData="M308.085,304.104C308.123,303.315 308.098,298.63 308.085,304.104L308.085,304.104z"/>
<path
android:fillColor="#FF000000"
android:pathData="M307.935,298.397c-1.305,-82.342 -12.059,-105.805 -94.352,-120.657c0,0 -11.584,14.761 -38.584,14.761s-38.586,-14.761 -38.586,-14.761c-81.395,14.69 -92.803,37.805 -94.303,117.982c-0.123,6.547 -0.18,6.891 -0.202,6.131c0.005,1.424 0.011,4.058 0.011,8.651c0,0 19.592,39.496 133.08,39.496c113.486,0 133.08,-39.496 133.08,-39.496c0,-2.951 0.002,-5.003 0.005,-6.399C308.062,304.575 308.018,303.664 307.935,298.397z"/>
</vector>

View File

@@ -102,8 +102,7 @@
<!-- Dialoge (Allgemein & Messwertbearbeitung) -->
<string name="dialog_title_edit_value">%1$s bearbeiten</string> <!-- Beispiel: "Gewicht bearbeiten" -->
<string name="dialog_title_change_date">%1$s ändern</string> <!-- Beispiel: "Datum ändern" oder "Messdatum ändern" -->
<string name="dialog_title_change_time">%1$s ändern</string> <!-- Beispiel: "Zeit ändern" oder "Messzeit ändern" -->
<string name="dialog_title_change_value">%1$s ändern</string> <!-- Beispiel: "Datum ändern" oder "Messdatum ändern" -->
<string name="dialog_title_select_color">Farbe auswählen</string>
<string name="dialog_title_select_icon">Symbol auswählen</string>
<string name="dialog_title_input_value">Wert eingeben</string>
@@ -153,6 +152,7 @@
<string name="measurement_type_comment">Kommentar</string>
<string name="measurement_type_date">Datum</string>
<string name="measurement_type_time">Uhrzeit</string>
<string name="measurement_type_user">Benutzer</string>
<string name="measurement_type_custom_default_name">Benutzerdefinierter Typ</string>
<!-- Einstellungen für Messarten -->

View File

@@ -102,9 +102,8 @@
<string name="toast_enter_valid_data">Please enter valid data.</string>
<!-- Dialogs (General & Measurement Edit) -->
<string name="dialog_title_edit_value">Edit %1$s</string> <!-- Example: "Edit Weight" -->
<string name="dialog_title_change_date">Change %1$s</string> <!-- Example: "Change Date" or "Change Measurement Date" -->
<string name="dialog_title_change_time">Change %1$s</string> <!-- Example: "Change Time" or "Change Measurement Time" -->
<string name="dialog_title_edit_value">Edit %1$s</string>
<string name="dialog_title_change_value">Change %1$s</string>
<string name="dialog_title_select_color">Select color</string>
<string name="dialog_title_select_icon">Select icon</string>
<string name="dialog_title_input_value">Input value</string>
@@ -155,6 +154,7 @@
<string name="measurement_type_comment">Comment</string>
<string name="measurement_type_date">Date</string>
<string name="measurement_type_time">Time</string>
<string name="measurement_type_user">User</string>
<string name="measurement_type_custom_default_name">Custom Type</string>
<!-- Measurement Type Settings -->