From 7c21428a283826ef8561ed049f3c950bd48f1101 Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 25 Jul 2026 18:30:00 +0200 Subject: [PATCH] =?UTF-8?q?Kroppssammans=C3=A4ttning:=20muskel/fett/vatten?= =?UTF-8?q?-%=20i=20vikt-dialogen=20+=20historiken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP --- app/build.gradle.kts | 2 +- .../fitnessdroid/data/GymRepository.kt | 24 ++++- .../fitnessdroid/ui/profile/ProfileScreen.kt | 89 ++++++++++++++++--- 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 14ddeaa..b61b869 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,7 +29,7 @@ android { minSdk = 31 targetSdk = 35 versionCode = ciRunNumber ?: 1 - versionName = "0.3.0" + (ciRunNumber?.let { "+build.$it" } ?: "-dev") + versionName = "0.3.1" + (ciRunNumber?.let { "+build.$it" } ?: "-dev") } signingConfigs { diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt b/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt index 04972c6..279a48f 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt @@ -183,7 +183,13 @@ class GymRepository( } /** 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 { client.execute( "mutation(\$input: AddBodyMeasurementInput!){addBodyMeasurement(input:\$input){id}}", @@ -191,6 +197,9 @@ class GymRepository( put("input", buildJsonObject { put("weightKg", weightKg) dateIso?.let { put("date", it) } + musclePercent?.let { put("musclePercent", it) } + fatPercent?.let { put("fatPercent", it) } + waterPercent?.let { put("waterPercent", it) } }) }, auth.bearerToken(), @@ -205,7 +214,15 @@ class GymRepository( 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( "mutation(\$input: UpdateBodyMeasurementInput!){updateBodyMeasurement(input:\$input){id}}", buildJsonObject { @@ -213,6 +230,9 @@ class GymRepository( put("id", id) put("weightKg", weightKg) dateIso?.let { put("date", it) } + musclePercent?.let { put("musclePercent", it) } + fatPercent?.let { put("fatPercent", it) } + waterPercent?.let { put("waterPercent", it) } }) }, auth.bearerToken(), diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/profile/ProfileScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/profile/ProfileScreen.kt index b6d16fb..1b134be 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/profile/ProfileScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/profile/ProfileScreen.kt @@ -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 { try { - repo.addBodyMeasurement(weightKg, dateIso) + repo.addBodyMeasurement(weightKg, dateIso, muscle, fat, water) message.value = "Vikten sparad" load() } 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 { 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() } catch (e: Exception) { message.value = "Kunde inte uppdatera — offline?" @@ -130,8 +137,11 @@ fun ProfileScreen( title = "Uppdatera kroppsvikt", initial = profile?.bodyWeightKg, initialDate = java.time.LocalDate.now(), - onSave = { weight, dateIso -> - viewModel.addWeight(weight, dateIso) + initialMuscle = null, + initialFat = null, + initialWater = null, + onSave = { weight, dateIso, muscle, fat, water -> + viewModel.addWeight(weight, dateIso, muscle, fat, water) showAddWeight = false }, onDismiss = { showAddWeight = false }, @@ -144,8 +154,11 @@ fun ProfileScreen( initial = m.weightKg, initialDate = runCatching { java.time.LocalDate.parse(m.date.take(10)) } .getOrElse { java.time.LocalDate.now() }, - onSave = { weight, dateIso -> - viewModel.updateMeasurement(m.id, weight, dateIso) + initialMuscle = m.musclePercent, + initialFat = m.fatPercent, + initialWater = m.waterPercent, + onSave = { weight, dateIso, muscle, fat, water -> + viewModel.updateMeasurement(m.id, weight, dateIso, muscle, fat, water) editTarget = null }, onDelete = { @@ -222,6 +235,19 @@ fun ProfileScreen( color = MaterialTheme.colorScheme.onSurfaceVariant, 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( "${m.weightKg.compact()} kg", style = MaterialTheme.typography.bodySmall, @@ -266,16 +292,22 @@ private fun WeightDialog( title: String, initial: Double?, initialDate: java.time.LocalDate, - onSave: (Double, String?) -> Unit, + initialMuscle: Double?, + initialFat: Double?, + initialWater: Double?, + onSave: (Double, String?, Double?, Double?, Double?) -> Unit, onDismiss: () -> Unit, onDelete: (() -> Unit)? = null, ) { - var text by remember { - mutableStateOf(initial?.let { if (it % 1.0 == 0.0) it.toInt().toString() else it.toString() } ?: "") - } + fun fmt(v: Double?) = v?.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 showDatePicker by remember { mutableStateOf(false) } val parsed = text.trim().replace(',', '.').toDoubleOrNull() + fun pct(s: String) = s.trim().replace(',', '.').toDoubleOrNull() if (showDatePicker) { val dateState = androidx.compose.material3.rememberDatePickerState( @@ -320,6 +352,37 @@ private fun WeightDialog( ) { 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 { TextButton(onClick = it) { 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 val iso = date.atTime(12, 0).atOffset(java.time.ZoneOffset.UTC) .toInstant().toString() - onSave(weight, iso) + onSave(weight, iso, pct(muscle), pct(fat), pct(water)) } }, enabled = parsed != null && parsed > 0,