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:
@@ -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("")
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -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>>
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
@@ -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)
|
||||
|
@@ -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.
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user