1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-17 22:11:35 +02:00

Enable measurement value conversion when unit changes

This commit is contained in:
oliexdev
2025-08-13 18:15:55 +02:00
parent 8691a33612
commit acded1e050
8 changed files with 276 additions and 3 deletions

View File

@@ -178,8 +178,11 @@ enum class MeasurementTypeKey(
enum class UnitType(val displayName: String) {
KG("kg"),
LB("lb"),
ST("st"),
PERCENT("%"),
CM("cm"),
INCH("in"),
KCAL("kcal"),
NONE("")
}

View File

@@ -179,6 +179,9 @@ class DatabaseRepository(
fun getValuesForMeasurement(measurementId: Int): Flow<List<MeasurementValue>> =
measurementValueDao.getValuesForMeasurement(measurementId)
fun getValuesForType(typeId: Int): Flow<List<MeasurementValue>> =
measurementValueDao.getValuesForType(typeId)
// --- Measurement Type Operations ---
fun getAllMeasurementTypes(): Flow<List<MeasurementType>> = measurementTypeDao.getAll()

View File

@@ -40,4 +40,7 @@ interface MeasurementValueDao {
@Query("SELECT * FROM MeasurementValue WHERE measurementId = :measurementId")
fun getValuesForMeasurement(measurementId: Int): Flow<List<MeasurementValue>>
@Query("SELECT * FROM MeasurementValue WHERE typeId = :typeId")
fun getValuesForType(typeId: Int): Flow<List<MeasurementValue>>
}

View File

@@ -18,6 +18,7 @@
package com.health.openscale.core.utils
import com.health.openscale.core.data.MeasureUnit
import com.health.openscale.core.data.UnitType
import com.health.openscale.core.data.WeightUnit
@@ -174,4 +175,64 @@ object Converters {
toInt32Be(data, 0, value)
return data
}
/**
* Converts a Float value from one UnitType to another, if a conversion is defined.
* Returns the original value if no conversion is applicable or units are the same.
*
* @param value The float value to convert.
* @param fromUnit The original UnitType of the value.
* @param toUnit The target UnitType for the value.
* @return The converted float value, or the original value if no conversion is done.
*/
@JvmStatic
fun convertFloatValueUnit(value: Float, fromUnit: UnitType, toUnit: UnitType): Float {
if (fromUnit == toUnit) return value
// KG -> Andere Gewichtseinheiten
if (fromUnit == UnitType.KG) {
return when (toUnit) {
UnitType.LB -> fromKilogram(value, WeightUnit.LB)
UnitType.ST -> fromKilogram(value, WeightUnit.ST)
else -> value // Keine Umrechnung zu anderen Typen von KG aus
}
}
// LB -> Andere Gewichtseinheiten (erst zu KG, dann zum Ziel)
if (fromUnit == UnitType.LB) {
val kgValue = toKilogram(value, WeightUnit.LB)
return when (toUnit) {
UnitType.KG -> kgValue
UnitType.ST -> fromKilogram(kgValue, WeightUnit.ST)
else -> value
}
}
// ST -> Andere Gewichtseinheiten (erst zu KG, dann zum Ziel)
if (fromUnit == UnitType.ST) {
val kgValue = toKilogram(value, WeightUnit.ST)
return when (toUnit) {
UnitType.KG -> kgValue
UnitType.LB -> fromKilogram(kgValue, WeightUnit.LB)
else -> value
}
}
// CM -> Andere Längeneinheiten
if (fromUnit == UnitType.CM) {
return when (toUnit) {
UnitType.INCH -> fromCentimeter(value, MeasureUnit.INCH)
else -> value
}
}
if (fromUnit == UnitType.INCH) {
val cmValue = toCentimeter(value, MeasureUnit.INCH)
return when (toUnit) {
UnitType.CM -> cmValue
else -> value
}
}
return value
}
}

View File

@@ -35,6 +35,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QuestionMark
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
@@ -48,6 +49,7 @@ import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -76,6 +78,7 @@ import com.health.openscale.ui.screen.SharedViewModel
import com.health.openscale.ui.screen.dialog.ColorPickerDialog
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].
@@ -117,6 +120,9 @@ fun MeasurementTypeDetailScreen(
var showColorPicker by remember { mutableStateOf(false) }
var showIconPicker by remember { mutableStateOf(false) }
var showConfirmDialog by remember { mutableStateOf(false) }
var pendingUpdatedType by remember { mutableStateOf<MeasurementType?>(null) }
val titleEdit = stringResource(R.string.measurement_type_detail_title_edit)
val titleAdd = stringResource(R.string.measurement_type_detail_title_add)
@@ -144,11 +150,20 @@ fun MeasurementTypeDetailScreen(
)
if (isEdit) {
val unitChanged = existingType!!.unit != updatedType.unit
val inputTypesAreFloat = existingType!!.inputType == InputFieldType.FLOAT && updatedType.inputType == InputFieldType.FLOAT
if (unitChanged && inputTypesAreFloat) {
pendingUpdatedType = updatedType
showConfirmDialog = true
} else {
settingsViewModel.updateMeasurementType(updatedType)
navController.popBackStack()
}
} else {
settingsViewModel.addMeasurementType(updatedType)
}
navController.popBackStack()
}
} else {
Toast.makeText(context, R.string.toast_enter_valid_data, Toast.LENGTH_SHORT).show()
}
@@ -156,6 +171,40 @@ fun MeasurementTypeDetailScreen(
)
}
if (showConfirmDialog && existingType != null && pendingUpdatedType != null) {
AlertDialog(
onDismissRequest = { showConfirmDialog = false },
title = { Text(stringResource(R.string.measurement_type_dialog_confirm_unit_change_title)) },
text = {
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() }
)
)
},
confirmButton = {
TextButton(onClick = {
settingsViewModel.updateMeasurementTypeAndConvertDataViewModelCentric(
originalType = existingType!!,
updatedType = pendingUpdatedType!!
)
showConfirmDialog = false
navController.popBackStack()
}) {
Text(stringResource(R.string.confirm_button))
}
},
dismissButton = {
TextButton(onClick = { showConfirmDialog = false }) {
Text(stringResource(R.string.cancel_button))
}
}
)
}
Column(
modifier = Modifier
.padding(16.dp)

View File

@@ -19,6 +19,7 @@ package com.health.openscale.ui.screen.settings
import android.content.ContentResolver
import android.net.Uri
import androidx.compose.material3.SnackbarDuration
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
@@ -31,6 +32,7 @@ import com.health.openscale.core.data.MeasurementTypeKey
import com.health.openscale.core.data.MeasurementValue
import com.health.openscale.core.data.User
import com.health.openscale.core.model.MeasurementWithValues
import com.health.openscale.core.utils.Converters
import com.health.openscale.core.utils.LogManager
import com.health.openscale.ui.screen.SharedViewModel
import kotlinx.coroutines.Dispatchers
@@ -1141,6 +1143,148 @@ class SettingsViewModel(
}
}
/**
* Updates an existing measurement type in the database.
* If the unit has changed for a FLOAT type, it will also convert associated measurement values.
* This version performs operations sequentially without a single overarching repository transaction
* and relies on the ViewModel for orchestration.
*
* @param originalType The MeasurementType as it was BEFORE any edits in the UI.
* Crucially contains the original unit and ID.
* @param updatedType The MeasurementType object with potentially updated information from the UI
* (new name, new unit, new color, etc.).
* @param showSnackbarMaster Boolean to control if a snackbar is shown after the entire operation.
* Individual snackbars for errors might still appear.
*/
fun updateMeasurementTypeAndConvertDataViewModelCentric(
originalType: MeasurementType,
updatedType: MeasurementType,
showSnackbarMaster: Boolean = true
) {
viewModelScope.launch {
LogManager.i(
TAG,
"ViewModelCentric Update: Type ID ${originalType.id}. Original Unit: ${originalType.unit}, New Unit: ${updatedType.unit}"
)
var conversionErrorOccurred = false
var valuesConvertedCount = 0
if (originalType.unit != updatedType.unit && originalType.inputType == InputFieldType.FLOAT && updatedType.inputType == InputFieldType.FLOAT) {
LogManager.i(
TAG,
"Unit changed for FLOAT type ID ${originalType.id} from ${originalType.unit} to ${updatedType.unit}. Converting values."
)
try {
val valuesToConvert = repository.getValuesForType(originalType.id).first() // .first() um den aktuellen Wert des Flows zu erhalten
if (valuesToConvert.isNotEmpty()) {
LogManager.d(TAG, "Found ${valuesToConvert.size} values of type ID ${originalType.id} to potentially convert.")
val updatedValuesBatch = mutableListOf<MeasurementValue>()
valuesToConvert.forEach { valueToConvert ->
valueToConvert.floatValue?.let { currentFloatVal ->
val convertedFloat = Converters.convertFloatValueUnit(
value = currentFloatVal,
fromUnit = originalType.unit,
toUnit = updatedType.unit
)
if (convertedFloat != currentFloatVal) {
updatedValuesBatch.add(valueToConvert.copy(floatValue = convertedFloat))
valuesConvertedCount++
}
}
}
if (updatedValuesBatch.isNotEmpty()) {
LogManager.d(TAG, "Updating ${updatedValuesBatch.size} values in batch for type ID ${originalType.id}.")
// Aktualisiere jeden Wert einzeln. Dein Repository.updateMeasurementValue
// stößt die Neuberechnung der abgeleiteten Werte an.
updatedValuesBatch.forEach { repository.updateMeasurementValue(it) }
LogManager.d(TAG, "Batch update of ${updatedValuesBatch.size} values completed for type ID ${originalType.id}.")
} else {
LogManager.i(TAG, "No values required actual conversion or update for type ID ${originalType.id} after checking.")
}
} else {
LogManager.i(TAG, "No values found for type ID ${originalType.id} to convert.")
}
} catch (e: Exception) {
LogManager.e(TAG, "Error during value conversion/update for type ID ${originalType.id}", e)
conversionErrorOccurred = true
// Verwende hier getDisplayName vom originalType, da updatedType möglicherweise noch nicht committet wurde.
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_update_error_conversion_failed,
formatArgs = listOf(originalType.name ?: originalType.key.toString())
)
}
} else if (originalType.unit != updatedType.unit) {
LogManager.i(
TAG,
"Unit changed for type ID ${originalType.id}, but InputType is not FLOAT (Original: ${originalType.inputType}, Updated: ${updatedType.inputType}). No value conversion performed."
)
}
if (!conversionErrorOccurred) {
try {
val finalTypeToUpdate = MeasurementType(
id = originalType.id,
key = originalType.key,
name = updatedType.name,
color = updatedType.color,
icon = updatedType.icon,
unit = updatedType.unit,
inputType = updatedType.inputType,
displayOrder = originalType.displayOrder,
isDerived = originalType.isDerived,
isEnabled = updatedType.isEnabled,
isPinned = updatedType.isPinned,
isOnRightYAxis = updatedType.isOnRightYAxis
)
repository.updateMeasurementType(finalTypeToUpdate)
LogManager.i(
TAG,
"MeasurementType (ID: ${originalType.id}) updated successfully to new unit '${finalTypeToUpdate.unit}'."
)
if (showSnackbarMaster) {
if (valuesConvertedCount > 0) {
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_and_values_converted_successfully,
formatArgs = listOf(updatedType.name ?: updatedType.key.toString(), valuesConvertedCount.toString()) // Context für getDisplayName wäre besser
)
} else if (originalType.unit != updatedType.unit && originalType.inputType == InputFieldType.FLOAT) {
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_unit_changed_no_values_converted,
formatArgs = listOf(updatedType.name ?: updatedType.key.toString()) // Context für getDisplayName wäre besser
)
}
else {
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_successfully,
formatArgs = listOf(updatedType.name ?: updatedType.key.toString()) // Context für getDisplayName wäre besser
)
}
}
// sharedViewModel.refreshMeasurementTypes() // Optional, wenn die Liste sich nicht automatisch aktualisiert
} catch (e: Exception) {
LogManager.e(TAG, "Error updating MeasurementType (ID: ${originalType.id}) itself", e)
sharedViewModel.showSnackbar(
messageResId = R.string.measurement_type_updated_error,
formatArgs = listOf(originalType.name ?: originalType.key.toString()) // Context für getDisplayName wäre besser
)
}
} else {
LogManager.w(
TAG,
"Skipped updating MeasurementType definition (ID: ${originalType.id}) due to prior value conversion error."
)
}
}
}
/**
* Updates an existing measurement type in the database.
* This operation is performed in a background coroutine.

View File

@@ -152,6 +152,11 @@
<string name="measurement_type_deleted_error">Fehler beim Löschen der Messart \'%1$s\'.</string>
<string name="measurement_type_updated_successfully">Messart \'%1$s\' erfolgreich aktualisiert.</string>
<string name="measurement_type_updated_error">Fehler beim Aktualisieren der Messart \'%1$s\'.</string>
<string name="measurement_type_updated_and_values_converted_successfully">Typ \'%1$s\' aktualisiert und %2$s Wert(e) erfolgreich konvertiert.</string>
<string name="measurement_type_update_error_conversion_failed">Fehler beim Konvertieren der Werte für Typ \'%1$s\'. Die Typdefinition wurde nicht aktualisiert.</string>
<string name="measurement_type_updated_unit_changed_no_values_converted">Typ \'%1$s\' aktualisiert. Einheit geändert, aber es mussten keine vorhandenen Werte konvertiert werden.</string>
<string name="measurement_type_dialog_confirm_unit_change_title">Einheitenänderung bestätigen</string>
<string name="measurement_type_dialog_confirm_unit_change_message">Wenn Sie die Einheit für \'%1$s\' von %2$s auf %3$s ändern, werden alle vorhandenen Datenpunkte konvertiert. Dieser Vorgang kann einige Zeit dauern und kann nicht einfach rückgängig gemacht werden. Möchten Sie fortfahren?</string>
<!-- Bluetooth -->
<string name="info_bluetooth_connection_error_scale_offline">Verbindung zur Waage fehlgeschlagen, bitte stellen Sie sicher, dass sie eingeschaltet ist.</string>

View File

@@ -151,8 +151,13 @@
<string name="measurement_type_added_error">Error adding measurement type \'%1$s\'.</string>
<string name="measurement_type_deleted_successfully">Measurement type \'%1$s\' deleted successfully.</string>
<string name="measurement_type_deleted_error">Error deleting measurement type \'%1$s\'.</string>
<string name="measurement_type_updated_successfully">Measurement type \'%1$s\' updated successfully.</string>
<string name="measurement_type_updated_error">Error updating measurement type \'%1$s\'.</string>
<string name="measurement_type_updated_successfully">Measurement type \'%1$s\' updated successfully.</string>
<string name="measurement_type_updated_and_values_converted_successfully">Type \'%1$s\' updated and %2$s value(s) converted successfully.</string>
<string name="measurement_type_update_error_conversion_failed">Error converting values for type \'%1$s\'. The type definition was not updated.</string>
<string name="measurement_type_updated_unit_changed_no_values_converted">Type \'%1$s\' updated. Unit changed, but no existing values required conversion.</string>
<string name="measurement_type_dialog_confirm_unit_change_title">Confirm Unit Change</string>
<string name="measurement_type_dialog_confirm_unit_change_message">Changing the unit for \'%1$s\' from %2$s to %3$s will convert all existing data points. This action may take some time and cannot be easily undone. Do you want to proceed?</string>
<!-- Bluetooth -->
<string name="info_bluetooth_connection_error_scale_offline">Could not connect to scale, please ensure it is on.</string>