mirror of
https://github.com/oliexdev/openScale.git
synced 2025-08-17 22:11:35 +02:00
Refactor: Remove unit settings from user, use measurement type settings
This commit is contained in:
@@ -2,11 +2,11 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"identityHash": "877ad250be34067d136497a388177415",
|
"identityHash": "3eb1fa6c355e18713262b7da7d1ffbdd",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "User",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
@@ -43,18 +43,6 @@
|
|||||||
"columnName": "activityLevel",
|
"columnName": "activityLevel",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "scaleUnit",
|
|
||||||
"columnName": "scaleUnit",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "measureUnit",
|
|
||||||
"columnName": "measureUnit",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
@@ -298,7 +286,7 @@
|
|||||||
],
|
],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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(
|
enum class MeasurementTypeKey(
|
||||||
val id: Int,
|
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),
|
WEIGHT(1, R.string.measurement_type_weight, listOf(UnitType.KG, UnitType.LB, UnitType.ST)),
|
||||||
BMI(2, R.string.measurement_type_bmi),
|
BMI(2, R.string.measurement_type_bmi, listOf(UnitType.NONE)),
|
||||||
BODY_FAT(3, R.string.measurement_type_body_fat),
|
BODY_FAT(3, R.string.measurement_type_body_fat, listOf(UnitType.PERCENT)),
|
||||||
WATER(4, R.string.measurement_type_water),
|
WATER(4, R.string.measurement_type_water, listOf(UnitType.PERCENT)),
|
||||||
MUSCLE(5, R.string.measurement_type_muscle),
|
MUSCLE(5, R.string.measurement_type_muscle, listOf(UnitType.PERCENT, UnitType.KG, UnitType.LB)),
|
||||||
LBM(6, R.string.measurement_type_lbm),
|
LBM(6, R.string.measurement_type_lbm, listOf(UnitType.KG, UnitType.LB, UnitType.ST)),
|
||||||
BONE(7, R.string.measurement_type_bone),
|
BONE(7, R.string.measurement_type_bone, listOf(UnitType.KG, UnitType.LB)),
|
||||||
WAIST(8, R.string.measurement_type_waist),
|
WAIST(8, R.string.measurement_type_waist, listOf(UnitType.CM, UnitType.INCH)),
|
||||||
WHR(9, R.string.measurement_type_whr),
|
WHR(9, R.string.measurement_type_whr, listOf(UnitType.NONE)),
|
||||||
WHTR(10, R.string.measurement_type_whtr),
|
WHTR(10, R.string.measurement_type_whtr, listOf(UnitType.NONE)),
|
||||||
HIPS(11, R.string.measurement_type_hips),
|
HIPS(11, R.string.measurement_type_hips, listOf(UnitType.CM, UnitType.INCH)),
|
||||||
VISCERAL_FAT(12, R.string.measurement_type_visceral_fat),
|
VISCERAL_FAT(12, R.string.measurement_type_visceral_fat, listOf(UnitType.PERCENT, UnitType.NONE)),
|
||||||
CHEST(13, R.string.measurement_type_chest),
|
CHEST(13, R.string.measurement_type_chest, listOf(UnitType.CM, UnitType.INCH)),
|
||||||
THIGH(14, R.string.measurement_type_thigh),
|
THIGH(14, R.string.measurement_type_thigh, listOf(UnitType.CM, UnitType.INCH)),
|
||||||
BICEPS(15, R.string.measurement_type_biceps),
|
BICEPS(15, R.string.measurement_type_biceps, listOf(UnitType.CM, UnitType.INCH)),
|
||||||
NECK(16, R.string.measurement_type_neck),
|
NECK(16, R.string.measurement_type_neck, listOf(UnitType.CM, UnitType.INCH)),
|
||||||
CALIPER_1(17, R.string.measurement_type_caliper1),
|
CALIPER_1(17, R.string.measurement_type_caliper1, listOf(UnitType.CM, UnitType.INCH)),
|
||||||
CALIPER_2(18, R.string.measurement_type_caliper2),
|
CALIPER_2(18, R.string.measurement_type_caliper2, listOf(UnitType.CM, UnitType.INCH)),
|
||||||
CALIPER_3(19, R.string.measurement_type_caliper3),
|
CALIPER_3(19, R.string.measurement_type_caliper3, listOf(UnitType.CM, UnitType.INCH)),
|
||||||
CALIPER(20, R.string.measurement_type_fat_caliper),
|
CALIPER(20, R.string.measurement_type_fat_caliper, listOf(UnitType.PERCENT, UnitType.NONE)),
|
||||||
BMR(21, R.string.measurement_type_bmr),
|
BMR(21, R.string.measurement_type_bmr, listOf(UnitType.KCAL)),
|
||||||
TDEE(22, R.string.measurement_type_tdee),
|
TDEE(22, R.string.measurement_type_tdee, listOf(UnitType.KCAL)),
|
||||||
CALORIES(23, R.string.measurement_type_calories),
|
CALORIES(23, R.string.measurement_type_calories, listOf(UnitType.KCAL)),
|
||||||
DATE(24, R.string.measurement_type_date),
|
DATE(24, R.string.measurement_type_date, listOf(UnitType.NONE)),
|
||||||
TIME(25, R.string.measurement_type_time),
|
TIME(25, R.string.measurement_type_time, listOf(UnitType.NONE)),
|
||||||
COMMENT(26, R.string.measurement_type_comment),
|
COMMENT(26, R.string.measurement_type_comment, listOf(UnitType.NONE)),
|
||||||
CUSTOM(99, R.string.measurement_type_custom_default_name);
|
CUSTOM(99, R.string.measurement_type_custom_default_name, UnitType.entries.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -27,7 +27,5 @@ data class User(
|
|||||||
val birthDate: Long,
|
val birthDate: Long,
|
||||||
val gender: GenderType,
|
val gender: GenderType,
|
||||||
val heightCm: Float,
|
val heightCm: Float,
|
||||||
val activityLevel: ActivityLevel,
|
val activityLevel: ActivityLevel
|
||||||
var scaleUnit: WeightUnit,
|
|
||||||
var measureUnit: MeasureUnit
|
|
||||||
)
|
)
|
@@ -54,6 +54,7 @@ import androidx.compose.material3.TextFieldDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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 com.health.openscale.ui.screen.dialog.getIconResIdByName
|
||||||
import kotlin.text.lowercase
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MeasurementTypeDetailScreen(
|
fun MeasurementTypeDetailScreen(
|
||||||
@@ -101,19 +92,44 @@ fun MeasurementTypeDetailScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val measurementTypes by sharedViewModel.measurementTypes.collectAsState()
|
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 }
|
measurementTypes.find { it.id == typeId }
|
||||||
}
|
}
|
||||||
val isEdit = typeId != -1
|
val isEdit = typeId != -1
|
||||||
|
|
||||||
var name by remember { mutableStateOf(existingType?.getDisplayName(context).orEmpty()) }
|
// Determine the MeasurementTypeKey for the allowed units logic.
|
||||||
var selectedUnit by remember { mutableStateOf(existingType?.unit ?: UnitType.NONE) }
|
// For new types, it's always CUSTOM; for existing types, it's the type's key.
|
||||||
var selectedInputType by remember { mutableStateOf(existingType?.inputType ?: InputFieldType.FLOAT) }
|
val currentMeasurementTypeKey = remember(originalExistingType, isEdit) {
|
||||||
var selectedColor by remember { mutableStateOf(existingType?.color ?: 0xFF6200EE.toInt()) } // Default color
|
if (isEdit) originalExistingType?.key ?: MeasurementTypeKey.CUSTOM
|
||||||
var selectedIcon by remember { mutableStateOf(existingType?.icon ?: "ic_weight") } // Default icon
|
else MeasurementTypeKey.CUSTOM
|
||||||
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) }
|
// 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 expandedUnit by remember { mutableStateOf(false) }
|
||||||
var expandedInputType 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 titleEdit = stringResource(R.string.measurement_type_detail_title_edit)
|
||||||
val titleAdd = stringResource(R.string.measurement_type_detail_title_add)
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
sharedViewModel.setTopBarTitle(
|
sharedViewModel.setTopBarTitle(if (isEdit) titleEdit else titleAdd)
|
||||||
if (isEdit) titleEdit
|
|
||||||
else titleAdd
|
|
||||||
)
|
|
||||||
sharedViewModel.setTopBarAction(
|
sharedViewModel.setTopBarAction(
|
||||||
SharedViewModel.TopBarAction(icon = Icons.Default.Save, onClick = {
|
SharedViewModel.TopBarAction(icon = Icons.Default.Save, onClick = {
|
||||||
if (name.isNotBlank()) {
|
if (name.isNotBlank()) {
|
||||||
val updatedType = MeasurementType(
|
// When creating the updatedType, use the key of the originalExistingType if it's an edit.
|
||||||
id = existingType?.id ?: 0, // Use 0 for new types, Room will autogenerate
|
// 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,
|
name = name,
|
||||||
icon = selectedIcon,
|
icon = selectedIcon,
|
||||||
color = selectedColor,
|
color = selectedColor,
|
||||||
unit = selectedUnit,
|
unit = selectedUnit,
|
||||||
inputType = selectedInputType,
|
inputType = selectedInputType,
|
||||||
displayOrder = existingType?.displayOrder ?: measurementTypes.size,
|
displayOrder = originalExistingType?.displayOrder ?: measurementTypes.size,
|
||||||
isEnabled = isEnabled,
|
isEnabled = isEnabled,
|
||||||
isPinned = isPinned,
|
isPinned = isPinned,
|
||||||
key = existingType?.key ?: MeasurementTypeKey.CUSTOM, // New types are custom
|
key = finalKey, // Use the correct key
|
||||||
isDerived = existingType?.isDerived ?: false, // New types are not derived by default
|
isDerived = originalExistingType?.isDerived ?: false,
|
||||||
isOnRightYAxis = isOnRightYAxis
|
isOnRightYAxis = isOnRightYAxis
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isEdit) {
|
if (isEdit && originalExistingType != null) {
|
||||||
val unitChanged = existingType!!.unit != updatedType.unit
|
val unitChanged = originalExistingType.unit != currentUpdatedType.unit
|
||||||
val inputTypesAreFloat = existingType!!.inputType == InputFieldType.FLOAT && updatedType.inputType == InputFieldType.FLOAT
|
val inputTypesAreFloat = originalExistingType.inputType == InputFieldType.FLOAT && currentUpdatedType.inputType == InputFieldType.FLOAT
|
||||||
|
|
||||||
if (unitChanged && inputTypesAreFloat) {
|
if (unitChanged && inputTypesAreFloat) {
|
||||||
pendingUpdatedType = updatedType
|
pendingUpdatedType = currentUpdatedType
|
||||||
showConfirmDialog = true
|
showConfirmDialog = true
|
||||||
} else {
|
} else {
|
||||||
settingsViewModel.updateMeasurementType(updatedType)
|
settingsViewModel.updateMeasurementType(currentUpdatedType)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settingsViewModel.addMeasurementType(updatedType)
|
settingsViewModel.addMeasurementType(currentUpdatedType)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -171,7 +209,7 @@ fun MeasurementTypeDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showConfirmDialog && existingType != null && pendingUpdatedType != null) {
|
if (showConfirmDialog && originalExistingType != null && pendingUpdatedType != null) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { showConfirmDialog = false },
|
onDismissRequest = { showConfirmDialog = false },
|
||||||
title = { Text(stringResource(R.string.measurement_type_dialog_confirm_unit_change_title)) },
|
title = { Text(stringResource(R.string.measurement_type_dialog_confirm_unit_change_title)) },
|
||||||
@@ -179,28 +217,24 @@ fun MeasurementTypeDetailScreen(
|
|||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.measurement_type_dialog_confirm_unit_change_message,
|
R.string.measurement_type_dialog_confirm_unit_change_message,
|
||||||
existingType!!.getDisplayName(context),
|
originalExistingType.getDisplayName(context),
|
||||||
existingType!!.unit.name.lowercase().replaceFirstChar { it.uppercase() },
|
originalExistingType.unit.displayName.lowercase().replaceFirstChar { it.uppercase() },
|
||||||
pendingUpdatedType!!.unit.name.lowercase().replaceFirstChar { it.uppercase() }
|
pendingUpdatedType!!.unit.displayName.lowercase().replaceFirstChar { it.uppercase() }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
settingsViewModel.updateMeasurementTypeAndConvertDataViewModelCentric(
|
settingsViewModel.updateMeasurementTypeAndConvertDataViewModelCentric(
|
||||||
originalType = existingType!!,
|
originalType = originalExistingType,
|
||||||
updatedType = pendingUpdatedType!!
|
updatedType = pendingUpdatedType!!
|
||||||
)
|
)
|
||||||
showConfirmDialog = false
|
showConfirmDialog = false
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
}) {
|
}) { Text(stringResource(R.string.confirm_button)) }
|
||||||
Text(stringResource(R.string.confirm_button))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showConfirmDialog = false }) {
|
TextButton(onClick = { showConfirmDialog = false }) { Text(stringResource(R.string.cancel_button)) }
|
||||||
Text(stringResource(R.string.cancel_button))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -222,12 +256,14 @@ fun MeasurementTypeDetailScreen(
|
|||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
label = { Text(stringResource(R.string.measurement_type_label_name)) },
|
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(
|
OutlinedTextField(
|
||||||
value = String.format("#%06X", 0xFFFFFF and selectedColor), // Display color hex string
|
value = String.format("#%06X", 0xFFFFFF and selectedColor),
|
||||||
onValueChange = {}, // Read-only
|
onValueChange = {}, // Read-only
|
||||||
label = { Text(stringResource(R.string.measurement_type_label_color)) },
|
label = { Text(stringResource(R.string.measurement_type_label_color)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -241,21 +277,20 @@ fun MeasurementTypeDetailScreen(
|
|||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(Color(selectedColor))
|
.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,
|
disabledTextColor = LocalContentColor.current,
|
||||||
disabledIndicatorColor = MaterialTheme.colorScheme.outline, // Standard outline
|
disabledIndicatorColor = MaterialTheme.colorScheme.outline,
|
||||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, // Standard label color
|
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
disabledContainerColor = Color.Transparent // No background fill
|
disabledContainerColor = Color.Transparent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Icon Selector
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = selectedIcon, // Display selected icon name
|
value = selectedIcon,
|
||||||
onValueChange = {}, // Read-only
|
onValueChange = {}, // Read-only
|
||||||
label = { Text(stringResource(R.string.measurement_type_label_icon)) },
|
label = { Text(stringResource(R.string.measurement_type_label_icon)) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -268,14 +303,13 @@ fun MeasurementTypeDetailScreen(
|
|||||||
painter = runCatching {
|
painter = runCatching {
|
||||||
painterResource(id = getIconResIdByName(selectedIcon))
|
painterResource(id = getIconResIdByName(selectedIcon))
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
// Fallback icon if resource name is invalid or not found
|
Icons.Filled.QuestionMark // Fallback icon
|
||||||
Icons.Filled.QuestionMark
|
} as Painter,
|
||||||
} as Painter, // Cast is safe due to getOrElse structure
|
|
||||||
contentDescription = stringResource(R.string.content_desc_selected_icon_preview),
|
contentDescription = stringResource(R.string.content_desc_selected_icon_preview),
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
colors = TextFieldDefaults.colors( // Custom colors for consistent look
|
colors = TextFieldDefaults.colors(
|
||||||
disabledTextColor = LocalContentColor.current,
|
disabledTextColor = LocalContentColor.current,
|
||||||
disabledIndicatorColor = MaterialTheme.colorScheme.outline,
|
disabledIndicatorColor = MaterialTheme.colorScheme.outline,
|
||||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -284,38 +318,59 @@ fun MeasurementTypeDetailScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnitType Dropdown
|
if (unitDropdownEnabled) {
|
||||||
ExposedDropdownMenuBox(
|
// UnitType Dropdown
|
||||||
expanded = expandedUnit,
|
ExposedDropdownMenuBox(
|
||||||
onExpandedChange = { expandedUnit = !expandedUnit }
|
expanded = expandedUnit && unitDropdownEnabled,
|
||||||
) {
|
onExpandedChange = {
|
||||||
OutlinedTextField(
|
if (unitDropdownEnabled) expandedUnit = !expandedUnit
|
||||||
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 }
|
|
||||||
) {
|
) {
|
||||||
UnitType.entries.forEach { unit ->
|
OutlinedTextField(
|
||||||
DropdownMenuItem(
|
readOnly = true,
|
||||||
text = { Text(unit.name.lowercase().replaceFirstChar { it.uppercase() }) },
|
value = selectedUnit.displayName.lowercase()
|
||||||
onClick = {
|
.replaceFirstChar { it.uppercase() },
|
||||||
selectedUnit = unit
|
onValueChange = {},
|
||||||
expandedUnit = false
|
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(
|
OutlinedTextField(
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
value = selectedInputType.name.lowercase().replaceFirstChar { it.uppercase() }, // Format for display
|
value = selectedInputType.name.lowercase().replaceFirstChar { it.uppercase() },
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
label = { Text(stringResource(R.string.measurement_type_label_input_type)) },
|
label = { Text(stringResource(R.string.measurement_type_label_input_type)) },
|
||||||
trailingIcon = {
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedInputType) },
|
||||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedInputType)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.menuAnchor( // Required for ExposedDropdownMenu
|
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable, enabled = true)
|
||||||
type = MenuAnchorType.PrimaryNotEditable,
|
|
||||||
enabled = true
|
|
||||||
)
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
ExposedDropdownMenu(
|
ExposedDropdownMenu(
|
||||||
@@ -371,73 +421,52 @@ fun MeasurementTypeDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color Picker Dialog
|
|
||||||
if (showColorPicker) {
|
if (showColorPicker) {
|
||||||
ColorPickerDialog(
|
ColorPickerDialog(
|
||||||
currentColor = Color(selectedColor),
|
currentColor = Color(selectedColor),
|
||||||
onColorSelected = {
|
onColorSelected = { selectedColor = it.toArgb() },
|
||||||
selectedColor = it.toArgb()
|
|
||||||
// showColorPicker = false // Keep picker open until explicitly dismissed by user
|
|
||||||
},
|
|
||||||
onDismiss = { showColorPicker = false }
|
onDismiss = { showColorPicker = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icon Picker Dialog
|
|
||||||
if (showIconPicker) {
|
if (showIconPicker) {
|
||||||
IconPickerDialog(
|
IconPickerDialog(
|
||||||
onIconSelected = {
|
onIconSelected = {
|
||||||
selectedIcon = it
|
selectedIcon = it
|
||||||
showIconPicker = false // Close picker after selection
|
showIconPicker = false
|
||||||
},
|
},
|
||||||
onDismiss = { 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
|
@Composable
|
||||||
private fun OutlinedSettingRow(
|
private fun OutlinedSettingRow(
|
||||||
label: String,
|
label: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
controlContent: @Composable () -> Unit
|
controlContent: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
Surface( // Surface for the border and background, mimicking OutlinedTextField
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = OutlinedTextFieldDefaults.MinHeight), // Minimum height similar to OutlinedTextField
|
.heightIn(min = OutlinedTextFieldDefaults.MinHeight),
|
||||||
shape = OutlinedTextFieldDefaults.shape, // Shape similar to OutlinedTextField
|
shape = OutlinedTextFieldDefaults.shape,
|
||||||
color = MaterialTheme.colorScheme.surface, // Background color (can be customized)
|
color = MaterialTheme.colorScheme.surface,
|
||||||
border = BorderStroke( // Border
|
border = BorderStroke(width = 1.dp, color = MaterialTheme.colorScheme.outline)
|
||||||
width = 1.dp, // OutlinedTextFieldDefaults.UnfocusedBorderThickness is internal, so using 1.dp
|
|
||||||
color = MaterialTheme.colorScheme.outline // Border color similar to OutlinedTextField
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding( // Internal padding similar to OutlinedTextField
|
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp),
|
||||||
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
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween // Pushes label to start, control to end
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
style = MaterialTheme.typography.bodyLarge, // Style for the "label"
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant // Color of the "label"
|
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
|
package com.health.openscale.ui.screen.settings
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.add
|
import androidx.compose.foundation.layout.add
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Save
|
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.DatePicker
|
||||||
import androidx.compose.material3.DatePickerDialog
|
import androidx.compose.material3.DatePickerDialog
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
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.MenuAnchorType
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
@@ -53,16 +62,20 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.health.openscale.R
|
import com.health.openscale.R
|
||||||
import com.health.openscale.core.data.ActivityLevel
|
import com.health.openscale.core.data.ActivityLevel
|
||||||
import com.health.openscale.core.data.GenderType
|
import com.health.openscale.core.data.GenderType
|
||||||
import com.health.openscale.core.data.MeasureUnit
|
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.User
|
||||||
import com.health.openscale.core.data.WeightUnit
|
import com.health.openscale.core.data.WeightUnit
|
||||||
|
import com.health.openscale.core.utils.Converters
|
||||||
import com.health.openscale.ui.screen.SharedViewModel
|
import com.health.openscale.ui.screen.SharedViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@@ -111,10 +124,11 @@ fun UserDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var gender by remember { mutableStateOf(user?.gender ?: GenderType.MALE) }
|
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 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 context = LocalContext.current
|
||||||
val dateFormatter = remember {
|
val dateFormatter = remember {
|
||||||
@@ -125,8 +139,6 @@ fun UserDetailScreen(
|
|||||||
val datePickerState = rememberDatePickerState(initialSelectedDateMillis = birthDate)
|
val datePickerState = rememberDatePickerState(initialSelectedDateMillis = birthDate)
|
||||||
var showDatePicker by remember { mutableStateOf(false) }
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
var activityLevelExpanded by remember { mutableStateOf(false) }
|
var activityLevelExpanded by remember { mutableStateOf(false) }
|
||||||
var scaleUnitExpanded by remember { mutableStateOf(false) }
|
|
||||||
var measureUnitExpanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (showDatePicker) {
|
if (showDatePicker) {
|
||||||
DatePickerDialog(
|
DatePickerDialog(
|
||||||
@@ -154,6 +166,24 @@ fun UserDetailScreen(
|
|||||||
val editUserTitle = stringResource(R.string.user_detail_edit_user_title)
|
val editUserTitle = stringResource(R.string.user_detail_edit_user_title)
|
||||||
val addUserTitle = stringResource(R.string.user_detail_add_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.
|
// Effect to set the top bar title and save action.
|
||||||
// This runs when userId changes or the screen is first composed.
|
// This runs when userId changes or the screen is first composed.
|
||||||
LaunchedEffect(userId) {
|
LaunchedEffect(userId) {
|
||||||
@@ -163,17 +193,25 @@ fun UserDetailScreen(
|
|||||||
)
|
)
|
||||||
sharedViewModel.setTopBarAction(
|
sharedViewModel.setTopBarAction(
|
||||||
SharedViewModel.TopBarAction(icon = Icons.Default.Save, onClick = {
|
SharedViewModel.TopBarAction(icon = Icons.Default.Save, onClick = {
|
||||||
val validHeight = height.toFloatOrNull()
|
val validNumericHeight = heightValueString.toFloatOrNull()
|
||||||
if (name.isNotBlank() && validHeight != null) {
|
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(
|
val newUser = User(
|
||||||
id = user?.id ?: 0, // Use existing ID if editing, or 0 for Room to auto-generate
|
id = user?.id ?: 0, // Use existing ID if editing, or 0 for Room to auto-generate
|
||||||
name = name,
|
name = name,
|
||||||
birthDate = birthDate,
|
birthDate = birthDate,
|
||||||
gender = gender,
|
gender = gender,
|
||||||
heightCm = validHeight,
|
heightCm = finalHeightCm,
|
||||||
activityLevel = activityLevel,
|
activityLevel = activityLevel
|
||||||
scaleUnit = scaleUnit,
|
|
||||||
measureUnit = measureUnit
|
|
||||||
)
|
)
|
||||||
settingsViewModel.viewModelScope.launch {
|
settingsViewModel.viewModelScope.launch {
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
@@ -203,7 +241,7 @@ fun UserDetailScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(scrollState), // Make the column scrollable
|
.verticalScroll(scrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
@@ -213,14 +251,59 @@ fun UserDetailScreen(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Text(stringResource(id = R.string.user_detail_label_height))
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = height,
|
value = heightValueString,
|
||||||
onValueChange = { height = it },
|
onValueChange = { newValue ->
|
||||||
label = { Text(stringResource(id = R.string.user_detail_label_height_cm)) }, // "Height (cm)"
|
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),
|
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"
|
Text(stringResource(id = R.string.user_detail_label_gender)) // "Gender"
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
GenderType.entries.forEach { option ->
|
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"
|
Text(stringResource(id = R.string.user_detail_label_birth_date)) // "Birth Date"
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = dateFormatter.format(Date(birthDate)),
|
value = dateFormatter.format(Date(birthDate)),
|
||||||
|
@@ -48,18 +48,17 @@
|
|||||||
<string name="user_updated_successfully">Benutzer %1$s erfolgreich aktualisiert.</string>
|
<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_updated_error">Fehler beim Aktualisieren von Benutzer %1$s.</string>
|
||||||
<string name="user_detail_label_name">Name</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_gender">Geschlecht</string>
|
||||||
<string name="user_detail_label_activity_level">Aktivitätslevel</string>
|
<string name="user_detail_label_activity_level">Aktivitätslevel</string>
|
||||||
<string name="user_detail_label_birth_date">Geburtsdatum</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_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="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_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_edit">Benutzer bearbeiten</string>
|
||||||
<string name="user_settings_content_description_delete">Benutzer löschen</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_settings_content_description_add_user">Neuen Benutzer hinzufügen</string>
|
||||||
|
<string name="user_detail_content_description_change_unit">Einheit ändern</string>
|
||||||
|
|
||||||
<!-- Messung CRUD & Operationen -->
|
<!-- Messung CRUD & Operationen -->
|
||||||
<string name="success_measurement_updated">Messung erfolgreich aktualisiert</string>
|
<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_successfully">User %1$s updated successfully.</string>
|
||||||
<string name="user_updated_error">Error updating user %1$s.</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_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_gender">Gender</string>
|
||||||
<string name="user_detail_label_activity_level">Activity Level</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_label_birth_date">Birth Date</string>
|
||||||
<string name="user_detail_error_invalid_data">Please enter valid data</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="height_value_cm">%.1f cm</string>
|
||||||
<string name="user_settings_item_details_conditional">Age: %1$d, Height: %2$s</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_edit">Edit user</string>
|
||||||
<string name="user_settings_content_description_delete">Delete 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_settings_content_description_add_user">Add new user</string>
|
||||||
|
<string name="user_detail_content_description_change_unit">Change unit</string>
|
||||||
|
|
||||||
<!-- Measurement CRUD & Operations -->
|
<!-- Measurement CRUD & Operations -->
|
||||||
<string name="success_measurement_updated">Measurement successfully updated</string>
|
<string name="success_measurement_updated">Measurement successfully updated</string>
|
||||||
|
Reference in New Issue
Block a user