Kroppssammansättning: muskel/fett/vatten-% i vikt-dialogen + historiken
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:
2026-07-25 18:30:00 +02:00
parent 7a4995823e
commit 7c21428a28
3 changed files with 99 additions and 16 deletions

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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,