mirror of
https://github.com/oliexdev/openScale.git
synced 2025-09-02 21:02:48 +02:00
This commit introduces the ability to select and apply body composition formulas (Body Fat, Body Water, LBM) within the measurement type detail screen.
This commit is contained in:
@@ -399,4 +399,97 @@ enum class ConnectionStatus {
|
||||
DISCONNECTING,
|
||||
/** A connection attempt failed or connection broke unexpectedly. */
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
|
||||
enum class BodyFatFormulaOption {
|
||||
OFF,
|
||||
DEURENBERG_1991,
|
||||
DEURENBERG_1992,
|
||||
EDDY_1976,
|
||||
GALLAGHER_2000_NON_ASIAN,
|
||||
GALLAGHER_2000_ASIAN;
|
||||
|
||||
fun displayName(context: Context) = when (this) {
|
||||
OFF -> context.getString(R.string.formula_off)
|
||||
DEURENBERG_1991 -> context.getString(R.string.formula_bf_deurenberg_1991)
|
||||
DEURENBERG_1992 -> context.getString(R.string.formula_bf_deurenberg_1992)
|
||||
EDDY_1976 -> context.getString(R.string.formula_bf_eddy_1976)
|
||||
GALLAGHER_2000_NON_ASIAN -> context.getString(R.string.formula_bf_gallagher_2000_non_asian)
|
||||
GALLAGHER_2000_ASIAN -> context.getString(R.string.formula_bf_gallagher_2000_asian)
|
||||
}
|
||||
|
||||
fun shortDescription(ctx: Context) = when (this) {
|
||||
OFF -> ctx.getString(R.string.formula_desc_off_short)
|
||||
DEURENBERG_1991 -> ctx.getString(R.string.bf_deurenberg_1991_short)
|
||||
DEURENBERG_1992 -> ctx.getString(R.string.bf_deurenberg_1992_short)
|
||||
EDDY_1976 -> ctx.getString(R.string.bf_eddy_1976_short)
|
||||
GALLAGHER_2000_NON_ASIAN -> ctx.getString(R.string.bf_gallagher_2000_non_asian_short)
|
||||
GALLAGHER_2000_ASIAN -> ctx.getString(R.string.bf_gallagher_2000_asian_short)
|
||||
}
|
||||
fun longDescription(ctx: Context) = when (this) {
|
||||
OFF -> ctx.getString(R.string.formula_desc_off_long)
|
||||
DEURENBERG_1991 -> ctx.getString(R.string.bf_deurenberg_1991_long)
|
||||
DEURENBERG_1992 -> ctx.getString(R.string.bf_deurenberg_1992_long)
|
||||
EDDY_1976 -> ctx.getString(R.string.bf_eddy_1976_long)
|
||||
GALLAGHER_2000_NON_ASIAN -> ctx.getString(R.string.bf_gallagher_2000_non_asian_long)
|
||||
GALLAGHER_2000_ASIAN -> ctx.getString(R.string.bf_gallagher_2000_asian_long)
|
||||
}
|
||||
}
|
||||
|
||||
enum class BodyWaterFormulaOption {
|
||||
OFF,
|
||||
BEHNKE_1963,
|
||||
DELWAIDE_CRENIER_1973,
|
||||
HUME_WEYERS_1971,
|
||||
LEE_SONG_KIM_2001;
|
||||
|
||||
fun displayName(context: Context) = when (this) {
|
||||
OFF -> context.getString(R.string.formula_off)
|
||||
BEHNKE_1963 -> context.getString(R.string.formula_bw_behnke_1963)
|
||||
DELWAIDE_CRENIER_1973 -> context.getString(R.string.formula_bw_delwaide_crenier_1973)
|
||||
HUME_WEYERS_1971 -> context.getString(R.string.formula_bw_hume_weyers_1971)
|
||||
LEE_SONG_KIM_2001 -> context.getString(R.string.formula_bw_lee_song_kim_2001)
|
||||
}
|
||||
|
||||
fun shortDescription(ctx: Context) = when (this) {
|
||||
OFF -> ctx.getString(R.string.formula_desc_off_short)
|
||||
BEHNKE_1963 -> ctx.getString(R.string.bw_behnke_1963_short)
|
||||
DELWAIDE_CRENIER_1973 -> ctx.getString(R.string.bw_delwaide_crenier_1973_short)
|
||||
HUME_WEYERS_1971 -> ctx.getString(R.string.bw_hume_weyers_1971_short)
|
||||
LEE_SONG_KIM_2001 -> ctx.getString(R.string.bw_lee_song_kim_2001_short)
|
||||
}
|
||||
fun longDescription(ctx: Context) = when (this) {
|
||||
OFF -> ctx.getString(R.string.formula_desc_off_long)
|
||||
BEHNKE_1963 -> ctx.getString(R.string.bw_behnke_1963_long)
|
||||
DELWAIDE_CRENIER_1973 -> ctx.getString(R.string.bw_delwaide_crenier_1973_long)
|
||||
HUME_WEYERS_1971 -> ctx.getString(R.string.bw_hume_weyers_1971_long)
|
||||
LEE_SONG_KIM_2001 -> ctx.getString(R.string.bw_lee_song_kim_2001_long)
|
||||
}
|
||||
}
|
||||
|
||||
enum class LbmFormulaOption {
|
||||
OFF,
|
||||
BOER_1984,
|
||||
HUME_1966,
|
||||
WEIGHT_MINUS_BODY_FAT;
|
||||
|
||||
fun displayName(context: Context) = when (this) {
|
||||
OFF -> context.getString(R.string.formula_off)
|
||||
BOER_1984 -> context.getString(R.string.formula_lbm_boer_1984)
|
||||
HUME_1966 -> context.getString(R.string.formula_lbm_hume_1966)
|
||||
WEIGHT_MINUS_BODY_FAT -> context.getString(R.string.formula_lbm_weight_minus_body_fat)
|
||||
}
|
||||
|
||||
fun shortDescription(ctx: Context) = when (this) {
|
||||
OFF -> ctx.getString(R.string.formula_desc_off_short)
|
||||
BOER_1984 -> ctx.getString(R.string.lbm_boer_1984_short)
|
||||
HUME_1966 -> ctx.getString(R.string.lbm_hume_1966_short)
|
||||
WEIGHT_MINUS_BODY_FAT -> ctx.getString(R.string.lbm_weight_minus_bf_short)
|
||||
}
|
||||
fun longDescription(ctx: Context) = when (this) {
|
||||
OFF -> ctx.getString(R.string.formula_desc_off_long)
|
||||
BOER_1984 -> ctx.getString(R.string.lbm_boer_1984_long)
|
||||
HUME_1966 -> ctx.getString(R.string.lbm_hume_1966_long)
|
||||
WEIGHT_MINUS_BODY_FAT -> ctx.getString(R.string.lbm_weight_minus_bf_long)
|
||||
}
|
||||
}
|
||||
|
@@ -31,6 +31,9 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.health.openscale.core.data.BackupInterval
|
||||
import com.health.openscale.core.data.BodyFatFormulaOption
|
||||
import com.health.openscale.core.data.BodyWaterFormulaOption
|
||||
import com.health.openscale.core.data.LbmFormulaOption
|
||||
import com.health.openscale.core.data.SmoothingAlgorithm
|
||||
import com.health.openscale.core.utils.LogManager
|
||||
import dagger.Binds
|
||||
@@ -89,6 +92,10 @@ object SettingsPreferenceKeys {
|
||||
val REMINDER_MINUTE = intPreferencesKey("reminder_minute")
|
||||
val REMINDER_DAYS = stringSetPreferencesKey("reminder_days")
|
||||
|
||||
val BODY_FAT_FORMULA_OPTION = stringPreferencesKey("body_fat_formula_option")
|
||||
val BODY_WATER_FORMULA_OPTION = stringPreferencesKey("body_water_formula_option")
|
||||
val LBM_FORMULA_OPTION = stringPreferencesKey("lbm_formula_option")
|
||||
|
||||
// Context strings for screen-specific settings (can be used as prefixes for dynamic keys)
|
||||
const val OVERVIEW_SCREEN_CONTEXT = "overview_screen"
|
||||
const val GRAPH_SCREEN_CONTEXT = "graph_screen"
|
||||
@@ -191,6 +198,15 @@ interface SettingsFacade {
|
||||
val reminderDays: Flow<Set<String>>
|
||||
suspend fun setReminderDays(days: Set<String>)
|
||||
|
||||
val selectedBodyFatFormula: Flow<BodyFatFormulaOption>
|
||||
suspend fun setSelectedBodyFatFormula(option: BodyFatFormulaOption)
|
||||
|
||||
val selectedBodyWaterFormula: Flow<BodyWaterFormulaOption>
|
||||
suspend fun setSelectedBodyWaterFormula(option: BodyWaterFormulaOption)
|
||||
|
||||
val selectedLbmFormula: Flow<LbmFormulaOption>
|
||||
suspend fun setSelectedLbmFormula(option: LbmFormulaOption)
|
||||
|
||||
// Generic Settings Accessors
|
||||
/**
|
||||
* Observes a setting with the given key name and default value.
|
||||
@@ -591,6 +607,54 @@ class SettingsFacadeImpl @Inject constructor(
|
||||
saveSetting(SettingsPreferenceKeys.REMINDER_DAYS.name, safe)
|
||||
}
|
||||
|
||||
override val selectedBodyFatFormula = dataStore.data
|
||||
.catch { exception ->
|
||||
LogManager.e(TAG, "Error reading BODY_FAT_FORMULA_OPTION", exception)
|
||||
if (exception is IOException) emit(emptyPreferences()) else throw exception
|
||||
}
|
||||
.map { prefs ->
|
||||
val raw = prefs[SettingsPreferenceKeys.BODY_FAT_FORMULA_OPTION] ?: BodyFatFormulaOption.OFF.name
|
||||
runCatching { BodyFatFormulaOption.valueOf(raw) }.getOrDefault(BodyFatFormulaOption.OFF)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override suspend fun setSelectedBodyFatFormula(option: BodyFatFormulaOption) {
|
||||
LogManager.d(TAG, "Setting BODY_FAT_FORMULA_OPTION to: ${option.name}")
|
||||
saveSetting(SettingsPreferenceKeys.BODY_FAT_FORMULA_OPTION.name, option.name)
|
||||
}
|
||||
|
||||
override val selectedBodyWaterFormula = dataStore.data
|
||||
.catch { exception ->
|
||||
LogManager.e(TAG, "Error reading BODY_WATER_FORMULA_OPTION", exception)
|
||||
if (exception is IOException) emit(emptyPreferences()) else throw exception
|
||||
}
|
||||
.map { prefs ->
|
||||
val raw = prefs[SettingsPreferenceKeys.BODY_WATER_FORMULA_OPTION] ?: BodyWaterFormulaOption.OFF.name
|
||||
runCatching { BodyWaterFormulaOption.valueOf(raw) }.getOrDefault(BodyWaterFormulaOption.OFF)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override suspend fun setSelectedBodyWaterFormula(option: BodyWaterFormulaOption) {
|
||||
LogManager.d(TAG, "Setting BODY_WATER_FORMULA_OPTION to: ${option.name}")
|
||||
saveSetting(SettingsPreferenceKeys.BODY_WATER_FORMULA_OPTION.name, option.name)
|
||||
}
|
||||
|
||||
override val selectedLbmFormula = dataStore.data
|
||||
.catch { exception ->
|
||||
LogManager.e(TAG, "Error reading LBM_FORMULA_OPTION", exception)
|
||||
if (exception is IOException) emit(emptyPreferences()) else throw exception
|
||||
}
|
||||
.map { prefs ->
|
||||
val raw = prefs[SettingsPreferenceKeys.LBM_FORMULA_OPTION] ?: LbmFormulaOption.OFF.name
|
||||
runCatching { LbmFormulaOption.valueOf(raw) }.getOrDefault(LbmFormulaOption.OFF)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override suspend fun setSelectedLbmFormula(option: LbmFormulaOption) {
|
||||
LogManager.d(TAG, "Setting LBM_FORMULA_OPTION to: ${option.name}")
|
||||
saveSetting(SettingsPreferenceKeys.LBM_FORMULA_OPTION.name, option.name)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T> observeSetting(keyName: String, defaultValue: T): Flow<T> {
|
||||
LogManager.v(TAG, "Observing setting: key='$keyName', type='${defaultValue!!::class.simpleName}'")
|
||||
|
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* openScale
|
||||
* Copyright (C) 2025 olie.xdev <olie.xdeveloper@googlemail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.health.openscale.core.usecase
|
||||
|
||||
import com.health.openscale.core.data.*
|
||||
import com.health.openscale.core.facade.SettingsFacade
|
||||
import com.health.openscale.core.utils.CalculationUtils
|
||||
import com.health.openscale.core.utils.ConverterUtils
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.math.pow
|
||||
|
||||
@Singleton
|
||||
class BodyCompositionUseCases @Inject constructor(
|
||||
private val settings: SettingsFacade,
|
||||
private val userUseCases: UserUseCases,
|
||||
private val measurementQuery: MeasurementQueryUseCases
|
||||
) {
|
||||
/**
|
||||
* Apply selected body-composition formulas for the given measurement.
|
||||
*
|
||||
* Rules:
|
||||
* - If a formula is OFF → leave values untouched (no overwrite, no insert).
|
||||
* - If a formula is ON → overwrite existing value; insert a new value if missing.
|
||||
* - If computed result is null → do not insert; keep existing value as-is.
|
||||
*
|
||||
* No DB I/O here; caller persists via CRUD.
|
||||
*/
|
||||
suspend fun applySelectedFormulasForMeasurement(
|
||||
measurement: Measurement,
|
||||
values: List<MeasurementValue>
|
||||
): List<MeasurementValue> {
|
||||
val bfOpt = settings.selectedBodyFatFormula.first()
|
||||
val bwOpt = settings.selectedBodyWaterFormula.first()
|
||||
val lbmOpt = settings.selectedLbmFormula.first()
|
||||
|
||||
if (bfOpt == BodyFatFormulaOption.OFF &&
|
||||
bwOpt == BodyWaterFormulaOption.OFF &&
|
||||
lbmOpt == LbmFormulaOption.OFF
|
||||
) return values
|
||||
|
||||
val user = userUseCases.observeUserById(measurement.userId).first() ?: return values
|
||||
val types = measurementQuery.getAllMeasurementTypes().first()
|
||||
val byKey = types.associateBy { it.key }
|
||||
|
||||
// Anchor: weight (in kg)
|
||||
val weightType = byKey[MeasurementTypeKey.WEIGHT] ?: return values
|
||||
val weightVal = values.find { it.typeId == weightType.id }?.floatValue ?: return values
|
||||
val weightKg = when (weightType.unit) {
|
||||
UnitType.KG -> weightVal
|
||||
UnitType.LB -> ConverterUtils.toKilogram(weightVal, WeightUnit.LB)
|
||||
UnitType.ST -> ConverterUtils.toKilogram(weightVal, WeightUnit.ST)
|
||||
else -> return values
|
||||
}
|
||||
|
||||
val heightCm = user.heightCm.toDouble().takeIf { it > 0.0 } ?: return values
|
||||
val ageYears = CalculationUtils.ageOn(measurement.timestamp, user.birthDate)
|
||||
val isMale = user.gender == GenderType.MALE
|
||||
|
||||
// BMI (prefer provided, otherwise compute)
|
||||
val bmiType = byKey[MeasurementTypeKey.BMI]
|
||||
val bmiProvided = bmiType?.let { t -> values.find { it.typeId == t.id }?.floatValue }
|
||||
val bmi = bmiProvided?.toDouble() ?: run {
|
||||
val hM = heightCm / 100.0
|
||||
weightKg / (hM.pow(2.0))
|
||||
}
|
||||
|
||||
val out = values.toMutableList()
|
||||
|
||||
// Helper: overwrite existing or insert new value if missing (only if newValue != null)
|
||||
fun upsertFloat(type: MeasurementType, newValue: Float?) {
|
||||
val idx = out.indexOfFirst { it.typeId == type.id }
|
||||
if (idx >= 0) {
|
||||
// Update existing (can set to null)
|
||||
out[idx] = out[idx].copy(floatValue = newValue)
|
||||
} else if (newValue != null) {
|
||||
// Insert new row only when we have a concrete value
|
||||
out.add(
|
||||
MeasurementValue(
|
||||
id = 0,
|
||||
measurementId = measurement.id, // 0 for new; CRUD layer will set final id
|
||||
typeId = type.id,
|
||||
floatValue = newValue
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- BODY FAT (%)
|
||||
byKey[MeasurementTypeKey.BODY_FAT]?.let { type ->
|
||||
if (bfOpt != BodyFatFormulaOption.OFF) {
|
||||
val bf = when (bfOpt) {
|
||||
BodyFatFormulaOption.DEURENBERG_1991 ->
|
||||
(1.2 * bmi) + (0.23 * ageYears) - if (isMale) 16.2 else 5.4
|
||||
BodyFatFormulaOption.DEURENBERG_1992 ->
|
||||
if (ageYears >= 16)
|
||||
(1.2 * bmi) + (0.23 * ageYears) - (10.8 * (if (isMale) 1 else 0)) - 5.4
|
||||
else
|
||||
(1.294 * bmi) + (0.20 * ageYears) - (11.4 * (if (isMale) 1 else 0)) - 8.0
|
||||
BodyFatFormulaOption.EDDY_1976 ->
|
||||
if (isMale) (1.281 * bmi) - 10.13 else (1.48 * bmi) - 7.0
|
||||
BodyFatFormulaOption.GALLAGHER_2000_NON_ASIAN ->
|
||||
64.5 - (848.0 / bmi) + (0.079 * ageYears)
|
||||
BodyFatFormulaOption.GALLAGHER_2000_ASIAN ->
|
||||
if (isMale) 51.9 - (740.0 / bmi) + (0.029 * ageYears)
|
||||
else 64.8 - (752.0 / bmi) + (0.016 * ageYears)
|
||||
BodyFatFormulaOption.OFF -> null
|
||||
}?.coerceAtLeast(0.0)
|
||||
|
||||
val newVal = bf?.toFloat()
|
||||
?.coerceIn(0f, 75f)
|
||||
?.let { CalculationUtils.roundTo(it) }
|
||||
|
||||
upsertFloat(type, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
// --- BODY WATER (stored either as %, or mass depending on type.unit)
|
||||
byKey[MeasurementTypeKey.WATER]?.let { type ->
|
||||
if (bwOpt != BodyWaterFormulaOption.OFF) {
|
||||
val liters = when (bwOpt) {
|
||||
BodyWaterFormulaOption.BEHNKE_1963 ->
|
||||
0.72 * ((if (isMale) 0.204 else 0.18) * (heightCm * heightCm)) / 100.0
|
||||
BodyWaterFormulaOption.DELWAIDE_CRENIER_1973 ->
|
||||
0.72 * (-1.976 + 0.907 * weightKg)
|
||||
BodyWaterFormulaOption.HUME_WEYERS_1971 ->
|
||||
if (isMale) (0.194786 * heightCm) + (0.296785 * weightKg) - 14.012934
|
||||
else (0.34454 * heightCm) + (0.183809 * weightKg) - 35.270121
|
||||
BodyWaterFormulaOption.LEE_SONG_KIM_2001 ->
|
||||
if (isMale) -28.3497 + (0.243057 * heightCm) + (0.366248 * weightKg)
|
||||
else -26.6224 + (0.262513 * heightCm) + (0.232948 * weightKg)
|
||||
BodyWaterFormulaOption.OFF -> null
|
||||
}?.coerceAtLeast(0.0)
|
||||
|
||||
val newVal: Float? = liters?.let {
|
||||
when (type.unit) {
|
||||
UnitType.PERCENT -> {
|
||||
val pct = (it / weightKg) * 100.0
|
||||
CalculationUtils.roundTo(pct.toFloat().coerceIn(0f, 100f))
|
||||
}
|
||||
UnitType.KG -> CalculationUtils.roundTo(it.toFloat().coerceAtLeast(0f))
|
||||
UnitType.LB -> CalculationUtils.roundTo(
|
||||
ConverterUtils.fromKilogram(it.toFloat(), WeightUnit.LB)
|
||||
)
|
||||
UnitType.ST -> CalculationUtils.roundTo(
|
||||
ConverterUtils.fromKilogram(it.toFloat(), WeightUnit.ST)
|
||||
)
|
||||
else -> {
|
||||
// Default to percent if unit is unexpected
|
||||
val pct = (it / weightKg) * 100.0
|
||||
CalculationUtils.roundTo(pct.toFloat().coerceIn(0f, 100f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
upsertFloat(type, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
// --- LBM (mass)
|
||||
byKey[MeasurementTypeKey.LBM]?.let { type ->
|
||||
if (lbmOpt != LbmFormulaOption.OFF) {
|
||||
val lbmKg = when (lbmOpt) {
|
||||
LbmFormulaOption.WEIGHT_MINUS_BODY_FAT -> {
|
||||
val bfType = byKey[MeasurementTypeKey.BODY_FAT]
|
||||
// Prefer freshly computed BF in 'out', fallback to original list
|
||||
val bfPercent = out.firstOrNull { it.typeId == bfType?.id }?.floatValue
|
||||
?: values.firstOrNull { it.typeId == bfType?.id }?.floatValue
|
||||
bfPercent?.let { weightKg * (1f - it / 100f) }?.toDouble()
|
||||
}
|
||||
LbmFormulaOption.BOER_1984 ->
|
||||
if (isMale) (0.4071 * weightKg) + (0.267 * heightCm) - 19.2
|
||||
else (0.252 * weightKg) + (0.473 * heightCm) - 48.3
|
||||
LbmFormulaOption.HUME_1966 ->
|
||||
if (isMale) (0.32810 * weightKg) + (0.33929 * heightCm) - 29.5336
|
||||
else (0.29569 * weightKg) + (0.41813 * heightCm) - 43.2933
|
||||
LbmFormulaOption.OFF -> null
|
||||
}?.coerceAtLeast(0.0)
|
||||
|
||||
val newVal = lbmKg?.let {
|
||||
when (type.unit) {
|
||||
UnitType.KG -> CalculationUtils.roundTo(it.toFloat())
|
||||
UnitType.LB -> CalculationUtils.roundTo(
|
||||
ConverterUtils.fromKilogram(it.toFloat(), WeightUnit.LB)
|
||||
)
|
||||
UnitType.ST -> CalculationUtils.roundTo(
|
||||
ConverterUtils.fromKilogram(it.toFloat(), WeightUnit.ST)
|
||||
)
|
||||
else -> CalculationUtils.roundTo(it.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
upsertFloat(type, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
}
|
@@ -44,10 +44,11 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class MeasurementCrudUseCases @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val databaseRepository: DatabaseRepository,
|
||||
private val settingsFacade: SettingsFacade,
|
||||
private val sync: SyncUseCases,
|
||||
private val settingsFacade: SettingsFacade
|
||||
) {
|
||||
private val bodyComposition: BodyCompositionUseCases,
|
||||
private val databaseRepository: DatabaseRepository
|
||||
) {
|
||||
private var lastVibrateTime = 0L
|
||||
|
||||
/**
|
||||
@@ -67,17 +68,19 @@ class MeasurementCrudUseCases @Inject constructor(
|
||||
measurement: Measurement,
|
||||
values: List<MeasurementValue>
|
||||
): Result<Int> = runCatching {
|
||||
val finalValues : List<MeasurementValue> = bodyComposition.applySelectedFormulasForMeasurement(measurement, values)
|
||||
|
||||
if (measurement.id == 0) {
|
||||
// Insert path
|
||||
val newId = databaseRepository.insertMeasurement(measurement).toInt()
|
||||
|
||||
values.forEach { v ->
|
||||
finalValues.forEach { v ->
|
||||
databaseRepository.insertMeasurementValue(v.copy(measurementId = newId))
|
||||
}
|
||||
|
||||
sync.triggerSyncInsert(measurement, values,"com.health.openscale.sync")
|
||||
sync.triggerSyncInsert(measurement, values,"com.health.openscale.sync.oss")
|
||||
sync.triggerSyncInsert(measurement, values,"com.health.openscale.sync.debug")
|
||||
sync.triggerSyncInsert(measurement, finalValues,"com.health.openscale.sync")
|
||||
sync.triggerSyncInsert(measurement, finalValues,"com.health.openscale.sync.oss")
|
||||
sync.triggerSyncInsert(measurement, finalValues,"com.health.openscale.sync.debug")
|
||||
|
||||
MeasurementWidget.refreshAll(appContext)
|
||||
|
||||
@@ -89,7 +92,7 @@ class MeasurementCrudUseCases @Inject constructor(
|
||||
databaseRepository.updateMeasurement(measurement)
|
||||
|
||||
val existing = databaseRepository.getValuesForMeasurement(measurement.id).first()
|
||||
val newSetIds = values.mapNotNull { if (it.id != 0) it.id else null }.toSet()
|
||||
val newSetIds = finalValues.mapNotNull { if (it.id != 0) it.id else null }.toSet()
|
||||
val existingIds = existing.map { it.id }.toSet()
|
||||
|
||||
// Delete removed values
|
||||
@@ -97,7 +100,7 @@ class MeasurementCrudUseCases @Inject constructor(
|
||||
toDelete.forEach { id -> databaseRepository.deleteMeasurementValueById(id) }
|
||||
|
||||
// Update or insert values
|
||||
values.forEach { v ->
|
||||
finalValues.forEach { v ->
|
||||
val exists = existing.any { it.id == v.id && v.id != 0 }
|
||||
if (exists) {
|
||||
databaseRepository.updateMeasurementValue(v.copy(measurementId = measurement.id))
|
||||
@@ -106,9 +109,9 @@ class MeasurementCrudUseCases @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
sync.triggerSyncUpdate(measurement, values, "com.health.openscale.sync")
|
||||
sync.triggerSyncUpdate(measurement, values,"com.health.openscale.sync.oss")
|
||||
sync.triggerSyncUpdate(measurement, values,"com.health.openscale.sync.debug")
|
||||
sync.triggerSyncUpdate(measurement, finalValues, "com.health.openscale.sync")
|
||||
sync.triggerSyncUpdate(measurement, finalValues,"com.health.openscale.sync.oss")
|
||||
sync.triggerSyncUpdate(measurement, finalValues,"com.health.openscale.sync.debug")
|
||||
|
||||
MeasurementWidget.refreshAll(appContext)
|
||||
|
||||
|
@@ -17,29 +17,27 @@
|
||||
*/
|
||||
package com.health.openscale.ui.screen.settings
|
||||
|
||||
import android.R.attr.enabled
|
||||
import android.R.attr.label
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
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.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.Warning
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
@@ -62,20 +60,22 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.health.openscale.R
|
||||
import com.health.openscale.core.data.BodyFatFormulaOption
|
||||
import com.health.openscale.core.data.BodyWaterFormulaOption
|
||||
import com.health.openscale.core.data.InputFieldType
|
||||
import com.health.openscale.core.data.LbmFormulaOption
|
||||
import com.health.openscale.core.data.MeasurementType
|
||||
import com.health.openscale.core.data.MeasurementTypeIcon
|
||||
import com.health.openscale.core.data.MeasurementTypeKey
|
||||
@@ -85,8 +85,16 @@ import com.health.openscale.ui.shared.SharedViewModel
|
||||
import com.health.openscale.ui.screen.dialog.ColorPickerDialog
|
||||
import com.health.openscale.ui.screen.dialog.IconPickerDialog
|
||||
import com.health.openscale.ui.shared.TopBarAction
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.text.lowercase
|
||||
|
||||
private sealed interface PendingDialog {
|
||||
data class UnitChange(val from: UnitType, val to: UnitType) : PendingDialog
|
||||
data class FormulaOnBodyFat(val option: BodyFatFormulaOption) : PendingDialog
|
||||
data class FormulaOnBodyWater(val option: BodyWaterFormulaOption) : PendingDialog
|
||||
data class FormulaOnLBM(val option: LbmFormulaOption) : PendingDialog
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MeasurementTypeDetailScreen(
|
||||
@@ -96,51 +104,29 @@ fun MeasurementTypeDetailScreen(
|
||||
settingsViewModel: SettingsViewModel,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val measurementTypes by sharedViewModel.measurementTypes.collectAsState()
|
||||
// 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
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
|
||||
val allowedInputTypesForKey = remember(currentMeasurementTypeKey) {
|
||||
currentMeasurementTypeKey.allowedInputType
|
||||
}
|
||||
val allowedUnitsForKey = remember(currentMeasurementTypeKey) { currentMeasurementTypeKey.allowedUnitTypes }
|
||||
val allowedInputTypesForKey = remember(currentMeasurementTypeKey) { currentMeasurementTypeKey.allowedInputType }
|
||||
|
||||
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)
|
||||
}
|
||||
val u = originalExistingType?.unit
|
||||
mutableStateOf(if (u != null && u in allowedUnitsForKey) u else allowedUnitsForKey.firstOrNull() ?: UnitType.NONE)
|
||||
}
|
||||
|
||||
var selectedInputType by remember {
|
||||
val initialInputType = originalExistingType?.inputType
|
||||
if (initialInputType != null && initialInputType in allowedInputTypesForKey) {
|
||||
mutableStateOf(initialInputType)
|
||||
} else {
|
||||
mutableStateOf(allowedInputTypesForKey.firstOrNull() ?: InputFieldType.FLOAT)
|
||||
}
|
||||
val itp = originalExistingType?.inputType
|
||||
mutableStateOf(if (itp != null && itp in allowedInputTypesForKey) itp else allowedInputTypesForKey.firstOrNull() ?: InputFieldType.FLOAT)
|
||||
}
|
||||
var selectedColor by remember { mutableStateOf(originalExistingType?.color ?: 0xFFFFA726.toInt()) }
|
||||
var selectedIcon by remember { mutableStateOf(originalExistingType?.icon ?: MeasurementTypeIcon.IC_DEFAULT) }
|
||||
@@ -148,140 +134,220 @@ fun MeasurementTypeDetailScreen(
|
||||
var isPinned by remember { mutableStateOf(originalExistingType?.isPinned ?: false) }
|
||||
var isOnRightYAxis by remember { mutableStateOf(originalExistingType?.isOnRightYAxis ?: false) }
|
||||
|
||||
val bodyFatFormulaOption by sharedViewModel.selectedBodyFatFormula.collectAsState(BodyFatFormulaOption.OFF)
|
||||
val bodyWaterFormulaOption by sharedViewModel.selectedBodyWaterFormula.collectAsState(BodyWaterFormulaOption.OFF)
|
||||
val lbmFormulaOption by sharedViewModel.selectedLbmFormula.collectAsState(LbmFormulaOption.OFF)
|
||||
|
||||
var bodyFatFormula by remember(bodyFatFormulaOption) { mutableStateOf(bodyFatFormulaOption) }
|
||||
var bodyWaterFormula by remember(bodyWaterFormulaOption) { mutableStateOf(bodyWaterFormulaOption) }
|
||||
var lbmFormula by remember(lbmFormulaOption) { mutableStateOf(lbmFormulaOption) }
|
||||
|
||||
var formulaInfoTitle by remember { mutableStateOf<String?>(null) }
|
||||
var formulaInfoText by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var pendingDialog by remember { mutableStateOf<PendingDialog?>(null) }
|
||||
|
||||
var expandedUnit by remember { mutableStateOf(false) }
|
||||
var expandedInputType by remember { mutableStateOf(false) }
|
||||
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)
|
||||
|
||||
val unitDropdownEnabled by remember(allowedUnitsForKey) {
|
||||
derivedStateOf { allowedUnitsForKey.size > 1 }
|
||||
}
|
||||
val inputTypeDropdownEnabled by remember(allowedInputTypesForKey) {
|
||||
derivedStateOf { allowedInputTypesForKey.size > 1 }
|
||||
}
|
||||
val unitDropdownEnabled by remember(allowedUnitsForKey) { derivedStateOf { allowedUnitsForKey.size > 1 } }
|
||||
val inputTypeDropdownEnabled by remember(allowedInputTypesForKey) { derivedStateOf { allowedInputTypesForKey.size > 1 } }
|
||||
|
||||
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
|
||||
}
|
||||
originalExistingType?.unit?.let { if (it in allowedUnitsForKey && it != selectedUnit) selectedUnit = it }
|
||||
if (allowedUnitsForKey.isNotEmpty() && selectedUnit !in allowedUnitsForKey) selectedUnit = allowedUnitsForKey.first()
|
||||
if (allowedUnitsForKey.isEmpty() && selectedUnit != UnitType.NONE) selectedUnit = UnitType.NONE
|
||||
}
|
||||
|
||||
LaunchedEffect(originalExistingType, allowedInputTypesForKey) {
|
||||
val currentInputTypeInExistingType = originalExistingType?.inputType
|
||||
if (currentInputTypeInExistingType != null && currentInputTypeInExistingType in allowedInputTypesForKey) {
|
||||
if (selectedInputType != currentInputTypeInExistingType) {
|
||||
selectedInputType = currentInputTypeInExistingType
|
||||
}
|
||||
} else if (allowedInputTypesForKey.isNotEmpty() && selectedInputType !in allowedInputTypesForKey) {
|
||||
selectedInputType = allowedInputTypesForKey.first()
|
||||
} else if (allowedInputTypesForKey.isEmpty()) {
|
||||
selectedInputType = InputFieldType.FLOAT
|
||||
}
|
||||
originalExistingType?.inputType?.let { if (it in allowedInputTypesForKey && it != selectedInputType) selectedInputType = it }
|
||||
if (allowedInputTypesForKey.isNotEmpty() && selectedInputType !in allowedInputTypesForKey) selectedInputType = allowedInputTypesForKey.first()
|
||||
if (allowedInputTypesForKey.isEmpty()) selectedInputType = InputFieldType.FLOAT
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
sharedViewModel.setTopBarTitle(if (isEdit) titleEdit else titleAdd)
|
||||
sharedViewModel.setTopBarAction(
|
||||
TopBarAction(icon = Icons.Default.Save, onClick = {
|
||||
if (name.isNotBlank()) {
|
||||
// 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
|
||||
if (name.isBlank()) {
|
||||
Toast.makeText(context, R.string.toast_enter_valid_data, Toast.LENGTH_SHORT).show()
|
||||
return@TopBarAction
|
||||
}
|
||||
|
||||
val currentUpdatedType = MeasurementType(
|
||||
id = originalExistingType?.id ?: 0,
|
||||
name = name,
|
||||
icon = selectedIcon,
|
||||
color = selectedColor,
|
||||
unit = selectedUnit,
|
||||
inputType = selectedInputType,
|
||||
displayOrder = originalExistingType?.displayOrder ?: measurementTypes.size,
|
||||
isEnabled = isEnabled,
|
||||
isPinned = isPinned,
|
||||
key = finalKey, // Use the correct key
|
||||
isDerived = originalExistingType?.isDerived ?: false,
|
||||
isOnRightYAxis = isOnRightYAxis
|
||||
)
|
||||
val finalKey = if (isEdit) originalExistingType?.key ?: MeasurementTypeKey.CUSTOM else MeasurementTypeKey.CUSTOM
|
||||
|
||||
if (isEdit && originalExistingType != null) {
|
||||
val unitChanged = originalExistingType.unit != currentUpdatedType.unit
|
||||
val inputTypesAreFloat =
|
||||
originalExistingType.inputType == InputFieldType.FLOAT && currentUpdatedType.inputType == InputFieldType.FLOAT
|
||||
val derivedForType = when (currentMeasurementTypeKey) {
|
||||
MeasurementTypeKey.BODY_FAT -> bodyFatFormula != BodyFatFormulaOption.OFF
|
||||
MeasurementTypeKey.WATER -> bodyWaterFormula != BodyWaterFormulaOption.OFF
|
||||
MeasurementTypeKey.LBM -> lbmFormula != LbmFormulaOption.OFF
|
||||
else -> originalExistingType?.isDerived ?: false
|
||||
}
|
||||
|
||||
if (unitChanged && inputTypesAreFloat) {
|
||||
pendingUpdatedType = currentUpdatedType
|
||||
showConfirmDialog = true
|
||||
} else {
|
||||
settingsViewModel.updateMeasurementType(currentUpdatedType)
|
||||
navController.popBackStack()
|
||||
}
|
||||
} else {
|
||||
settingsViewModel.addMeasurementType(currentUpdatedType)
|
||||
navController.popBackStack()
|
||||
val updatedType = MeasurementType(
|
||||
id = originalExistingType?.id ?: 0,
|
||||
name = name,
|
||||
icon = selectedIcon,
|
||||
color = selectedColor,
|
||||
unit = selectedUnit,
|
||||
inputType = selectedInputType,
|
||||
displayOrder = originalExistingType?.displayOrder ?: measurementTypes.size,
|
||||
isEnabled = isEnabled,
|
||||
isPinned = isPinned,
|
||||
key = finalKey,
|
||||
isDerived = derivedForType,
|
||||
isOnRightYAxis = isOnRightYAxis
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
when (currentMeasurementTypeKey) {
|
||||
MeasurementTypeKey.BODY_FAT -> if (bodyFatFormula != bodyFatFormulaOption) sharedViewModel.setSelectedBodyFatFormula(bodyFatFormula)
|
||||
MeasurementTypeKey.WATER -> if (bodyWaterFormula != bodyWaterFormulaOption) sharedViewModel.setSelectedBodyWaterFormula(bodyWaterFormula)
|
||||
MeasurementTypeKey.LBM -> if (lbmFormula != lbmFormulaOption) sharedViewModel.setSelectedLbmFormula(lbmFormula)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
val unitChanged = isEdit && (originalExistingType?.unit != selectedUnit)
|
||||
val needsConversion =
|
||||
unitChanged &&
|
||||
(originalExistingType?.inputType == InputFieldType.FLOAT) &&
|
||||
(selectedInputType == InputFieldType.FLOAT)
|
||||
|
||||
if (isEdit && originalExistingType != null) {
|
||||
if (needsConversion) {
|
||||
settingsViewModel.updateMeasurementTypeWithConversion(
|
||||
originalType = originalExistingType,
|
||||
updatedType = updatedType
|
||||
)
|
||||
} else {
|
||||
settingsViewModel.updateMeasurementType(updatedType)
|
||||
}
|
||||
navController.popBackStack()
|
||||
} else {
|
||||
Toast.makeText(context, R.string.toast_enter_valid_data, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
settingsViewModel.addMeasurementType(updatedType)
|
||||
navController.popBackStack()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (showConfirmDialog && originalExistingType != null && pendingUpdatedType != null) {
|
||||
fun requestFormulaChange(newValue: Any) {
|
||||
val turningOn = when (newValue) {
|
||||
is BodyFatFormulaOption -> newValue != BodyFatFormulaOption.OFF
|
||||
is BodyWaterFormulaOption -> newValue != BodyWaterFormulaOption.OFF
|
||||
is LbmFormulaOption -> newValue != LbmFormulaOption.OFF
|
||||
else -> false
|
||||
}
|
||||
if (turningOn) {
|
||||
pendingDialog = when (newValue) {
|
||||
is BodyFatFormulaOption -> PendingDialog.FormulaOnBodyFat(newValue)
|
||||
is BodyWaterFormulaOption -> PendingDialog.FormulaOnBodyWater(newValue)
|
||||
is LbmFormulaOption -> PendingDialog.FormulaOnLBM(newValue)
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
when (newValue) {
|
||||
is BodyFatFormulaOption -> { bodyFatFormula = newValue; }
|
||||
is BodyWaterFormulaOption -> { bodyWaterFormula = newValue; }
|
||||
is LbmFormulaOption -> { lbmFormula = newValue; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pendingDialog?.let { dlg ->
|
||||
val titleId = when (dlg) {
|
||||
is PendingDialog.UnitChange -> R.string.measurement_type_dialog_confirm_unit_change_title
|
||||
is PendingDialog.FormulaOnBodyFat,
|
||||
is PendingDialog.FormulaOnBodyWater,
|
||||
is PendingDialog.FormulaOnLBM -> R.string.formula_warning_title
|
||||
}
|
||||
|
||||
val message = when (dlg) {
|
||||
is PendingDialog.UnitChange -> {
|
||||
val typeName = (originalExistingType?.getDisplayName(context) ?: name)
|
||||
val fromName = selectedUnit.displayName.lowercase().replaceFirstChar { it.uppercase() }
|
||||
val toName = dlg.to.displayName.lowercase().replaceFirstChar { it.uppercase() }
|
||||
context.getString(
|
||||
R.string.measurement_type_dialog_confirm_unit_change_message,
|
||||
typeName, fromName, toName
|
||||
)
|
||||
}
|
||||
is PendingDialog.FormulaOnBodyFat,
|
||||
is PendingDialog.FormulaOnBodyWater,
|
||||
is PendingDialog.FormulaOnLBM -> {
|
||||
context.getString(R.string.formula_warning_message)
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { showConfirmDialog = false },
|
||||
title = { Text(stringResource(R.string.measurement_type_dialog_confirm_unit_change_title)) },
|
||||
onDismissRequest = { pendingDialog = null },
|
||||
icon= {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(titleId),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.measurement_type_dialog_confirm_unit_change_message,
|
||||
originalExistingType.getDisplayName(context),
|
||||
originalExistingType.unit.displayName.lowercase().replaceFirstChar { it.uppercase() },
|
||||
pendingUpdatedType!!.unit.displayName.lowercase().replaceFirstChar { it.uppercase() }
|
||||
)
|
||||
text = message,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
settingsViewModel.updateMeasurementTypeWithConversion(
|
||||
originalType = originalExistingType,
|
||||
updatedType = pendingUpdatedType!!
|
||||
TextButton(
|
||||
onClick = {
|
||||
when (dlg) {
|
||||
is PendingDialog.UnitChange -> {
|
||||
selectedUnit = dlg.to
|
||||
}
|
||||
is PendingDialog.FormulaOnBodyFat -> {
|
||||
bodyFatFormula = dlg.option
|
||||
}
|
||||
is PendingDialog.FormulaOnBodyWater -> {
|
||||
bodyWaterFormula = dlg.option
|
||||
}
|
||||
is PendingDialog.FormulaOnLBM -> {
|
||||
lbmFormula = dlg.option
|
||||
}
|
||||
}
|
||||
pendingDialog = null
|
||||
},
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
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 = { pendingDialog = null }) {
|
||||
Text(stringResource(R.string.cancel_button))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.padding(16.dp).fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_enabled)) {
|
||||
Switch(
|
||||
checked = isEnabled,
|
||||
onCheckedChange = { isEnabled = it }
|
||||
)
|
||||
Switch(checked = isEnabled, onCheckedChange = { isEnabled = it })
|
||||
}
|
||||
|
||||
if (!isEdit || (originalExistingType?.key == MeasurementTypeKey.CUSTOM)) {
|
||||
@@ -295,20 +361,13 @@ fun MeasurementTypeDetailScreen(
|
||||
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
onValueChange = {}, // Read-only
|
||||
onValueChange = {},
|
||||
label = { Text(stringResource(R.string.measurement_type_label_color)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { showColorPicker = true },
|
||||
modifier = Modifier.fillMaxWidth().clickable { showColorPicker = true },
|
||||
readOnly = true,
|
||||
enabled = false, // To make it look like a display field that's clickable
|
||||
enabled = false,
|
||||
trailingIcon = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(selectedColor))
|
||||
)
|
||||
Box(modifier = Modifier.size(24.dp).clip(CircleShape).background(Color(selectedColor)))
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
disabledTextColor = LocalContentColor.current,
|
||||
@@ -321,18 +380,12 @@ fun MeasurementTypeDetailScreen(
|
||||
|
||||
OutlinedTextField(
|
||||
value = "",
|
||||
onValueChange = {}, // Read-only
|
||||
onValueChange = {},
|
||||
label = { Text(stringResource(R.string.measurement_type_label_icon)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { showIconPicker = true },
|
||||
modifier = Modifier.fillMaxWidth().clickable { showIconPicker = true },
|
||||
readOnly = true,
|
||||
enabled = false, // To make it look like a display field
|
||||
trailingIcon = {
|
||||
MeasurementIcon(
|
||||
icon = selectedIcon,
|
||||
)
|
||||
},
|
||||
enabled = false,
|
||||
trailingIcon = { MeasurementIcon(icon = selectedIcon) },
|
||||
colors = TextFieldDefaults.colors(
|
||||
disabledTextColor = LocalContentColor.current,
|
||||
disabledIndicatorColor = MaterialTheme.colorScheme.outline,
|
||||
@@ -342,24 +395,64 @@ fun MeasurementTypeDetailScreen(
|
||||
)
|
||||
)
|
||||
|
||||
// Formula pickers use local state
|
||||
when (currentMeasurementTypeKey) {
|
||||
MeasurementTypeKey.BODY_FAT -> {
|
||||
FormulaPickerRow(
|
||||
label = stringResource(R.string.formula_label_body_fat),
|
||||
currentText = bodyFatFormula.displayName(context),
|
||||
options = BodyFatFormulaOption.entries,
|
||||
optionLabel = { it.displayName(context) },
|
||||
optionSubtitle = { it.shortDescription(context) },
|
||||
onInfo = { opt ->
|
||||
formulaInfoTitle = opt.displayName(context)
|
||||
formulaInfoText = opt.longDescription(context)
|
||||
},
|
||||
onSelect = { requestFormulaChange( it) }
|
||||
)
|
||||
}
|
||||
MeasurementTypeKey.WATER -> {
|
||||
FormulaPickerRow(
|
||||
label = stringResource(R.string.formula_label_body_water),
|
||||
currentText = bodyWaterFormula.displayName(context),
|
||||
options = BodyWaterFormulaOption.entries,
|
||||
optionLabel = { it.displayName(context) },
|
||||
optionSubtitle = { it.shortDescription(context) },
|
||||
onInfo = { opt ->
|
||||
formulaInfoTitle = opt.displayName(context)
|
||||
formulaInfoText = opt.longDescription(context)
|
||||
},
|
||||
onSelect = { requestFormulaChange(it) }
|
||||
)
|
||||
}
|
||||
MeasurementTypeKey.LBM -> {
|
||||
FormulaPickerRow(
|
||||
label = stringResource(R.string.formula_label_lbm),
|
||||
currentText = lbmFormula.displayName(context),
|
||||
options = LbmFormulaOption.entries,
|
||||
optionLabel = { it.displayName(context) },
|
||||
optionSubtitle = { it.shortDescription(context) },
|
||||
onInfo = { opt ->
|
||||
formulaInfoTitle = opt.displayName(context)
|
||||
formulaInfoText = opt.longDescription(context)
|
||||
},
|
||||
onSelect = { requestFormulaChange(it) }
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
if (unitDropdownEnabled) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expandedUnit && unitDropdownEnabled,
|
||||
onExpandedChange = {
|
||||
if (unitDropdownEnabled) expandedUnit = !expandedUnit
|
||||
},
|
||||
onExpandedChange = { if (unitDropdownEnabled) expandedUnit = !expandedUnit },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedSettingRow(
|
||||
label = stringResource(R.string.measurement_type_label_unit),
|
||||
surfaceModifier = Modifier
|
||||
.menuAnchor(
|
||||
type = MenuAnchorType.PrimaryNotEditable,
|
||||
enabled = unitDropdownEnabled
|
||||
)
|
||||
.clickable(enabled = unitDropdownEnabled) {
|
||||
if (unitDropdownEnabled) expandedUnit = true
|
||||
},
|
||||
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable, enabled = unitDropdownEnabled)
|
||||
.clickable(enabled = unitDropdownEnabled) { if (unitDropdownEnabled) expandedUnit = true },
|
||||
controlContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
@@ -367,13 +460,10 @@ fun MeasurementTypeDetailScreen(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = if (unitDropdownEnabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
if (unitDropdownEnabled) {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedUnit)
|
||||
}
|
||||
if (unitDropdownEnabled) ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedUnit)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (unitDropdownEnabled) {
|
||||
ExposedDropdownMenu(
|
||||
expanded = expandedUnit,
|
||||
@@ -383,19 +473,24 @@ fun MeasurementTypeDetailScreen(
|
||||
allowedUnitsForKey.forEach { unit ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Text(
|
||||
text = unit.displayName,
|
||||
modifier = Modifier.padding(end = 32.dp)
|
||||
)
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
|
||||
Text(unit.displayName, modifier = Modifier.padding(end = 32.dp))
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
selectedUnit = unit
|
||||
expandedUnit = false
|
||||
if (unit == selectedUnit) return@DropdownMenuItem
|
||||
|
||||
val needsConfirm =
|
||||
isEdit &&
|
||||
(originalExistingType?.inputType == InputFieldType.FLOAT) &&
|
||||
(selectedInputType == InputFieldType.FLOAT)
|
||||
|
||||
if (needsConfirm) {
|
||||
pendingDialog = PendingDialog.UnitChange(from = selectedUnit, to = unit)
|
||||
} else {
|
||||
selectedUnit = unit
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -404,36 +499,21 @@ fun MeasurementTypeDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// InputFieldType Dropdown
|
||||
if (inputTypeDropdownEnabled) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expandedInputType,
|
||||
onExpandedChange = { expandedInputType = !expandedInputType }
|
||||
) {
|
||||
ExposedDropdownMenuBox(expanded = expandedInputType, onExpandedChange = { expandedInputType = !expandedInputType }) {
|
||||
OutlinedTextField(
|
||||
readOnly = true,
|
||||
value = selectedInputType.name.lowercase().replaceFirstChar { it.uppercase() },
|
||||
onValueChange = {},
|
||||
label = { Text(stringResource(R.string.measurement_type_label_input_type)) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedInputType) },
|
||||
modifier = Modifier
|
||||
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable, enabled = true)
|
||||
.fillMaxWidth()
|
||||
modifier = Modifier.menuAnchor(type = MenuAnchorType.PrimaryNotEditable, enabled = true).fillMaxWidth()
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expandedInputType,
|
||||
onDismissRequest = { expandedInputType = false }
|
||||
) {
|
||||
ExposedDropdownMenu(expanded = expandedInputType, onDismissRequest = { expandedInputType = false }) {
|
||||
allowedInputTypesForKey.forEach { type ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
type.name.lowercase().replaceFirstChar { it.uppercase() })
|
||||
},
|
||||
onClick = {
|
||||
selectedInputType = type
|
||||
expandedInputType = false
|
||||
}
|
||||
text = { Text(type.name.lowercase().replaceFirstChar { it.uppercase() }) },
|
||||
onClick = { selectedInputType = type; expandedInputType = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -441,17 +521,10 @@ fun MeasurementTypeDetailScreen(
|
||||
}
|
||||
|
||||
OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_pinned)) {
|
||||
Switch(
|
||||
checked = isPinned,
|
||||
onCheckedChange = { isPinned = it }
|
||||
)
|
||||
Switch(checked = isPinned, onCheckedChange = { isPinned = it })
|
||||
}
|
||||
|
||||
OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_on_right_y_axis)) {
|
||||
Switch(
|
||||
checked = isOnRightYAxis,
|
||||
onCheckedChange = { isOnRightYAxis = it }
|
||||
)
|
||||
Switch(checked = isOnRightYAxis, onCheckedChange = { isOnRightYAxis = it })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,19 +535,48 @@ fun MeasurementTypeDetailScreen(
|
||||
onDismiss = { showColorPicker = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showIconPicker) {
|
||||
IconPickerDialog(
|
||||
iconBackgroundColor = Color(selectedColor),
|
||||
onIconSelected = {
|
||||
selectedIcon = it
|
||||
showIconPicker = false
|
||||
},
|
||||
onIconSelected = { selectedIcon = it; showIconPicker = false },
|
||||
onDismiss = { showIconPicker = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (formulaInfoTitle != null && formulaInfoText != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { formulaInfoTitle = null; formulaInfoText = null },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = formulaInfoTitle!!,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = formulaInfoText!!,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { formulaInfoTitle = null; formulaInfoText = null }) {
|
||||
Text(stringResource(R.string.dialog_ok))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun OutlinedSettingRow(
|
||||
label: String,
|
||||
@@ -507,3 +609,93 @@ private fun OutlinedSettingRow(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun <T> FormulaPickerRow(
|
||||
label: String,
|
||||
currentText: String,
|
||||
options: List<T>,
|
||||
optionLabel: (T) -> String,
|
||||
optionSubtitle: ((T) -> String)? = null,
|
||||
onInfo: ((T) -> Unit)? = null,
|
||||
onSelect: (T) -> Unit
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedSettingRow(
|
||||
label = label,
|
||||
surfaceModifier = Modifier
|
||||
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable, enabled = true)
|
||||
.clickable { expanded = true },
|
||||
controlContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = currentText,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 2,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.widthIn(min = 24.dp, max = 240.dp)
|
||||
.padding(end = 12.dp),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.End
|
||||
)
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier.exposedDropdownSize(matchTextFieldWidth = true)
|
||||
) {
|
||||
options.forEach { opt ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Text(
|
||||
optionLabel(opt),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.End
|
||||
)
|
||||
optionSubtitle?.let { sub ->
|
||||
Text(
|
||||
sub(opt),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 2,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.End
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingIcon = if (onInfo != null) {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.clickable { onInfo(opt) },
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
onClick = {
|
||||
expanded = false
|
||||
onSelect(opt)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -180,6 +180,52 @@
|
||||
<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>
|
||||
|
||||
<string name="formula_label_body_fat">Körperfett-Formel</string>
|
||||
<string name="formula_label_body_water">Körperwasser-Formel</string>
|
||||
<string name="formula_label_lbm">Magermasse-Formel</string>
|
||||
<string name="formula_warning_title">Formelberechnung</string>
|
||||
<string name="formula_warning_message">Diese Messgröße wird schreibgeschützt und automatisch berechnet. Sie aktualisiert sich bei Änderungen der Basiswerte. Gilt für neue oder bearbeitete Messungen; bestehende bleiben unverändert.</string>
|
||||
<string name="formula_off">Aus</string>
|
||||
<string name="formula_bf_deurenberg_1991">Deurenberg (1991)</string>
|
||||
<string name="formula_bf_deurenberg_1992">Deurenberg (1992)</string>
|
||||
<string name="formula_bf_eddy_1976">Eddy et al. (1976)</string>
|
||||
<string name="formula_bf_gallagher_2000_non_asian">Gallagher (2000) – Nicht-asiatisch</string>
|
||||
<string name="formula_bf_gallagher_2000_asian">Gallagher (2000) – Asiatisch</string>
|
||||
<string name="formula_bw_behnke_1963">Behnke et al. (1963)</string>
|
||||
<string name="formula_bw_delwaide_crenier_1973">Delwaide & Crenier (1973)</string>
|
||||
<string name="formula_bw_hume_weyers_1971">Hume & Weyers (1971)</string>
|
||||
<string name="formula_bw_lee_song_kim_2001">Lee, Song & Kim (2001)</string>
|
||||
<string name="formula_lbm_boer_1984">Boer (1984)</string>
|
||||
<string name="formula_lbm_hume_1966">Hume (1966)</string>
|
||||
<string name="formula_lbm_weight_minus_body_fat">Gewicht − Körperfett</string>
|
||||
|
||||
<string name="formula_desc_off_short">Keine automatische Berechnung</string>
|
||||
<string name="formula_desc_off_long">Diese Messgröße wird nicht automatisch berechnet. Werte können manuell eingegeben werden.</string>
|
||||
<string name="bf_deurenberg_1991_short">Klassische Regression (Erw.)</string>
|
||||
<string name="bf_deurenberg_1991_long">Weit verbreitete Bevölkerungsformel von 1991. Für die Allgemeinbevölkerung gedacht; bei sehr schlanken oder sehr muskulösen Personen ggf. ungenauer.</string>
|
||||
<string name="bf_deurenberg_1992_short">Angepasst für Jugend & Erw.</string>
|
||||
<string name="bf_deurenberg_1992_long">Variante mit Anpassung für jüngere Nutzer. Allgemeine Schätzung; sportliche oder atypische Körperbauten können abweichen.</string>
|
||||
<string name="bf_eddy_1976_short">Frühes, geschlechtsspez. Modell</string>
|
||||
<string name="bf_eddy_1976_long">Ältere Regression mit getrennten Koeffizienten für Männer und Frauen. Einfach, aber nicht für Spezialpopulationen optimiert.</string>
|
||||
<string name="bf_gallagher_2000_non_asian_short">Gallagher (Nicht-asiatisch)</string>
|
||||
<string name="bf_gallagher_2000_non_asian_long">Referenzmodell von 2000 für nicht-asiatische Populationen. Nützliche Basis; Genauigkeit variiert je nach Körpertyp.</string>
|
||||
<string name="bf_gallagher_2000_asian_short">Gallagher (Asiatisch)</string>
|
||||
<string name="bf_gallagher_2000_asian_long">Variante für asiatische Populationen. Verwenden, wenn dies besser zum Nutzerhintergrund passt.</string>
|
||||
<string name="bw_behnke_1963_short">Behnke TBW-Schätzung</string>
|
||||
<string name="bw_behnke_1963_long">Klassische Schätzung des Gesamtkörperwassers. Einfach, jedoch nicht für Athleten oder Randfälle individualisiert.</string>
|
||||
<string name="bw_delwaide_crenier_1973_short">Delwaide & Crenier</string>
|
||||
<string name="bw_delwaide_crenier_1973_long">TBW-Formel mit Fokus auf Körpermasse. Gute allgemeine Schätzung; Präzision variiert mit dem Körpertyp.</string>
|
||||
<string name="bw_hume_weyers_1971_short">Hume–Weyers TBW</string>
|
||||
<string name="bw_hume_weyers_1971_long">Weit verbreitetes TBW-Modell mit Körpergröße und Gewicht, geschlechtsspezifisch.</string>
|
||||
<string name="bw_lee_song_kim_2001_short">Lee–Song–Kim</string>
|
||||
<string name="bw_lee_song_kim_2001_long">Neuere TBW-Schätzung, kalibriert an koreanischen Kollektiven. Sinnvoll, wenn dies zum Nutzer passt.</string>
|
||||
<string name="lbm_boer_1984_short">Boer LBM (klinisch)</string>
|
||||
<string name="lbm_boer_1984_long">Fettfreie Masse, häufig in der Klinik genutzt. Geschlechtsspezifische Regression aus Größe und Gewicht.</string>
|
||||
<string name="lbm_hume_1966_short">Hume LBM</string>
|
||||
<string name="lbm_hume_1966_long">Frühes LBM-Modell. Einfach und verbreitet; für sehr sportliche Körpertypen weniger angepasst.</string>
|
||||
<string name="lbm_weight_minus_bf_short">Gewicht − Körperfett</string>
|
||||
<string name="lbm_weight_minus_bf_long">Berechnet LBM als Gesamtgewicht minus Fettmasse aus der gewählten Körperfettformel. Genauigkeit hängt von der BF-Schätzung ab.</string>
|
||||
|
||||
<!-- Bluetooth -->
|
||||
<string name="info_bluetooth_connection_error_scale_offline">Verbindung zur Waage fehlgeschlagen, bitte stellen Sie sicher, dass sie eingeschaltet ist.</string>
|
||||
<string name="info_step_on_scale">Bitte barfuß auf die Waage steigen</string>
|
||||
|
@@ -182,6 +182,53 @@
|
||||
<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>
|
||||
|
||||
<string name="formula_label_body_fat">Body Fat Formula</string>
|
||||
<string name="formula_label_body_water">Body Water Formula</string>
|
||||
<string name="formula_label_lbm">Lean Body Mass Formula</string>
|
||||
<string name="formula_warning_title">Formula Calculation</string>
|
||||
<string name="formula_warning_message">This metric becomes read-only and auto-calculated. It updates when base values change. Applies to new or edited measurements; existing ones stay unchanged.</string>
|
||||
|
||||
<string name="formula_off">Off</string>
|
||||
<string name="formula_bf_deurenberg_1991">Deurenberg (1991)</string>
|
||||
<string name="formula_bf_deurenberg_1992">Deurenberg (1992)</string>
|
||||
<string name="formula_bf_eddy_1976">Eddy et al. (1976)</string>
|
||||
<string name="formula_bf_gallagher_2000_non_asian">Gallagher (2000) – Non-Asian</string>
|
||||
<string name="formula_bf_gallagher_2000_asian">Gallagher (2000) – Asian</string>
|
||||
<string name="formula_bw_behnke_1963">Behnke et al. (1963)</string>
|
||||
<string name="formula_bw_delwaide_crenier_1973">Delwaide & Crenier (1973)</string>
|
||||
<string name="formula_bw_hume_weyers_1971">Hume & Weyers (1971)</string>
|
||||
<string name="formula_bw_lee_song_kim_2001">Lee, Song & Kim (2001)</string>
|
||||
<string name="formula_lbm_boer_1984">Boer (1984)</string>
|
||||
<string name="formula_lbm_hume_1966">Hume (1966)</string>
|
||||
<string name="formula_lbm_weight_minus_body_fat">Weight − Body Fat</string>
|
||||
|
||||
<string name="formula_desc_off_short">No automatic calculation</string>
|
||||
<string name="formula_desc_off_long">This metric is not auto-calculated. You can enter values manually.</string>
|
||||
<string name="bf_deurenberg_1991_short">Classic regression for adults</string>
|
||||
<string name="bf_deurenberg_1991_long">A widely used population formula from 1991. Suited for general adult populations; may be less accurate for very lean or very muscular individuals.</string>
|
||||
<string name="bf_deurenberg_1992_short">Adjusted for teens & adults</string>
|
||||
<string name="bf_deurenberg_1992_long">Variant that adapts for younger users. A general-purpose estimate; athletic or atypical physiques can deviate.</string>
|
||||
<string name="bf_eddy_1976_short">Early sex-specific model</string>
|
||||
<string name="bf_eddy_1976_long">Older regression with separate coefficients for men and women. Simple and fast but not tailored to special populations.</string>
|
||||
<string name="bf_gallagher_2000_non_asian_short">Gallagher (non-Asian)</string>
|
||||
<string name="bf_gallagher_2000_non_asian_long">Reference model from 2000 for non-Asian populations. Useful baseline; accuracy varies across body types.</string>
|
||||
<string name="bf_gallagher_2000_asian_short">Gallagher (Asian)</string>
|
||||
<string name="bf_gallagher_2000_asian_long">Companion model tuned for Asian populations. Use when this better matches the user’s background.</string>
|
||||
<string name="bw_behnke_1963_short">Behnke TBW estimate</string>
|
||||
<string name="bw_behnke_1963_long">Classic total body water estimate. A simple approach; not individualized for athletes or edge cases.</string>
|
||||
<string name="bw_delwaide_crenier_1973_short">Delwaide & Crenier</string>
|
||||
<string name="bw_delwaide_crenier_1973_long">TBW formula emphasizing body mass. Reasonable general estimate; precision varies with physique.</string>
|
||||
<string name="bw_hume_weyers_1971_short">Hume–Weyers TBW</string>
|
||||
<string name="bw_hume_weyers_1971_long">Common TBW model using height and weight, with sex-specific coefficients.</string>
|
||||
<string name="bw_lee_song_kim_2001_short">Lee–Song–Kim</string>
|
||||
<string name="bw_lee_song_kim_2001_long">Later TBW estimate calibrated on Korean cohorts. Consider if that population match applies.</string>
|
||||
<string name="lbm_boer_1984_short">Boer LBM (clinical)</string>
|
||||
<string name="lbm_boer_1984_long">Lean body mass estimate popular in clinical settings. Sex-specific regression from height and weight.</string>
|
||||
<string name="lbm_hume_1966_short">Hume LBM</string>
|
||||
<string name="lbm_hume_1966_long">Early LBM model. Simple and widely used; not tailored to very athletic builds.</string>
|
||||
<string name="lbm_weight_minus_bf_short">Weight − Body Fat</string>
|
||||
<string name="lbm_weight_minus_bf_long">Computes LBM as total weight minus fat mass derived from the chosen body-fat formula. Accuracy depends on the BF estimate.</string>
|
||||
|
||||
<!-- Bluetooth -->
|
||||
<string name="info_bluetooth_connection_error_scale_offline">Could not connect to scale, please ensure it is on.</string>
|
||||
<string name="info_step_on_scale">Please step barefoot on the scale</string>
|
||||
|
Reference in New Issue
Block a user