mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-16 05:34:05 +02:00
Refactor: Remove unit settings from user, use measurement type settings
This commit is contained in:
@@ -2,11 +2,11 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 7,
|
||||
"identityHash": "877ad250be34067d136497a388177415",
|
||||
"identityHash": "3eb1fa6c355e18713262b7da7d1ffbdd",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "User",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `birthDate` INTEGER NOT NULL, `gender` TEXT NOT NULL, `heightCm` REAL NOT NULL, `activityLevel` TEXT NOT NULL, `scaleUnit` TEXT NOT NULL, `measureUnit` TEXT NOT NULL)",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `birthDate` INTEGER NOT NULL, `gender` TEXT NOT NULL, `heightCm` REAL NOT NULL, `activityLevel` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
@@ -43,18 +43,6 @@
|
||||
"columnName": "activityLevel",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scaleUnit",
|
||||
"columnName": "scaleUnit",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "measureUnit",
|
||||
"columnName": "measureUnit",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -298,7 +286,7 @@
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '877ad250be34067d136497a388177415')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3eb1fa6c355e18713262b7da7d1ffbdd')"
|
||||
]
|
||||
}
|
||||
}
|
@@ -144,35 +144,36 @@ enum class MeasureUnit {
|
||||
|
||||
enum class MeasurementTypeKey(
|
||||
val id: Int,
|
||||
@StringRes val localizedNameResId: Int // Added: Nullable resource ID for the name
|
||||
@StringRes val localizedNameResId: Int,
|
||||
val allowedUnitTypes: List<UnitType>
|
||||
) {
|
||||
WEIGHT(1, R.string.measurement_type_weight),
|
||||
BMI(2, R.string.measurement_type_bmi),
|
||||
BODY_FAT(3, R.string.measurement_type_body_fat),
|
||||
WATER(4, R.string.measurement_type_water),
|
||||
MUSCLE(5, R.string.measurement_type_muscle),
|
||||
LBM(6, R.string.measurement_type_lbm),
|
||||
BONE(7, R.string.measurement_type_bone),
|
||||
WAIST(8, R.string.measurement_type_waist),
|
||||
WHR(9, R.string.measurement_type_whr),
|
||||
WHTR(10, R.string.measurement_type_whtr),
|
||||
HIPS(11, R.string.measurement_type_hips),
|
||||
VISCERAL_FAT(12, R.string.measurement_type_visceral_fat),
|
||||
CHEST(13, R.string.measurement_type_chest),
|
||||
THIGH(14, R.string.measurement_type_thigh),
|
||||
BICEPS(15, R.string.measurement_type_biceps),
|
||||
NECK(16, R.string.measurement_type_neck),
|
||||
CALIPER_1(17, R.string.measurement_type_caliper1),
|
||||
CALIPER_2(18, R.string.measurement_type_caliper2),
|
||||
CALIPER_3(19, R.string.measurement_type_caliper3),
|
||||
CALIPER(20, R.string.measurement_type_fat_caliper),
|
||||
BMR(21, R.string.measurement_type_bmr),
|
||||
TDEE(22, R.string.measurement_type_tdee),
|
||||
CALORIES(23, R.string.measurement_type_calories),
|
||||
DATE(24, R.string.measurement_type_date),
|
||||
TIME(25, R.string.measurement_type_time),
|
||||
COMMENT(26, R.string.measurement_type_comment),
|
||||
CUSTOM(99, R.string.measurement_type_custom_default_name);
|
||||
WEIGHT(1, R.string.measurement_type_weight, listOf(UnitType.KG, UnitType.LB, UnitType.ST)),
|
||||
BMI(2, R.string.measurement_type_bmi, listOf(UnitType.NONE)),
|
||||
BODY_FAT(3, R.string.measurement_type_body_fat, listOf(UnitType.PERCENT)),
|
||||
WATER(4, R.string.measurement_type_water, listOf(UnitType.PERCENT)),
|
||||
MUSCLE(5, R.string.measurement_type_muscle, listOf(UnitType.PERCENT, UnitType.KG, UnitType.LB)),
|
||||
LBM(6, R.string.measurement_type_lbm, listOf(UnitType.KG, UnitType.LB, UnitType.ST)),
|
||||
BONE(7, R.string.measurement_type_bone, listOf(UnitType.KG, UnitType.LB)),
|
||||
WAIST(8, R.string.measurement_type_waist, listOf(UnitType.CM, UnitType.INCH)),
|
||||
WHR(9, R.string.measurement_type_whr, listOf(UnitType.NONE)),
|
||||
WHTR(10, R.string.measurement_type_whtr, listOf(UnitType.NONE)),
|
||||
HIPS(11, R.string.measurement_type_hips, listOf(UnitType.CM, UnitType.INCH)),
|
||||
VISCERAL_FAT(12, R.string.measurement_type_visceral_fat, listOf(UnitType.PERCENT, UnitType.NONE)),
|
||||
CHEST(13, R.string.measurement_type_chest, listOf(UnitType.CM, UnitType.INCH)),
|
||||
THIGH(14, R.string.measurement_type_thigh, listOf(UnitType.CM, UnitType.INCH)),
|
||||
BICEPS(15, R.string.measurement_type_biceps, listOf(UnitType.CM, UnitType.INCH)),
|
||||
NECK(16, R.string.measurement_type_neck, listOf(UnitType.CM, UnitType.INCH)),
|
||||
CALIPER_1(17, R.string.measurement_type_caliper1, listOf(UnitType.CM, UnitType.INCH)),
|
||||
CALIPER_2(18, R.string.measurement_type_caliper2, listOf(UnitType.CM, UnitType.INCH)),
|
||||
CALIPER_3(19, R.string.measurement_type_caliper3, listOf(UnitType.CM, UnitType.INCH)),
|
||||
CALIPER(20, R.string.measurement_type_fat_caliper, listOf(UnitType.PERCENT, UnitType.NONE)),
|
||||
BMR(21, R.string.measurement_type_bmr, listOf(UnitType.KCAL)),
|
||||
TDEE(22, R.string.measurement_type_tdee, listOf(UnitType.KCAL)),
|
||||
CALORIES(23, R.string.measurement_type_calories, listOf(UnitType.KCAL)),
|
||||
DATE(24, R.string.measurement_type_date, listOf(UnitType.NONE)),
|
||||
TIME(25, R.string.measurement_type_time, listOf(UnitType.NONE)),
|
||||
COMMENT(26, R.string.measurement_type_comment, listOf(UnitType.NONE)),
|
||||
CUSTOM(99, R.string.measurement_type_custom_default_name, UnitType.entries.toList());
|
||||
}
|
||||
|
||||
|
||||
|
@@ -27,7 +27,5 @@ data class User(
|
||||
val birthDate: Long,
|
||||
val gender: GenderType,
|
||||
val heightCm: Float,
|
||||
val activityLevel: ActivityLevel,
|
||||
var scaleUnit: WeightUnit,
|
||||
var measureUnit: MeasureUnit
|
||||
val activityLevel: ActivityLevel
|
||||
)
|
@@ -54,6 +54,7 @@ import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -80,16 +81,6 @@ import com.health.openscale.ui.screen.dialog.IconPickerDialog
|
||||
import com.health.openscale.ui.screen.dialog.getIconResIdByName
|
||||
import kotlin.text.lowercase
|
||||
|
||||
/**
|
||||
* Composable screen for creating or editing a [MeasurementType].
|
||||
* It allows users to define the name, unit, input type, color, icon,
|
||||
* and enabled/pinned status for a measurement type.
|
||||
*
|
||||
* @param navController NavController for navigating back after saving or cancelling.
|
||||
* @param typeId The ID of the [MeasurementType] to edit. If -1, a new type is being created.
|
||||
* @param sharedViewModel The [SharedViewModel] for accessing shared app state like existing measurement types and setting top bar properties.
|
||||
* @param settingsViewModel The [SettingsViewModel] for performing add or update operations on measurement types.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MeasurementTypeDetailScreen(
|
||||
@@ -101,19 +92,44 @@ fun MeasurementTypeDetailScreen(
|
||||
val context = LocalContext.current
|
||||
|
||||
val measurementTypes by sharedViewModel.measurementTypes.collectAsState()
|
||||
val existingType = remember(measurementTypes, typeId) {
|
||||
// Stores the original state of the measurement type before any UI changes.
|
||||
// Crucial for the conversion logic to have the true original state.
|
||||
val originalExistingType = remember(measurementTypes, typeId) {
|
||||
measurementTypes.find { it.id == typeId }
|
||||
}
|
||||
val isEdit = typeId != -1
|
||||
|
||||
var name by remember { mutableStateOf(existingType?.getDisplayName(context).orEmpty()) }
|
||||
var selectedUnit by remember { mutableStateOf(existingType?.unit ?: UnitType.NONE) }
|
||||
var selectedInputType by remember { mutableStateOf(existingType?.inputType ?: InputFieldType.FLOAT) }
|
||||
var selectedColor by remember { mutableStateOf(existingType?.color ?: 0xFF6200EE.toInt()) } // Default color
|
||||
var selectedIcon by remember { mutableStateOf(existingType?.icon ?: "ic_weight") } // Default icon
|
||||
var isEnabled by remember { mutableStateOf(existingType?.isEnabled ?: true) } // Default to true for new types
|
||||
var isPinned by remember { mutableStateOf(existingType?.isPinned ?: false) } // Default to false for new types
|
||||
var isOnRightYAxis by remember { mutableStateOf(existingType?.isOnRightYAxis ?: false) }
|
||||
// Determine the MeasurementTypeKey for the allowed units logic.
|
||||
// For new types, it's always CUSTOM; for existing types, it's the type's key.
|
||||
val currentMeasurementTypeKey = remember(originalExistingType, isEdit) {
|
||||
if (isEdit) originalExistingType?.key ?: MeasurementTypeKey.CUSTOM
|
||||
else MeasurementTypeKey.CUSTOM
|
||||
}
|
||||
|
||||
// Get the list of allowed units based on the key.
|
||||
val allowedUnitsForKey = remember(currentMeasurementTypeKey) {
|
||||
currentMeasurementTypeKey.allowedUnitTypes
|
||||
}
|
||||
|
||||
var name by remember { mutableStateOf(originalExistingType?.getDisplayName(context).orEmpty()) }
|
||||
|
||||
// Safely set selectedUnit. If the existing unit isn't allowed or if no existing unit,
|
||||
// use the first allowed unit.
|
||||
var selectedUnit by remember {
|
||||
val initialUnit = originalExistingType?.unit
|
||||
if (initialUnit != null && initialUnit in allowedUnitsForKey) {
|
||||
mutableStateOf(initialUnit)
|
||||
} else {
|
||||
mutableStateOf(allowedUnitsForKey.firstOrNull() ?: UnitType.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
var selectedInputType by remember { mutableStateOf(originalExistingType?.inputType ?: InputFieldType.FLOAT) }
|
||||
var selectedColor by remember { mutableStateOf(originalExistingType?.color ?: 0xFF6200EE.toInt()) }
|
||||
var selectedIcon by remember { mutableStateOf(originalExistingType?.icon ?: "ic_weight") }
|
||||
var isEnabled by remember { mutableStateOf(originalExistingType?.isEnabled ?: true) }
|
||||
var isPinned by remember { mutableStateOf(originalExistingType?.isPinned ?: false) }
|
||||
var isOnRightYAxis by remember { mutableStateOf(originalExistingType?.isOnRightYAxis ?: false) }
|
||||
|
||||
var expandedUnit by remember { mutableStateOf(false) }
|
||||
var expandedInputType by remember { mutableStateOf(false) }
|
||||
@@ -126,42 +142,64 @@ fun MeasurementTypeDetailScreen(
|
||||
val titleEdit = stringResource(R.string.measurement_type_detail_title_edit)
|
||||
val titleAdd = stringResource(R.string.measurement_type_detail_title_add)
|
||||
|
||||
// Determines if the unit dropdown should be enabled (i.e., if there's more than one allowed unit).
|
||||
val unitDropdownEnabled by remember(allowedUnitsForKey) {
|
||||
derivedStateOf { allowedUnitsForKey.size > 1 }
|
||||
}
|
||||
|
||||
// Effect to re-evaluate and set selectedUnit if originalExistingType or allowedUnitsForKey change.
|
||||
// This ensures selectedUnit is always valid.
|
||||
LaunchedEffect(originalExistingType, allowedUnitsForKey) {
|
||||
val currentUnitInExistingType = originalExistingType?.unit
|
||||
if (currentUnitInExistingType != null && currentUnitInExistingType in allowedUnitsForKey) {
|
||||
if (selectedUnit != currentUnitInExistingType) { // Only update if different to avoid recomposition loops
|
||||
selectedUnit = currentUnitInExistingType
|
||||
}
|
||||
} else if (allowedUnitsForKey.isNotEmpty() && selectedUnit !in allowedUnitsForKey) {
|
||||
selectedUnit = allowedUnitsForKey.first()
|
||||
} else if (allowedUnitsForKey.isEmpty() && selectedUnit != UnitType.NONE) {
|
||||
// This case should ideally not be reached if keys are well-defined.
|
||||
selectedUnit = UnitType.NONE
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
sharedViewModel.setTopBarTitle(
|
||||
if (isEdit) titleEdit
|
||||
else titleAdd
|
||||
)
|
||||
sharedViewModel.setTopBarTitle(if (isEdit) titleEdit else titleAdd)
|
||||
sharedViewModel.setTopBarAction(
|
||||
SharedViewModel.TopBarAction(icon = Icons.Default.Save, onClick = {
|
||||
if (name.isNotBlank()) {
|
||||
val updatedType = MeasurementType(
|
||||
id = existingType?.id ?: 0, // Use 0 for new types, Room will autogenerate
|
||||
// When creating the updatedType, use the key of the originalExistingType if it's an edit.
|
||||
// For new types, it's MeasurementTypeKey.CUSTOM.
|
||||
val finalKey = if (isEdit) originalExistingType?.key ?: MeasurementTypeKey.CUSTOM else MeasurementTypeKey.CUSTOM
|
||||
|
||||
val currentUpdatedType = MeasurementType(
|
||||
id = originalExistingType?.id ?: 0,
|
||||
name = name,
|
||||
icon = selectedIcon,
|
||||
color = selectedColor,
|
||||
unit = selectedUnit,
|
||||
inputType = selectedInputType,
|
||||
displayOrder = existingType?.displayOrder ?: measurementTypes.size,
|
||||
displayOrder = originalExistingType?.displayOrder ?: measurementTypes.size,
|
||||
isEnabled = isEnabled,
|
||||
isPinned = isPinned,
|
||||
key = existingType?.key ?: MeasurementTypeKey.CUSTOM, // New types are custom
|
||||
isDerived = existingType?.isDerived ?: false, // New types are not derived by default
|
||||
key = finalKey, // Use the correct key
|
||||
isDerived = originalExistingType?.isDerived ?: false,
|
||||
isOnRightYAxis = isOnRightYAxis
|
||||
)
|
||||
|
||||
if (isEdit) {
|
||||
val unitChanged = existingType!!.unit != updatedType.unit
|
||||
val inputTypesAreFloat = existingType!!.inputType == InputFieldType.FLOAT && updatedType.inputType == InputFieldType.FLOAT
|
||||
if (isEdit && originalExistingType != null) {
|
||||
val unitChanged = originalExistingType.unit != currentUpdatedType.unit
|
||||
val inputTypesAreFloat = originalExistingType.inputType == InputFieldType.FLOAT && currentUpdatedType.inputType == InputFieldType.FLOAT
|
||||
|
||||
if (unitChanged && inputTypesAreFloat) {
|
||||
pendingUpdatedType = updatedType
|
||||
pendingUpdatedType = currentUpdatedType
|
||||
showConfirmDialog = true
|
||||
} else {
|
||||
settingsViewModel.updateMeasurementType(updatedType)
|
||||
settingsViewModel.updateMeasurementType(currentUpdatedType)
|
||||
navController.popBackStack()
|
||||
}
|
||||
} else {
|
||||
settingsViewModel.addMeasurementType(updatedType)
|
||||
settingsViewModel.addMeasurementType(currentUpdatedType)
|
||||
navController.popBackStack()
|
||||
}
|
||||
} else {
|
||||
@@ -171,7 +209,7 @@ fun MeasurementTypeDetailScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (showConfirmDialog && existingType != null && pendingUpdatedType != null) {
|
||||
if (showConfirmDialog && originalExistingType != null && pendingUpdatedType != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showConfirmDialog = false },
|
||||
title = { Text(stringResource(R.string.measurement_type_dialog_confirm_unit_change_title)) },
|
||||
@@ -179,28 +217,24 @@ fun MeasurementTypeDetailScreen(
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.measurement_type_dialog_confirm_unit_change_message,
|
||||
existingType!!.getDisplayName(context),
|
||||
existingType!!.unit.name.lowercase().replaceFirstChar { it.uppercase() },
|
||||
pendingUpdatedType!!.unit.name.lowercase().replaceFirstChar { it.uppercase() }
|
||||
originalExistingType.getDisplayName(context),
|
||||
originalExistingType.unit.displayName.lowercase().replaceFirstChar { it.uppercase() },
|
||||
pendingUpdatedType!!.unit.displayName.lowercase().replaceFirstChar { it.uppercase() }
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
settingsViewModel.updateMeasurementTypeAndConvertDataViewModelCentric(
|
||||
originalType = existingType!!,
|
||||
originalType = originalExistingType,
|
||||
updatedType = pendingUpdatedType!!
|
||||
)
|
||||
showConfirmDialog = false
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.confirm_button))
|
||||
}
|
||||
}) { Text(stringResource(R.string.confirm_button)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showConfirmDialog = false }) {
|
||||
Text(stringResource(R.string.cancel_button))
|
||||
}
|
||||
TextButton(onClick = { showConfirmDialog = false }) { Text(stringResource(R.string.cancel_button)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -222,12 +256,14 @@ fun MeasurementTypeDetailScreen(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text(stringResource(R.string.measurement_type_label_name)) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
// Name field is editable for new types or existing CUSTOM types.
|
||||
// For predefined types, the name is typically not user-editable.
|
||||
enabled = !isEdit || (originalExistingType?.key == MeasurementTypeKey.CUSTOM)
|
||||
)
|
||||
|
||||
// Color Selector
|
||||
OutlinedTextField(
|
||||
value = String.format("#%06X", 0xFFFFFF and selectedColor), // Display color hex string
|
||||
value = String.format("#%06X", 0xFFFFFF and selectedColor),
|
||||
onValueChange = {}, // Read-only
|
||||
label = { Text(stringResource(R.string.measurement_type_label_color)) },
|
||||
modifier = Modifier
|
||||
@@ -241,21 +277,20 @@ fun MeasurementTypeDetailScreen(
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(selectedColor))
|
||||
.border(1.dp, Color.Gray, CircleShape) // Visually indicate the color
|
||||
.border(1.dp, Color.Gray, CircleShape)
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults.colors( // Custom colors to make it look enabled despite being readOnly
|
||||
colors = TextFieldDefaults.colors(
|
||||
disabledTextColor = LocalContentColor.current,
|
||||
disabledIndicatorColor = MaterialTheme.colorScheme.outline, // Standard outline
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, // Standard label color
|
||||
disabledIndicatorColor = MaterialTheme.colorScheme.outline,
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledContainerColor = Color.Transparent // No background fill
|
||||
disabledContainerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
|
||||
// Icon Selector
|
||||
OutlinedTextField(
|
||||
value = selectedIcon, // Display selected icon name
|
||||
value = selectedIcon,
|
||||
onValueChange = {}, // Read-only
|
||||
label = { Text(stringResource(R.string.measurement_type_label_icon)) },
|
||||
modifier = Modifier
|
||||
@@ -268,14 +303,13 @@ fun MeasurementTypeDetailScreen(
|
||||
painter = runCatching {
|
||||
painterResource(id = getIconResIdByName(selectedIcon))
|
||||
}.getOrElse {
|
||||
// Fallback icon if resource name is invalid or not found
|
||||
Icons.Filled.QuestionMark
|
||||
} as Painter, // Cast is safe due to getOrElse structure
|
||||
Icons.Filled.QuestionMark // Fallback icon
|
||||
} as Painter,
|
||||
contentDescription = stringResource(R.string.content_desc_selected_icon_preview),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
},
|
||||
colors = TextFieldDefaults.colors( // Custom colors for consistent look
|
||||
colors = TextFieldDefaults.colors(
|
||||
disabledTextColor = LocalContentColor.current,
|
||||
disabledIndicatorColor = MaterialTheme.colorScheme.outline,
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -284,38 +318,59 @@ fun MeasurementTypeDetailScreen(
|
||||
)
|
||||
)
|
||||
|
||||
// UnitType Dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expandedUnit,
|
||||
onExpandedChange = { expandedUnit = !expandedUnit }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
readOnly = true,
|
||||
value = selectedUnit.name.lowercase().replaceFirstChar { it.uppercase() }, // Format for display
|
||||
onValueChange = {},
|
||||
label = { Text(stringResource(R.string.measurement_type_label_unit)) },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedUnit)
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor( // Required for ExposedDropdownMenu
|
||||
type = MenuAnchorType.PrimaryNotEditable,
|
||||
enabled = true
|
||||
)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expandedUnit,
|
||||
onDismissRequest = { expandedUnit = false }
|
||||
if (unitDropdownEnabled) {
|
||||
// UnitType Dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expandedUnit && unitDropdownEnabled,
|
||||
onExpandedChange = {
|
||||
if (unitDropdownEnabled) expandedUnit = !expandedUnit
|
||||
}
|
||||
) {
|
||||
UnitType.entries.forEach { unit ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(unit.name.lowercase().replaceFirstChar { it.uppercase() }) },
|
||||
onClick = {
|
||||
selectedUnit = unit
|
||||
expandedUnit = false
|
||||
OutlinedTextField(
|
||||
readOnly = true,
|
||||
value = selectedUnit.displayName.lowercase()
|
||||
.replaceFirstChar { it.uppercase() },
|
||||
onValueChange = {},
|
||||
label = { Text(stringResource(R.string.measurement_type_label_unit)) },
|
||||
trailingIcon = {
|
||||
if (unitDropdownEnabled) {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedUnit)
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor(
|
||||
type = MenuAnchorType.PrimaryNotEditable,
|
||||
enabled = unitDropdownEnabled
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
colors = if (!unitDropdownEnabled) OutlinedTextFieldDefaults.colors(
|
||||
disabledTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledBorderColor = MaterialTheme.colorScheme.outline,
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(
|
||||
alpha = 0.38f
|
||||
)
|
||||
) else OutlinedTextFieldDefaults.colors()
|
||||
)
|
||||
if (unitDropdownEnabled) {
|
||||
ExposedDropdownMenu(
|
||||
expanded = expandedUnit,
|
||||
onDismissRequest = { expandedUnit = false }
|
||||
) {
|
||||
allowedUnitsForKey.forEach { unit ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
unit.displayName.lowercase()
|
||||
.replaceFirstChar { it.uppercase() })
|
||||
},
|
||||
onClick = {
|
||||
selectedUnit = unit
|
||||
expandedUnit = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,17 +382,12 @@ fun MeasurementTypeDetailScreen(
|
||||
) {
|
||||
OutlinedTextField(
|
||||
readOnly = true,
|
||||
value = selectedInputType.name.lowercase().replaceFirstChar { it.uppercase() }, // Format for display
|
||||
value = selectedInputType.name.lowercase().replaceFirstChar { it.uppercase() },
|
||||
onValueChange = {},
|
||||
label = { Text(stringResource(R.string.measurement_type_label_input_type)) },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedInputType)
|
||||
},
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedInputType) },
|
||||
modifier = Modifier
|
||||
.menuAnchor( // Required for ExposedDropdownMenu
|
||||
type = MenuAnchorType.PrimaryNotEditable,
|
||||
enabled = true
|
||||
)
|
||||
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable, enabled = true)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
@@ -371,73 +421,52 @@ fun MeasurementTypeDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Color Picker Dialog
|
||||
if (showColorPicker) {
|
||||
ColorPickerDialog(
|
||||
currentColor = Color(selectedColor),
|
||||
onColorSelected = {
|
||||
selectedColor = it.toArgb()
|
||||
// showColorPicker = false // Keep picker open until explicitly dismissed by user
|
||||
},
|
||||
onColorSelected = { selectedColor = it.toArgb() },
|
||||
onDismiss = { showColorPicker = false }
|
||||
)
|
||||
}
|
||||
|
||||
// Icon Picker Dialog
|
||||
if (showIconPicker) {
|
||||
IconPickerDialog(
|
||||
onIconSelected = {
|
||||
selectedIcon = it
|
||||
showIconPicker = false // Close picker after selection
|
||||
showIconPicker = false
|
||||
},
|
||||
onDismiss = { showIconPicker = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A private composable function that creates a row styled like an [OutlinedTextField]
|
||||
* but designed to hold a label and a custom control (e.g., a [Switch]).
|
||||
*
|
||||
* @param label The text to display as the label for this setting row.
|
||||
* @param modifier Modifier for this composable.
|
||||
* @param controlContent A composable lambda that defines the control to be placed on the right side of the row.
|
||||
*/
|
||||
@Composable
|
||||
private fun OutlinedSettingRow(
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
controlContent: @Composable () -> Unit
|
||||
) {
|
||||
Surface( // Surface for the border and background, mimicking OutlinedTextField
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = OutlinedTextFieldDefaults.MinHeight), // Minimum height similar to OutlinedTextField
|
||||
shape = OutlinedTextFieldDefaults.shape, // Shape similar to OutlinedTextField
|
||||
color = MaterialTheme.colorScheme.surface, // Background color (can be customized)
|
||||
border = BorderStroke( // Border
|
||||
width = 1.dp, // OutlinedTextFieldDefaults.UnfocusedBorderThickness is internal, so using 1.dp
|
||||
color = MaterialTheme.colorScheme.outline // Border color similar to OutlinedTextField
|
||||
)
|
||||
.heightIn(min = OutlinedTextFieldDefaults.MinHeight),
|
||||
shape = OutlinedTextFieldDefaults.shape,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outline)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding( // Internal padding similar to OutlinedTextField
|
||||
start = 16.dp, // Similar to OutlinedTextFieldTokens.InputLeadingPadding
|
||||
end = 16.dp, // Similar to OutlinedTextFieldTokens.InputTrailingPadding
|
||||
top = 8.dp, // Less top padding as the label is centered vertically
|
||||
bottom = 8.dp
|
||||
),
|
||||
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween // Pushes label to start, control to end
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge, // Style for the "label"
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant // Color of the "label"
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
controlContent() // The Switch or other control is placed here
|
||||
controlContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,25 +18,34 @@
|
||||
package com.health.openscale.ui.screen.settings
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.add
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material.icons.filled.UnfoldMore
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
@@ -53,16 +62,20 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavController
|
||||
import com.health.openscale.R
|
||||
import com.health.openscale.core.data.ActivityLevel
|
||||
import com.health.openscale.core.data.GenderType
|
||||
import com.health.openscale.core.data.MeasureUnit
|
||||
import com.health.openscale.core.data.UnitType
|
||||
import com.health.openscale.core.data.User
|
||||
import com.health.openscale.core.data.WeightUnit
|
||||
import com.health.openscale.core.utils.Converters
|
||||
import com.health.openscale.ui.screen.SharedViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DateFormat
|
||||
@@ -111,10 +124,11 @@ fun UserDetailScreen(
|
||||
}
|
||||
}
|
||||
var gender by remember { mutableStateOf(user?.gender ?: GenderType.MALE) }
|
||||
var height by remember { mutableStateOf(user?.heightCm?.toString().orEmpty()) }
|
||||
var heightInputUnit by remember { mutableStateOf(UnitType.CM) }
|
||||
var heightValueString by remember { mutableStateOf("") }
|
||||
val heightUnitsOptions = listOf(UnitType.CM, UnitType.INCH)
|
||||
|
||||
var activityLevel by remember { mutableStateOf(user?.activityLevel ?: ActivityLevel.SEDENTARY) }
|
||||
var scaleUnit by remember { mutableStateOf(user?.scaleUnit ?: WeightUnit.KG) }
|
||||
var measureUnit by remember { mutableStateOf(user?.measureUnit ?: MeasureUnit.CM) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val dateFormatter = remember {
|
||||
@@ -125,8 +139,6 @@ fun UserDetailScreen(
|
||||
val datePickerState = rememberDatePickerState(initialSelectedDateMillis = birthDate)
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
var activityLevelExpanded by remember { mutableStateOf(false) }
|
||||
var scaleUnitExpanded by remember { mutableStateOf(false) }
|
||||
var measureUnitExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDatePicker) {
|
||||
DatePickerDialog(
|
||||
@@ -154,6 +166,24 @@ fun UserDetailScreen(
|
||||
val editUserTitle = stringResource(R.string.user_detail_edit_user_title)
|
||||
val addUserTitle = stringResource(R.string.user_detail_add_user_title)
|
||||
|
||||
LaunchedEffect(user, heightInputUnit) {
|
||||
user?.heightCm?.let { cmValue -> // user.heightCm ist die in der DB gespeicherte Höhe in CM
|
||||
if (cmValue > 0f) {
|
||||
heightValueString = if (heightInputUnit == UnitType.CM) {
|
||||
String.format(Locale.US, "%.1f", cmValue)
|
||||
} else { // heightInputUnit == UnitType.INCH
|
||||
// Konvertiere den CM-Wert aus der DB in Zoll für die Anzeige
|
||||
val inchesValue = Converters.convertFloatValueUnit(cmValue, UnitType.CM, UnitType.INCH)
|
||||
String.format(Locale.US, "%.1f", inchesValue)
|
||||
}
|
||||
} else {
|
||||
heightValueString = "" // Wenn keine valide gespeicherte Höhe, Feld leeren
|
||||
}
|
||||
} ?: run {
|
||||
heightValueString = "" // Neuer User, Feld leer
|
||||
}
|
||||
}
|
||||
|
||||
// Effect to set the top bar title and save action.
|
||||
// This runs when userId changes or the screen is first composed.
|
||||
LaunchedEffect(userId) {
|
||||
@@ -163,17 +193,25 @@ fun UserDetailScreen(
|
||||
)
|
||||
sharedViewModel.setTopBarAction(
|
||||
SharedViewModel.TopBarAction(icon = Icons.Default.Save, onClick = {
|
||||
val validHeight = height.toFloatOrNull()
|
||||
if (name.isNotBlank() && validHeight != null) {
|
||||
val validNumericHeight = heightValueString.toFloatOrNull()
|
||||
var finalHeightCm: Float? = null
|
||||
|
||||
if (validNumericHeight != null && validNumericHeight > 0f) {
|
||||
finalHeightCm = if (heightInputUnit == UnitType.CM) {
|
||||
validNumericHeight // Wert ist bereits in CM
|
||||
} else { // heightInputUnit == UnitType.INCH
|
||||
// Konvertiere den in Zoll eingegebenen Wert zurück zu CM für die Speicherung
|
||||
Converters.convertFloatValueUnit(validNumericHeight, UnitType.INCH, UnitType.CM)
|
||||
}
|
||||
}
|
||||
if (name.isNotBlank() && finalHeightCm != null) {
|
||||
val newUser = User(
|
||||
id = user?.id ?: 0, // Use existing ID if editing, or 0 for Room to auto-generate
|
||||
name = name,
|
||||
birthDate = birthDate,
|
||||
gender = gender,
|
||||
heightCm = validHeight,
|
||||
activityLevel = activityLevel,
|
||||
scaleUnit = scaleUnit,
|
||||
measureUnit = measureUnit
|
||||
heightCm = finalHeightCm,
|
||||
activityLevel = activityLevel
|
||||
)
|
||||
settingsViewModel.viewModelScope.launch {
|
||||
if (isEdit) {
|
||||
@@ -203,7 +241,7 @@ fun UserDetailScreen(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState), // Make the column scrollable
|
||||
.verticalScroll(scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
@@ -213,14 +251,59 @@ fun UserDetailScreen(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text(stringResource(id = R.string.user_detail_label_height))
|
||||
OutlinedTextField(
|
||||
value = height,
|
||||
onValueChange = { height = it },
|
||||
label = { Text(stringResource(id = R.string.user_detail_label_height_cm)) }, // "Height (cm)"
|
||||
value = heightValueString,
|
||||
onValueChange = { newValue ->
|
||||
val filteredValue = newValue.filter { it.isDigit() || it == '.' }
|
||||
if (filteredValue.count { it == '.' } <= 1) {
|
||||
heightValueString = filteredValue
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(R.string.user_detail_label_height)) },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
val currentIndex = heightUnitsOptions.indexOf(heightInputUnit)
|
||||
val nextIndex = (currentIndex + 1) % heightUnitsOptions.size
|
||||
val newUnit = heightUnitsOptions[nextIndex]
|
||||
|
||||
val currentNumericValue = heightValueString.toFloatOrNull()
|
||||
if (currentNumericValue != null && currentNumericValue > 0f) {
|
||||
val convertedValue = Converters.convertFloatValueUnit(currentNumericValue, heightInputUnit, newUnit)
|
||||
heightValueString = String.format(Locale.US, "%.1f", convertedValue)
|
||||
} else {
|
||||
heightValueString = ""
|
||||
}
|
||||
heightInputUnit = newUnit
|
||||
}) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = heightInputUnit.displayName.uppercase(),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
)
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Filled.UnfoldMore,
|
||||
contentDescription = stringResource(R.string.user_detail_content_description_change_unit),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
Text(stringResource(id = R.string.user_detail_label_gender)) // "Gender"
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
GenderType.entries.forEach { option ->
|
||||
@@ -275,74 +358,6 @@ fun UserDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = scaleUnitExpanded,
|
||||
onExpandedChange = { scaleUnitExpanded = !scaleUnitExpanded },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = scaleUnit.toString(),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(id = R.string.user_detail_label_scale_unit)) },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = scaleUnitExpanded)
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = scaleUnitExpanded,
|
||||
onDismissRequest = { scaleUnitExpanded = false }
|
||||
) {
|
||||
WeightUnit.entries.forEach { selectionOption ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(selectionOption.toString()) },
|
||||
onClick = {
|
||||
scaleUnit = selectionOption
|
||||
scaleUnitExpanded = false
|
||||
},
|
||||
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = measureUnitExpanded,
|
||||
onExpandedChange = { measureUnitExpanded = !measureUnitExpanded },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = measureUnit.toString(),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(stringResource(id = R.string.user_detail_label_measure_unit)) },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = measureUnitExpanded)
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = measureUnitExpanded,
|
||||
onDismissRequest = { measureUnitExpanded = false }
|
||||
) {
|
||||
MeasureUnit.entries.forEach { selectionOption ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(selectionOption.toString()) },
|
||||
onClick = {
|
||||
measureUnit = selectionOption
|
||||
measureUnitExpanded = false
|
||||
},
|
||||
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(stringResource(id = R.string.user_detail_label_birth_date)) // "Birth Date"
|
||||
OutlinedTextField(
|
||||
value = dateFormatter.format(Date(birthDate)),
|
||||
|
@@ -48,18 +48,17 @@
|
||||
<string name="user_updated_successfully">Benutzer %1$s erfolgreich aktualisiert.</string>
|
||||
<string name="user_updated_error">Fehler beim Aktualisieren von Benutzer %1$s.</string>
|
||||
<string name="user_detail_label_name">Name</string>
|
||||
<string name="user_detail_label_height_cm">Größe (cm)</string>
|
||||
<string name="user_detail_label_height">Größe</string>
|
||||
<string name="user_detail_label_gender">Geschlecht</string>
|
||||
<string name="user_detail_label_activity_level">Aktivitätslevel</string>
|
||||
<string name="user_detail_label_birth_date">Geburtsdatum</string>
|
||||
<string name="user_detail_error_invalid_data">Bitte geben Sie gültige Daten ein</string>
|
||||
<string name="user_detail_label_scale_unit">Gewichtseinheit</string>
|
||||
<string name="user_detail_label_measure_unit">Größeneinheit</string>
|
||||
<string name="height_value_cm">%.1f cm</string>
|
||||
<string name="user_settings_item_details_conditional">Alter: %1$d, Größe: %2$s</string>
|
||||
<string name="user_settings_content_description_edit">Benutzer bearbeiten</string>
|
||||
<string name="user_settings_content_description_delete">Benutzer löschen</string>
|
||||
<string name="user_settings_content_description_add_user">Neuen Benutzer hinzufügen</string>
|
||||
<string name="user_detail_content_description_change_unit">Einheit ändern</string>
|
||||
|
||||
<!-- Messung CRUD & Operationen -->
|
||||
<string name="success_measurement_updated">Messung erfolgreich aktualisiert</string>
|
||||
|
@@ -49,18 +49,17 @@
|
||||
<string name="user_updated_successfully">User %1$s updated successfully.</string>
|
||||
<string name="user_updated_error">Error updating user %1$s.</string>
|
||||
<string name="user_detail_label_name">Name</string>
|
||||
<string name="user_detail_label_height_cm">Height (cm)</string>
|
||||
<string name="user_detail_label_height">Height</string>
|
||||
<string name="user_detail_label_gender">Gender</string>
|
||||
<string name="user_detail_label_activity_level">Activity Level</string>
|
||||
<string name="user_detail_label_birth_date">Birth Date</string>
|
||||
<string name="user_detail_error_invalid_data">Please enter valid data</string>
|
||||
<string name="user_detail_label_scale_unit">Scale Unit</string>
|
||||
<string name="user_detail_label_measure_unit">Measure Unit</string>
|
||||
<string name="height_value_cm">%.1f cm</string>
|
||||
<string name="user_settings_item_details_conditional">Age: %1$d, Height: %2$s</string>
|
||||
<string name="user_settings_content_description_edit">Edit user</string>
|
||||
<string name="user_settings_content_description_delete">Delete user</string>
|
||||
<string name="user_settings_content_description_add_user">Add new user</string>
|
||||
<string name="user_detail_content_description_change_unit">Change unit</string>
|
||||
|
||||
<!-- Measurement CRUD & Operations -->
|
||||
<string name="success_measurement_updated">Measurement successfully updated</string>
|
||||
|
Reference in New Issue
Block a user