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,
|
DISCONNECTING,
|
||||||
/** A connection attempt failed or connection broke unexpectedly. */
|
/** A connection attempt failed or connection broke unexpectedly. */
|
||||||
FAILED
|
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.core.stringSetPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.health.openscale.core.data.BackupInterval
|
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.data.SmoothingAlgorithm
|
||||||
import com.health.openscale.core.utils.LogManager
|
import com.health.openscale.core.utils.LogManager
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
@@ -89,6 +92,10 @@ object SettingsPreferenceKeys {
|
|||||||
val REMINDER_MINUTE = intPreferencesKey("reminder_minute")
|
val REMINDER_MINUTE = intPreferencesKey("reminder_minute")
|
||||||
val REMINDER_DAYS = stringSetPreferencesKey("reminder_days")
|
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)
|
// Context strings for screen-specific settings (can be used as prefixes for dynamic keys)
|
||||||
const val OVERVIEW_SCREEN_CONTEXT = "overview_screen"
|
const val OVERVIEW_SCREEN_CONTEXT = "overview_screen"
|
||||||
const val GRAPH_SCREEN_CONTEXT = "graph_screen"
|
const val GRAPH_SCREEN_CONTEXT = "graph_screen"
|
||||||
@@ -191,6 +198,15 @@ interface SettingsFacade {
|
|||||||
val reminderDays: Flow<Set<String>>
|
val reminderDays: Flow<Set<String>>
|
||||||
suspend fun setReminderDays(days: 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
|
// Generic Settings Accessors
|
||||||
/**
|
/**
|
||||||
* Observes a setting with the given key name and default value.
|
* 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)
|
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")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T> observeSetting(keyName: String, defaultValue: T): Flow<T> {
|
override fun <T> observeSetting(keyName: String, defaultValue: T): Flow<T> {
|
||||||
LogManager.v(TAG, "Observing setting: key='$keyName', type='${defaultValue!!::class.simpleName}'")
|
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
|
@Singleton
|
||||||
class MeasurementCrudUseCases @Inject constructor(
|
class MeasurementCrudUseCases @Inject constructor(
|
||||||
@ApplicationContext private val appContext: Context,
|
@ApplicationContext private val appContext: Context,
|
||||||
private val databaseRepository: DatabaseRepository,
|
private val settingsFacade: SettingsFacade,
|
||||||
private val sync: SyncUseCases,
|
private val sync: SyncUseCases,
|
||||||
private val settingsFacade: SettingsFacade
|
private val bodyComposition: BodyCompositionUseCases,
|
||||||
) {
|
private val databaseRepository: DatabaseRepository
|
||||||
|
) {
|
||||||
private var lastVibrateTime = 0L
|
private var lastVibrateTime = 0L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,17 +68,19 @@ class MeasurementCrudUseCases @Inject constructor(
|
|||||||
measurement: Measurement,
|
measurement: Measurement,
|
||||||
values: List<MeasurementValue>
|
values: List<MeasurementValue>
|
||||||
): Result<Int> = runCatching {
|
): Result<Int> = runCatching {
|
||||||
|
val finalValues : List<MeasurementValue> = bodyComposition.applySelectedFormulasForMeasurement(measurement, values)
|
||||||
|
|
||||||
if (measurement.id == 0) {
|
if (measurement.id == 0) {
|
||||||
// Insert path
|
// Insert path
|
||||||
val newId = databaseRepository.insertMeasurement(measurement).toInt()
|
val newId = databaseRepository.insertMeasurement(measurement).toInt()
|
||||||
|
|
||||||
values.forEach { v ->
|
finalValues.forEach { v ->
|
||||||
databaseRepository.insertMeasurementValue(v.copy(measurementId = newId))
|
databaseRepository.insertMeasurementValue(v.copy(measurementId = newId))
|
||||||
}
|
}
|
||||||
|
|
||||||
sync.triggerSyncInsert(measurement, values,"com.health.openscale.sync")
|
sync.triggerSyncInsert(measurement, finalValues,"com.health.openscale.sync")
|
||||||
sync.triggerSyncInsert(measurement, values,"com.health.openscale.sync.oss")
|
sync.triggerSyncInsert(measurement, finalValues,"com.health.openscale.sync.oss")
|
||||||
sync.triggerSyncInsert(measurement, values,"com.health.openscale.sync.debug")
|
sync.triggerSyncInsert(measurement, finalValues,"com.health.openscale.sync.debug")
|
||||||
|
|
||||||
MeasurementWidget.refreshAll(appContext)
|
MeasurementWidget.refreshAll(appContext)
|
||||||
|
|
||||||
@@ -89,7 +92,7 @@ class MeasurementCrudUseCases @Inject constructor(
|
|||||||
databaseRepository.updateMeasurement(measurement)
|
databaseRepository.updateMeasurement(measurement)
|
||||||
|
|
||||||
val existing = databaseRepository.getValuesForMeasurement(measurement.id).first()
|
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()
|
val existingIds = existing.map { it.id }.toSet()
|
||||||
|
|
||||||
// Delete removed values
|
// Delete removed values
|
||||||
@@ -97,7 +100,7 @@ class MeasurementCrudUseCases @Inject constructor(
|
|||||||
toDelete.forEach { id -> databaseRepository.deleteMeasurementValueById(id) }
|
toDelete.forEach { id -> databaseRepository.deleteMeasurementValueById(id) }
|
||||||
|
|
||||||
// Update or insert values
|
// Update or insert values
|
||||||
values.forEach { v ->
|
finalValues.forEach { v ->
|
||||||
val exists = existing.any { it.id == v.id && v.id != 0 }
|
val exists = existing.any { it.id == v.id && v.id != 0 }
|
||||||
if (exists) {
|
if (exists) {
|
||||||
databaseRepository.updateMeasurementValue(v.copy(measurementId = measurement.id))
|
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, finalValues, "com.health.openscale.sync")
|
||||||
sync.triggerSyncUpdate(measurement, values,"com.health.openscale.sync.oss")
|
sync.triggerSyncUpdate(measurement, finalValues,"com.health.openscale.sync.oss")
|
||||||
sync.triggerSyncUpdate(measurement, values,"com.health.openscale.sync.debug")
|
sync.triggerSyncUpdate(measurement, finalValues,"com.health.openscale.sync.debug")
|
||||||
|
|
||||||
MeasurementWidget.refreshAll(appContext)
|
MeasurementWidget.refreshAll(appContext)
|
||||||
|
|
||||||
|
@@ -17,29 +17,27 @@
|
|||||||
*/
|
*/
|
||||||
package com.health.openscale.ui.screen.settings
|
package com.health.openscale.ui.screen.settings
|
||||||
|
|
||||||
import android.R.attr.enabled
|
|
||||||
import android.R.attr.label
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
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.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
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.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.AlertDialog
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
@@ -62,20 +60,22 @@ import androidx.compose.runtime.derivedStateOf
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.health.openscale.R
|
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.InputFieldType
|
||||||
|
import com.health.openscale.core.data.LbmFormulaOption
|
||||||
import com.health.openscale.core.data.MeasurementType
|
import com.health.openscale.core.data.MeasurementType
|
||||||
import com.health.openscale.core.data.MeasurementTypeIcon
|
import com.health.openscale.core.data.MeasurementTypeIcon
|
||||||
import com.health.openscale.core.data.MeasurementTypeKey
|
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.ColorPickerDialog
|
||||||
import com.health.openscale.ui.screen.dialog.IconPickerDialog
|
import com.health.openscale.ui.screen.dialog.IconPickerDialog
|
||||||
import com.health.openscale.ui.shared.TopBarAction
|
import com.health.openscale.ui.shared.TopBarAction
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.text.lowercase
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MeasurementTypeDetailScreen(
|
fun MeasurementTypeDetailScreen(
|
||||||
@@ -96,51 +104,29 @@ fun MeasurementTypeDetailScreen(
|
|||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
val measurementTypes by sharedViewModel.measurementTypes.collectAsState()
|
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) {
|
val originalExistingType = remember(measurementTypes, typeId) {
|
||||||
measurementTypes.find { it.id == typeId }
|
measurementTypes.find { it.id == typeId }
|
||||||
}
|
}
|
||||||
val isEdit = typeId != -1
|
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) {
|
val currentMeasurementTypeKey = remember(originalExistingType, isEdit) {
|
||||||
if (isEdit) originalExistingType?.key ?: MeasurementTypeKey.CUSTOM
|
if (isEdit) originalExistingType?.key ?: MeasurementTypeKey.CUSTOM else MeasurementTypeKey.CUSTOM
|
||||||
else MeasurementTypeKey.CUSTOM
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the list of allowed units based on the key.
|
val allowedUnitsForKey = remember(currentMeasurementTypeKey) { currentMeasurementTypeKey.allowedUnitTypes }
|
||||||
val allowedUnitsForKey = remember(currentMeasurementTypeKey) {
|
val allowedInputTypesForKey = remember(currentMeasurementTypeKey) { currentMeasurementTypeKey.allowedInputType }
|
||||||
currentMeasurementTypeKey.allowedUnitTypes
|
|
||||||
}
|
|
||||||
|
|
||||||
val allowedInputTypesForKey = remember(currentMeasurementTypeKey) {
|
|
||||||
currentMeasurementTypeKey.allowedInputType
|
|
||||||
}
|
|
||||||
|
|
||||||
var name by remember { mutableStateOf(originalExistingType?.getDisplayName(context).orEmpty()) }
|
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 {
|
var selectedUnit by remember {
|
||||||
val initialUnit = originalExistingType?.unit
|
val u = originalExistingType?.unit
|
||||||
if (initialUnit != null && initialUnit in allowedUnitsForKey) {
|
mutableStateOf(if (u != null && u in allowedUnitsForKey) u else allowedUnitsForKey.firstOrNull() ?: UnitType.NONE)
|
||||||
mutableStateOf(initialUnit)
|
|
||||||
} else {
|
|
||||||
mutableStateOf(allowedUnitsForKey.firstOrNull() ?: UnitType.NONE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedInputType by remember {
|
var selectedInputType by remember {
|
||||||
val initialInputType = originalExistingType?.inputType
|
val itp = originalExistingType?.inputType
|
||||||
if (initialInputType != null && initialInputType in allowedInputTypesForKey) {
|
mutableStateOf(if (itp != null && itp in allowedInputTypesForKey) itp else allowedInputTypesForKey.firstOrNull() ?: InputFieldType.FLOAT)
|
||||||
mutableStateOf(initialInputType)
|
|
||||||
} else {
|
|
||||||
mutableStateOf(allowedInputTypesForKey.firstOrNull() ?: InputFieldType.FLOAT)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var selectedColor by remember { mutableStateOf(originalExistingType?.color ?: 0xFFFFA726.toInt()) }
|
var selectedColor by remember { mutableStateOf(originalExistingType?.color ?: 0xFFFFA726.toInt()) }
|
||||||
var selectedIcon by remember { mutableStateOf(originalExistingType?.icon ?: MeasurementTypeIcon.IC_DEFAULT) }
|
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 isPinned by remember { mutableStateOf(originalExistingType?.isPinned ?: false) }
|
||||||
var isOnRightYAxis by remember { mutableStateOf(originalExistingType?.isOnRightYAxis ?: 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 expandedUnit by remember { mutableStateOf(false) }
|
||||||
var expandedInputType by remember { mutableStateOf(false) }
|
var expandedInputType by remember { mutableStateOf(false) }
|
||||||
var showColorPicker by remember { mutableStateOf(false) }
|
var showColorPicker by remember { mutableStateOf(false) }
|
||||||
var showIconPicker 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 titleEdit = stringResource(R.string.measurement_type_detail_title_edit)
|
||||||
val titleAdd = stringResource(R.string.measurement_type_detail_title_add)
|
val titleAdd = stringResource(R.string.measurement_type_detail_title_add)
|
||||||
|
|
||||||
val unitDropdownEnabled by remember(allowedUnitsForKey) {
|
val unitDropdownEnabled by remember(allowedUnitsForKey) { derivedStateOf { allowedUnitsForKey.size > 1 } }
|
||||||
derivedStateOf { allowedUnitsForKey.size > 1 }
|
val inputTypeDropdownEnabled by remember(allowedInputTypesForKey) { derivedStateOf { allowedInputTypesForKey.size > 1 } }
|
||||||
}
|
|
||||||
val inputTypeDropdownEnabled by remember(allowedInputTypesForKey) {
|
|
||||||
derivedStateOf { allowedInputTypesForKey.size > 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(originalExistingType, allowedUnitsForKey) {
|
LaunchedEffect(originalExistingType, allowedUnitsForKey) {
|
||||||
val currentUnitInExistingType = originalExistingType?.unit
|
originalExistingType?.unit?.let { if (it in allowedUnitsForKey && it != selectedUnit) selectedUnit = it }
|
||||||
if (currentUnitInExistingType != null && currentUnitInExistingType in allowedUnitsForKey) {
|
if (allowedUnitsForKey.isNotEmpty() && selectedUnit !in allowedUnitsForKey) selectedUnit = allowedUnitsForKey.first()
|
||||||
if (selectedUnit != currentUnitInExistingType) { // Only update if different to avoid recomposition loops
|
if (allowedUnitsForKey.isEmpty() && selectedUnit != UnitType.NONE) selectedUnit = UnitType.NONE
|
||||||
selectedUnit = currentUnitInExistingType
|
|
||||||
}
|
|
||||||
} else if (allowedUnitsForKey.isNotEmpty() && selectedUnit !in allowedUnitsForKey) {
|
|
||||||
selectedUnit = allowedUnitsForKey.first()
|
|
||||||
} else if (allowedUnitsForKey.isEmpty() && selectedUnit != UnitType.NONE) {
|
|
||||||
// This case should ideally not be reached if keys are well-defined.
|
|
||||||
selectedUnit = UnitType.NONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(originalExistingType, allowedInputTypesForKey) {
|
LaunchedEffect(originalExistingType, allowedInputTypesForKey) {
|
||||||
val currentInputTypeInExistingType = originalExistingType?.inputType
|
originalExistingType?.inputType?.let { if (it in allowedInputTypesForKey && it != selectedInputType) selectedInputType = it }
|
||||||
if (currentInputTypeInExistingType != null && currentInputTypeInExistingType in allowedInputTypesForKey) {
|
if (allowedInputTypesForKey.isNotEmpty() && selectedInputType !in allowedInputTypesForKey) selectedInputType = allowedInputTypesForKey.first()
|
||||||
if (selectedInputType != currentInputTypeInExistingType) {
|
if (allowedInputTypesForKey.isEmpty()) selectedInputType = InputFieldType.FLOAT
|
||||||
selectedInputType = currentInputTypeInExistingType
|
|
||||||
}
|
|
||||||
} else if (allowedInputTypesForKey.isNotEmpty() && selectedInputType !in allowedInputTypesForKey) {
|
|
||||||
selectedInputType = allowedInputTypesForKey.first()
|
|
||||||
} else if (allowedInputTypesForKey.isEmpty()) {
|
|
||||||
selectedInputType = InputFieldType.FLOAT
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
sharedViewModel.setTopBarTitle(if (isEdit) titleEdit else titleAdd)
|
sharedViewModel.setTopBarTitle(if (isEdit) titleEdit else titleAdd)
|
||||||
sharedViewModel.setTopBarAction(
|
sharedViewModel.setTopBarAction(
|
||||||
TopBarAction(icon = Icons.Default.Save, onClick = {
|
TopBarAction(icon = Icons.Default.Save, onClick = {
|
||||||
if (name.isNotBlank()) {
|
if (name.isBlank()) {
|
||||||
// When creating the updatedType, use the key of the originalExistingType if it's an edit.
|
Toast.makeText(context, R.string.toast_enter_valid_data, Toast.LENGTH_SHORT).show()
|
||||||
// For new types, it's MeasurementTypeKey.CUSTOM.
|
return@TopBarAction
|
||||||
val finalKey = if (isEdit) originalExistingType?.key ?: MeasurementTypeKey.CUSTOM else MeasurementTypeKey.CUSTOM
|
}
|
||||||
|
|
||||||
val currentUpdatedType = MeasurementType(
|
val finalKey = if (isEdit) originalExistingType?.key ?: MeasurementTypeKey.CUSTOM else MeasurementTypeKey.CUSTOM
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isEdit && originalExistingType != null) {
|
val derivedForType = when (currentMeasurementTypeKey) {
|
||||||
val unitChanged = originalExistingType.unit != currentUpdatedType.unit
|
MeasurementTypeKey.BODY_FAT -> bodyFatFormula != BodyFatFormulaOption.OFF
|
||||||
val inputTypesAreFloat =
|
MeasurementTypeKey.WATER -> bodyWaterFormula != BodyWaterFormulaOption.OFF
|
||||||
originalExistingType.inputType == InputFieldType.FLOAT && currentUpdatedType.inputType == InputFieldType.FLOAT
|
MeasurementTypeKey.LBM -> lbmFormula != LbmFormulaOption.OFF
|
||||||
|
else -> originalExistingType?.isDerived ?: false
|
||||||
|
}
|
||||||
|
|
||||||
if (unitChanged && inputTypesAreFloat) {
|
val updatedType = MeasurementType(
|
||||||
pendingUpdatedType = currentUpdatedType
|
id = originalExistingType?.id ?: 0,
|
||||||
showConfirmDialog = true
|
name = name,
|
||||||
} else {
|
icon = selectedIcon,
|
||||||
settingsViewModel.updateMeasurementType(currentUpdatedType)
|
color = selectedColor,
|
||||||
navController.popBackStack()
|
unit = selectedUnit,
|
||||||
}
|
inputType = selectedInputType,
|
||||||
} else {
|
displayOrder = originalExistingType?.displayOrder ?: measurementTypes.size,
|
||||||
settingsViewModel.addMeasurementType(currentUpdatedType)
|
isEnabled = isEnabled,
|
||||||
navController.popBackStack()
|
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 {
|
} else {
|
||||||
Toast.makeText(context, R.string.toast_enter_valid_data, Toast.LENGTH_SHORT)
|
settingsViewModel.addMeasurementType(updatedType)
|
||||||
.show()
|
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(
|
AlertDialog(
|
||||||
onDismissRequest = { showConfirmDialog = false },
|
onDismissRequest = { pendingDialog = null },
|
||||||
title = { Text(stringResource(R.string.measurement_type_dialog_confirm_unit_change_title)) },
|
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 = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
text = message,
|
||||||
R.string.measurement_type_dialog_confirm_unit_change_message,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
originalExistingType.getDisplayName(context),
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
originalExistingType.unit.displayName.lowercase().replaceFirstChar { it.uppercase() },
|
|
||||||
pendingUpdatedType!!.unit.displayName.lowercase().replaceFirstChar { it.uppercase() }
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = {
|
TextButton(
|
||||||
settingsViewModel.updateMeasurementTypeWithConversion(
|
onClick = {
|
||||||
originalType = originalExistingType,
|
when (dlg) {
|
||||||
updatedType = pendingUpdatedType!!
|
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 = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showConfirmDialog = false }) { Text(stringResource(R.string.cancel_button)) }
|
TextButton(onClick = { pendingDialog = null }) {
|
||||||
|
Text(stringResource(R.string.cancel_button))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(16.dp).fillMaxSize(),
|
||||||
.padding(16.dp)
|
|
||||||
.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_enabled)) {
|
OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_enabled)) {
|
||||||
Switch(
|
Switch(checked = isEnabled, onCheckedChange = { isEnabled = it })
|
||||||
checked = isEnabled,
|
|
||||||
onCheckedChange = { isEnabled = it }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isEdit || (originalExistingType?.key == MeasurementTypeKey.CUSTOM)) {
|
if (!isEdit || (originalExistingType?.key == MeasurementTypeKey.CUSTOM)) {
|
||||||
@@ -295,20 +361,13 @@ fun MeasurementTypeDetailScreen(
|
|||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = "",
|
value = "",
|
||||||
onValueChange = {}, // Read-only
|
onValueChange = {},
|
||||||
label = { Text(stringResource(R.string.measurement_type_label_color)) },
|
label = { Text(stringResource(R.string.measurement_type_label_color)) },
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().clickable { showColorPicker = true },
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { showColorPicker = true },
|
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
enabled = false, // To make it look like a display field that's clickable
|
enabled = false,
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Box(
|
Box(modifier = Modifier.size(24.dp).clip(CircleShape).background(Color(selectedColor)))
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(Color(selectedColor))
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
disabledTextColor = LocalContentColor.current,
|
disabledTextColor = LocalContentColor.current,
|
||||||
@@ -321,18 +380,12 @@ fun MeasurementTypeDetailScreen(
|
|||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = "",
|
value = "",
|
||||||
onValueChange = {}, // Read-only
|
onValueChange = {},
|
||||||
label = { Text(stringResource(R.string.measurement_type_label_icon)) },
|
label = { Text(stringResource(R.string.measurement_type_label_icon)) },
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().clickable { showIconPicker = true },
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { showIconPicker = true },
|
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
enabled = false, // To make it look like a display field
|
enabled = false,
|
||||||
trailingIcon = {
|
trailingIcon = { MeasurementIcon(icon = selectedIcon) },
|
||||||
MeasurementIcon(
|
|
||||||
icon = selectedIcon,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
disabledTextColor = LocalContentColor.current,
|
disabledTextColor = LocalContentColor.current,
|
||||||
disabledIndicatorColor = MaterialTheme.colorScheme.outline,
|
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) {
|
if (unitDropdownEnabled) {
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
expanded = expandedUnit && unitDropdownEnabled,
|
expanded = expandedUnit && unitDropdownEnabled,
|
||||||
onExpandedChange = {
|
onExpandedChange = { if (unitDropdownEnabled) expandedUnit = !expandedUnit },
|
||||||
if (unitDropdownEnabled) expandedUnit = !expandedUnit
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
OutlinedSettingRow(
|
OutlinedSettingRow(
|
||||||
label = stringResource(R.string.measurement_type_label_unit),
|
label = stringResource(R.string.measurement_type_label_unit),
|
||||||
surfaceModifier = Modifier
|
surfaceModifier = Modifier
|
||||||
.menuAnchor(
|
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable, enabled = unitDropdownEnabled)
|
||||||
type = MenuAnchorType.PrimaryNotEditable,
|
.clickable(enabled = unitDropdownEnabled) { if (unitDropdownEnabled) expandedUnit = true },
|
||||||
enabled = unitDropdownEnabled
|
|
||||||
)
|
|
||||||
.clickable(enabled = unitDropdownEnabled) {
|
|
||||||
if (unitDropdownEnabled) expandedUnit = true
|
|
||||||
},
|
|
||||||
controlContent = {
|
controlContent = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
@@ -367,13 +460,10 @@ fun MeasurementTypeDetailScreen(
|
|||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = if (unitDropdownEnabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
color = if (unitDropdownEnabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||||
)
|
)
|
||||||
if (unitDropdownEnabled) {
|
if (unitDropdownEnabled) ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedUnit)
|
||||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedUnit)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (unitDropdownEnabled) {
|
if (unitDropdownEnabled) {
|
||||||
ExposedDropdownMenu(
|
ExposedDropdownMenu(
|
||||||
expanded = expandedUnit,
|
expanded = expandedUnit,
|
||||||
@@ -383,19 +473,24 @@ fun MeasurementTypeDetailScreen(
|
|||||||
allowedUnitsForKey.forEach { unit ->
|
allowedUnitsForKey.forEach { unit ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Box(
|
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Text(unit.displayName, modifier = Modifier.padding(end = 32.dp))
|
||||||
contentAlignment = Alignment.CenterEnd
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = unit.displayName,
|
|
||||||
modifier = Modifier.padding(end = 32.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
selectedUnit = unit
|
|
||||||
expandedUnit = false
|
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) {
|
if (inputTypeDropdownEnabled) {
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(expanded = expandedInputType, onExpandedChange = { expandedInputType = !expandedInputType }) {
|
||||||
expanded = expandedInputType,
|
|
||||||
onExpandedChange = { expandedInputType = !expandedInputType }
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
readOnly = true,
|
readOnly = true,
|
||||||
value = selectedInputType.name.lowercase().replaceFirstChar { it.uppercase() },
|
value = selectedInputType.name.lowercase().replaceFirstChar { it.uppercase() },
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
label = { Text(stringResource(R.string.measurement_type_label_input_type)) },
|
label = { Text(stringResource(R.string.measurement_type_label_input_type)) },
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedInputType) },
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedInputType) },
|
||||||
modifier = Modifier
|
modifier = Modifier.menuAnchor(type = MenuAnchorType.PrimaryNotEditable, enabled = true).fillMaxWidth()
|
||||||
.menuAnchor(type = MenuAnchorType.PrimaryNotEditable, enabled = true)
|
|
||||||
.fillMaxWidth()
|
|
||||||
)
|
)
|
||||||
ExposedDropdownMenu(
|
ExposedDropdownMenu(expanded = expandedInputType, onDismissRequest = { expandedInputType = false }) {
|
||||||
expanded = expandedInputType,
|
|
||||||
onDismissRequest = { expandedInputType = false }
|
|
||||||
) {
|
|
||||||
allowedInputTypesForKey.forEach { type ->
|
allowedInputTypesForKey.forEach { type ->
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = { Text(type.name.lowercase().replaceFirstChar { it.uppercase() }) },
|
||||||
Text(
|
onClick = { selectedInputType = type; expandedInputType = false }
|
||||||
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)) {
|
OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_pinned)) {
|
||||||
Switch(
|
Switch(checked = isPinned, onCheckedChange = { isPinned = it })
|
||||||
checked = isPinned,
|
|
||||||
onCheckedChange = { isPinned = it }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_on_right_y_axis)) {
|
OutlinedSettingRow(label = stringResource(R.string.measurement_type_label_on_right_y_axis)) {
|
||||||
Switch(
|
Switch(checked = isOnRightYAxis, onCheckedChange = { isOnRightYAxis = it })
|
||||||
checked = isOnRightYAxis,
|
|
||||||
onCheckedChange = { isOnRightYAxis = it }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,19 +535,48 @@ fun MeasurementTypeDetailScreen(
|
|||||||
onDismiss = { showColorPicker = false }
|
onDismiss = { showColorPicker = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showIconPicker) {
|
if (showIconPicker) {
|
||||||
IconPickerDialog(
|
IconPickerDialog(
|
||||||
iconBackgroundColor = Color(selectedColor),
|
iconBackgroundColor = Color(selectedColor),
|
||||||
onIconSelected = {
|
onIconSelected = { selectedIcon = it; showIconPicker = false },
|
||||||
selectedIcon = it
|
|
||||||
showIconPicker = false
|
|
||||||
},
|
|
||||||
onDismiss = { 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
|
@Composable
|
||||||
private fun OutlinedSettingRow(
|
private fun OutlinedSettingRow(
|
||||||
label: String,
|
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_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="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 -->
|
<!-- 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_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>
|
<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_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="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 -->
|
<!-- Bluetooth -->
|
||||||
<string name="info_bluetooth_connection_error_scale_offline">Could not connect to scale, please ensure it is on.</string>
|
<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>
|
<string name="info_step_on_scale">Please step barefoot on the scale</string>
|
||||||
|
Reference in New Issue
Block a user