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:
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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
|
||||
|
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
18
android_app/app/src/main/res/drawable/ic_user.xml
Normal file
18
android_app/app/src/main/res/drawable/ic_user.xml
Normal 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>
|
@@ -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 -->
|
||||
|
@@ -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 -->
|
||||
|
Reference in New Issue
Block a user