Kroppssammansättning: muskel/fett/vatten-% i vikt-dialogen + historiken
All checks were successful
release / build-release (push) Successful in 5m21s
All checks were successful
release / build-release (push) Successful in 5m21s
Valfria fält vid ny mätning och rättning; visas i historiklistan. Tomt fält vid rättning rensar värdet på servern (-1-konventionen). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP
This commit is contained in:
@@ -29,7 +29,7 @@ android {
|
|||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = ciRunNumber ?: 1
|
versionCode = ciRunNumber ?: 1
|
||||||
versionName = "0.3.0" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
|
versionName = "0.3.1" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -183,7 +183,13 @@ class GymRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Ny mätning. Faller tillbaka på gamla updateBodyWeight mot äldre API. */
|
/** Ny mätning. Faller tillbaka på gamla updateBodyWeight mot äldre API. */
|
||||||
suspend fun addBodyMeasurement(weightKg: Double, dateIso: String? = null) {
|
suspend fun addBodyMeasurement(
|
||||||
|
weightKg: Double,
|
||||||
|
dateIso: String? = null,
|
||||||
|
musclePercent: Double? = null,
|
||||||
|
fatPercent: Double? = null,
|
||||||
|
waterPercent: Double? = null,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
client.execute(
|
client.execute(
|
||||||
"mutation(\$input: AddBodyMeasurementInput!){addBodyMeasurement(input:\$input){id}}",
|
"mutation(\$input: AddBodyMeasurementInput!){addBodyMeasurement(input:\$input){id}}",
|
||||||
@@ -191,6 +197,9 @@ class GymRepository(
|
|||||||
put("input", buildJsonObject {
|
put("input", buildJsonObject {
|
||||||
put("weightKg", weightKg)
|
put("weightKg", weightKg)
|
||||||
dateIso?.let { put("date", it) }
|
dateIso?.let { put("date", it) }
|
||||||
|
musclePercent?.let { put("musclePercent", it) }
|
||||||
|
fatPercent?.let { put("fatPercent", it) }
|
||||||
|
waterPercent?.let { put("waterPercent", it) }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
auth.bearerToken(),
|
auth.bearerToken(),
|
||||||
@@ -205,7 +214,15 @@ class GymRepository(
|
|||||||
store.saveBodyWeight(weightKg)
|
store.saveBodyWeight(weightKg)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateBodyMeasurement(id: String, weightKg: Double, dateIso: String?) {
|
/** null = rör inte fältet, -1 = rensa (serverns konvention). */
|
||||||
|
suspend fun updateBodyMeasurement(
|
||||||
|
id: String,
|
||||||
|
weightKg: Double,
|
||||||
|
dateIso: String?,
|
||||||
|
musclePercent: Double? = null,
|
||||||
|
fatPercent: Double? = null,
|
||||||
|
waterPercent: Double? = null,
|
||||||
|
) {
|
||||||
client.execute(
|
client.execute(
|
||||||
"mutation(\$input: UpdateBodyMeasurementInput!){updateBodyMeasurement(input:\$input){id}}",
|
"mutation(\$input: UpdateBodyMeasurementInput!){updateBodyMeasurement(input:\$input){id}}",
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
@@ -213,6 +230,9 @@ class GymRepository(
|
|||||||
put("id", id)
|
put("id", id)
|
||||||
put("weightKg", weightKg)
|
put("weightKg", weightKg)
|
||||||
dateIso?.let { put("date", it) }
|
dateIso?.let { put("date", it) }
|
||||||
|
musclePercent?.let { put("musclePercent", it) }
|
||||||
|
fatPercent?.let { put("fatPercent", it) }
|
||||||
|
waterPercent?.let { put("waterPercent", it) }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
auth.bearerToken(),
|
auth.bearerToken(),
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ class ProfileViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addWeight(weightKg: Double, dateIso: String?) {
|
fun addWeight(weightKg: Double, dateIso: String?, muscle: Double?, fat: Double?, water: Double?) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
repo.addBodyMeasurement(weightKg, dateIso)
|
repo.addBodyMeasurement(weightKg, dateIso, muscle, fat, water)
|
||||||
message.value = "Vikten sparad"
|
message.value = "Vikten sparad"
|
||||||
load()
|
load()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -78,10 +78,17 @@ class ProfileViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMeasurement(id: String, weightKg: Double, dateIso: String?) {
|
fun updateMeasurement(
|
||||||
|
id: String, weightKg: Double, dateIso: String?,
|
||||||
|
muscle: Double?, fat: Double?, water: Double?,
|
||||||
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
repo.updateBodyMeasurement(id, weightKg, dateIso)
|
// -1 rensar fältet på servern om användaren tömt det
|
||||||
|
repo.updateBodyMeasurement(
|
||||||
|
id, weightKg, dateIso,
|
||||||
|
muscle ?: -1.0, fat ?: -1.0, water ?: -1.0,
|
||||||
|
)
|
||||||
load()
|
load()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
message.value = "Kunde inte uppdatera — offline?"
|
message.value = "Kunde inte uppdatera — offline?"
|
||||||
@@ -130,8 +137,11 @@ fun ProfileScreen(
|
|||||||
title = "Uppdatera kroppsvikt",
|
title = "Uppdatera kroppsvikt",
|
||||||
initial = profile?.bodyWeightKg,
|
initial = profile?.bodyWeightKg,
|
||||||
initialDate = java.time.LocalDate.now(),
|
initialDate = java.time.LocalDate.now(),
|
||||||
onSave = { weight, dateIso ->
|
initialMuscle = null,
|
||||||
viewModel.addWeight(weight, dateIso)
|
initialFat = null,
|
||||||
|
initialWater = null,
|
||||||
|
onSave = { weight, dateIso, muscle, fat, water ->
|
||||||
|
viewModel.addWeight(weight, dateIso, muscle, fat, water)
|
||||||
showAddWeight = false
|
showAddWeight = false
|
||||||
},
|
},
|
||||||
onDismiss = { showAddWeight = false },
|
onDismiss = { showAddWeight = false },
|
||||||
@@ -144,8 +154,11 @@ fun ProfileScreen(
|
|||||||
initial = m.weightKg,
|
initial = m.weightKg,
|
||||||
initialDate = runCatching { java.time.LocalDate.parse(m.date.take(10)) }
|
initialDate = runCatching { java.time.LocalDate.parse(m.date.take(10)) }
|
||||||
.getOrElse { java.time.LocalDate.now() },
|
.getOrElse { java.time.LocalDate.now() },
|
||||||
onSave = { weight, dateIso ->
|
initialMuscle = m.musclePercent,
|
||||||
viewModel.updateMeasurement(m.id, weight, dateIso)
|
initialFat = m.fatPercent,
|
||||||
|
initialWater = m.waterPercent,
|
||||||
|
onSave = { weight, dateIso, muscle, fat, water ->
|
||||||
|
viewModel.updateMeasurement(m.id, weight, dateIso, muscle, fat, water)
|
||||||
editTarget = null
|
editTarget = null
|
||||||
},
|
},
|
||||||
onDelete = {
|
onDelete = {
|
||||||
@@ -222,6 +235,19 @@ fun ProfileScreen(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
val composition = listOfNotNull(
|
||||||
|
m.musclePercent?.let { "M ${it.compact()}%" },
|
||||||
|
m.fatPercent?.let { "F ${it.compact()}%" },
|
||||||
|
m.waterPercent?.let { "V ${it.compact()}%" },
|
||||||
|
).joinToString(" · ")
|
||||||
|
if (composition.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
composition,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
"${m.weightKg.compact()} kg",
|
"${m.weightKg.compact()} kg",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@@ -266,16 +292,22 @@ private fun WeightDialog(
|
|||||||
title: String,
|
title: String,
|
||||||
initial: Double?,
|
initial: Double?,
|
||||||
initialDate: java.time.LocalDate,
|
initialDate: java.time.LocalDate,
|
||||||
onSave: (Double, String?) -> Unit,
|
initialMuscle: Double?,
|
||||||
|
initialFat: Double?,
|
||||||
|
initialWater: Double?,
|
||||||
|
onSave: (Double, String?, Double?, Double?, Double?) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onDelete: (() -> Unit)? = null,
|
onDelete: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var text by remember {
|
fun fmt(v: Double?) = v?.let { if (it % 1.0 == 0.0) it.toInt().toString() else it.toString() } ?: ""
|
||||||
mutableStateOf(initial?.let { if (it % 1.0 == 0.0) it.toInt().toString() else it.toString() } ?: "")
|
var text by remember { mutableStateOf(fmt(initial)) }
|
||||||
}
|
var muscle by remember { mutableStateOf(fmt(initialMuscle)) }
|
||||||
|
var fat by remember { mutableStateOf(fmt(initialFat)) }
|
||||||
|
var water by remember { mutableStateOf(fmt(initialWater)) }
|
||||||
var date by remember { mutableStateOf(initialDate) }
|
var date by remember { mutableStateOf(initialDate) }
|
||||||
var showDatePicker by remember { mutableStateOf(false) }
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
val parsed = text.trim().replace(',', '.').toDoubleOrNull()
|
val parsed = text.trim().replace(',', '.').toDoubleOrNull()
|
||||||
|
fun pct(s: String) = s.trim().replace(',', '.').toDoubleOrNull()
|
||||||
|
|
||||||
if (showDatePicker) {
|
if (showDatePicker) {
|
||||||
val dateState = androidx.compose.material3.rememberDatePickerState(
|
val dateState = androidx.compose.material3.rememberDatePickerState(
|
||||||
@@ -320,6 +352,37 @@ private fun WeightDialog(
|
|||||||
) {
|
) {
|
||||||
Text("Datum: $date")
|
Text("Datum: $date")
|
||||||
}
|
}
|
||||||
|
Text(
|
||||||
|
"Kroppssammansättning (valfritt)",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = muscle,
|
||||||
|
onValueChange = { muscle = it },
|
||||||
|
label = { Text("Muskel %") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = fat,
|
||||||
|
onValueChange = { fat = it },
|
||||||
|
label = { Text("Fett %") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OutlinedTextField(
|
||||||
|
value = water,
|
||||||
|
onValueChange = { water = it },
|
||||||
|
label = { Text("Vatten %") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
onDelete?.let {
|
onDelete?.let {
|
||||||
TextButton(onClick = it) {
|
TextButton(onClick = it) {
|
||||||
Text("Ta bort mätningen", color = MaterialTheme.colorScheme.error)
|
Text("Ta bort mätningen", color = MaterialTheme.colorScheme.error)
|
||||||
@@ -334,7 +397,7 @@ private fun WeightDialog(
|
|||||||
// Mitt på dagen UTC så datumet inte glider en dag i någon tidszon
|
// Mitt på dagen UTC så datumet inte glider en dag i någon tidszon
|
||||||
val iso = date.atTime(12, 0).atOffset(java.time.ZoneOffset.UTC)
|
val iso = date.atTime(12, 0).atOffset(java.time.ZoneOffset.UTC)
|
||||||
.toInstant().toString()
|
.toInstant().toString()
|
||||||
onSave(weight, iso)
|
onSave(weight, iso, pct(muscle), pct(fat), pct(water))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = parsed != null && parsed > 0,
|
enabled = parsed != null && parsed > 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user