Etapp 1: kroppsdata synkas mot servern + BMR + dagens kcal-mätare på Hem
All checks were successful
release / build-release (push) Successful in 5m54s
All checks were successful
release / build-release (push) Successful in 5m54s
- Profile/myProfile med heightCm/birthYear/sex (fallback mot äldre server), updateProfile-mutation i GymApi - Kroppsdata sparas lokalt och synkas: serverns värden vinner, lokala pushas upp om servern är tom (migrerar vågens lokala data) - Mifflin–St Jeor-BMR (data/Bmr.kt) - Hemskärm: Dagens kalorier-kort — aktivt loggat (dagens pass), telefonen (platshållare för etapp 5) och passivt (BMR-andel av dygnet, tickar per minut). Uppmaning om kroppsdata saknas. - Version 0.5.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg
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.4.0" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
|
versionName = "0.5.0" + (ciRunNumber?.let { "+build.$it" } ?: "-dev")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
20
app/src/main/java/eu/brassepc/fitnessdroid/data/Bmr.kt
Normal file
20
app/src/main/java/eu/brassepc/fitnessdroid/data/Bmr.kt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package eu.brassepc.fitnessdroid.data
|
||||||
|
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basalmetabolism (kcal/dygn) enligt Mifflin–St Jeor:
|
||||||
|
* 10·vikt + 6.25·längd − 5·ålder + 5 (man) / −161 (kvinna).
|
||||||
|
* null om något av underlagen saknas.
|
||||||
|
*/
|
||||||
|
fun bmrKcalPerDay(
|
||||||
|
weightKg: Double?,
|
||||||
|
heightCm: Double?,
|
||||||
|
birthYear: Int?,
|
||||||
|
isFemale: Boolean?,
|
||||||
|
): Double? {
|
||||||
|
if (weightKg == null || heightCm == null || birthYear == null || isFemale == null) return null
|
||||||
|
val age = (LocalDate.now().year - birthYear).coerceIn(0, 120)
|
||||||
|
val k = if (isFemale) -161.0 else 5.0
|
||||||
|
return 10.0 * weightKg + 6.25 * heightCm - 5.0 * age + k
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package eu.brassepc.fitnessdroid.data
|
package eu.brassepc.fitnessdroid.data
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Profile(
|
data class Profile(
|
||||||
@@ -10,6 +12,10 @@ data class Profile(
|
|||||||
val displayName: String? = null,
|
val displayName: String? = null,
|
||||||
val email: String? = null,
|
val email: String? = null,
|
||||||
val bodyWeightKg: Double? = null,
|
val bodyWeightKg: Double? = null,
|
||||||
|
val heightCm: Double? = null,
|
||||||
|
val birthYear: Int? = null,
|
||||||
|
/** "male"/"female" — serverns konvention */
|
||||||
|
val sex: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Autentiserade anrop mot gym-API:t. Query-strängarna speglar gymTrackerApi.js. */
|
/** Autentiserade anrop mot gym-API:t. Query-strängarna speglar gymTrackerApi.js. */
|
||||||
@@ -18,14 +24,37 @@ class GymApi(
|
|||||||
private val auth: AuthRepository,
|
private val auth: AuthRepository,
|
||||||
) {
|
) {
|
||||||
suspend fun myProfile(): Profile {
|
suspend fun myProfile(): Profile {
|
||||||
val data = client.execute(MY_PROFILE, token = auth.bearerToken())
|
// Fallback till gamla fältuppsättningen mot en äldre server.
|
||||||
|
val data = try {
|
||||||
|
client.execute(MY_PROFILE, token = auth.bearerToken())
|
||||||
|
} catch (e: GraphQlException) {
|
||||||
|
client.execute(MY_PROFILE_LEGACY, token = auth.bearerToken())
|
||||||
|
}
|
||||||
val profile = data["myProfile"]?.jsonObject
|
val profile = data["myProfile"]?.jsonObject
|
||||||
?: throw GraphQlException(listOf("Kunde inte hämta profilen"))
|
?: throw GraphQlException(listOf("Kunde inte hämta profilen"))
|
||||||
return client.json.decodeFromJsonElement(profile)
|
return client.json.decodeFromJsonElement(profile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Kroppsdata till servern. null = rör ej, -1/"" = rensa (serverns konvention). */
|
||||||
|
suspend fun updateProfile(heightCm: Double?, birthYear: Int?, sex: String?): Profile {
|
||||||
|
val data = client.execute(
|
||||||
|
"mutation(\$h: Float,\$y: Int,\$s: String){updateProfile(heightCm:\$h,birthYear:\$y,sex:\$s){username displayName email bodyWeightKg heightCm birthYear sex}}",
|
||||||
|
buildJsonObject {
|
||||||
|
heightCm?.let { put("h", it) }
|
||||||
|
birthYear?.let { put("y", it) }
|
||||||
|
sex?.let { put("s", it) }
|
||||||
|
},
|
||||||
|
auth.bearerToken(),
|
||||||
|
)
|
||||||
|
val profile = data["updateProfile"]?.jsonObject
|
||||||
|
?: throw GraphQlException(listOf("Kunde inte spara kroppsdata"))
|
||||||
|
return client.json.decodeFromJsonElement(profile)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MY_PROFILE =
|
private const val MY_PROFILE =
|
||||||
|
"query{myProfile{username displayName email bodyWeightKg heightCm birthYear sex}}"
|
||||||
|
private const val MY_PROFILE_LEGACY =
|
||||||
"query{myProfile{username displayName email bodyWeightKg}}"
|
"query{myProfile{username displayName email bodyWeightKg}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Bedtime
|
||||||
import androidx.compose.material.icons.filled.CloudDone
|
import androidx.compose.material.icons.filled.CloudDone
|
||||||
import androidx.compose.material.icons.filled.CloudOff
|
import androidx.compose.material.icons.filled.CloudOff
|
||||||
import androidx.compose.material.icons.filled.CloudUpload
|
import androidx.compose.material.icons.filled.CloudUpload
|
||||||
|
import androidx.compose.material.icons.filled.FitnessCenter
|
||||||
|
import androidx.compose.material.icons.filled.LocalFireDepartment
|
||||||
|
import androidx.compose.material.icons.filled.PhoneAndroid
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Star
|
import androidx.compose.material.icons.filled.Star
|
||||||
import androidx.compose.material.icons.filled.SystemUpdate
|
import androidx.compose.material.icons.filled.SystemUpdate
|
||||||
@@ -43,6 +47,7 @@ fun HomeScreen(
|
|||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val updateState by viewModel.updateState.collectAsStateWithLifecycle()
|
val updateState by viewModel.updateState.collectAsStateWithLifecycle()
|
||||||
val displayName by viewModel.displayName.collectAsStateWithLifecycle()
|
val displayName by viewModel.displayName.collectAsStateWithLifecycle()
|
||||||
|
val kcalToday by viewModel.kcalToday.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -66,6 +71,8 @@ fun HomeScreen(
|
|||||||
SyncChip(status = uiState.syncStatus, pending = uiState.pendingCount)
|
SyncChip(status = uiState.syncStatus, pending = uiState.pendingCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KcalTodayCard(kcalToday)
|
||||||
|
|
||||||
updateState?.let { update ->
|
updateState?.let { update ->
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Row(
|
Row(
|
||||||
@@ -197,6 +204,92 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dagens kaloriförbränning — aktivt loggat / telefon / passivt (BMR). */
|
||||||
|
@Composable
|
||||||
|
private fun KcalTodayCard(kcal: KcalToday) {
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.LocalFireDepartment,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Dagens kalorier",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
modifier = Modifier.weight(1f).padding(start = 8.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"${kcal.totalKcal.toInt()} kcal",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
KcalRow(
|
||||||
|
icon = Icons.Default.FitnessCenter,
|
||||||
|
label = "Aktivt loggat",
|
||||||
|
value = "${kcal.activeKcal.toInt()} kcal",
|
||||||
|
)
|
||||||
|
KcalRow(
|
||||||
|
icon = Icons.Default.PhoneAndroid,
|
||||||
|
label = "Telefonen (steg m.m.)",
|
||||||
|
value = "kommer snart",
|
||||||
|
dimValue = true,
|
||||||
|
)
|
||||||
|
KcalRow(
|
||||||
|
icon = Icons.Default.Bedtime,
|
||||||
|
label = "Passivt (BMR)",
|
||||||
|
value = kcal.passiveKcalSoFar?.let {
|
||||||
|
"${it.toInt()} kcal · ${kcal.bmrPerDay?.toInt()} vid midnatt"
|
||||||
|
} ?: "—",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!kcal.hasBodyData) {
|
||||||
|
Text(
|
||||||
|
"Fyll i vikt + kroppsdata (längd/födelseår/kön) i profilen " +
|
||||||
|
"så kan den passiva förbränningen räknas ut.",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KcalRow(
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
dimValue: Boolean = false,
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = if (dimValue) MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
else MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SyncChip(status: SyncStatus, pending: Int) {
|
private fun SyncChip(status: SyncStatus, pending: Int) {
|
||||||
val (icon, text) = when {
|
val (icon, text) = when {
|
||||||
|
|||||||
@@ -30,10 +30,30 @@ sealed interface UpdateState {
|
|||||||
data class Failed(val message: String) : UpdateState
|
data class Failed(val message: String) : UpdateState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dagens kaloriförbränning, tredelad. Telefondelen (steg/auto-aktiviteter via
|
||||||
|
* Health Connect) kommer i en senare etapp och är null tills dess.
|
||||||
|
*/
|
||||||
|
data class KcalToday(
|
||||||
|
/** Aktivt loggat: gympass (och framöver aktiviteter) */
|
||||||
|
val activeKcal: Double = 0.0,
|
||||||
|
/** Passivt: BMR-andel av dygnet som gått */
|
||||||
|
val passiveKcalSoFar: Double? = null,
|
||||||
|
/** BMR för hela dygnet (för "i mål vid midnatt"-visning) */
|
||||||
|
val bmrPerDay: Double? = null,
|
||||||
|
/** Telefonens auto-loggade (steg) — etapp 5 */
|
||||||
|
val phoneKcal: Double? = null,
|
||||||
|
/** false = längd/födelseår/kön saknas, mätaren kan inte räkna passivt */
|
||||||
|
val hasBodyData: Boolean = false,
|
||||||
|
) {
|
||||||
|
val totalKcal: Double get() = activeKcal + (passiveKcalSoFar ?: 0.0) + (phoneKcal ?: 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
class HomeViewModel(
|
class HomeViewModel(
|
||||||
private val repo: GymRepository,
|
private val repo: GymRepository,
|
||||||
auth: AuthRepository,
|
auth: AuthRepository,
|
||||||
private val updateChecker: eu.brassepc.fitnessdroid.data.UpdateChecker,
|
private val updateChecker: eu.brassepc.fitnessdroid.data.UpdateChecker,
|
||||||
|
private val gymApi: eu.brassepc.fitnessdroid.data.GymApi,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val updateState = kotlinx.coroutines.flow.MutableStateFlow<UpdateState?>(null)
|
val updateState = kotlinx.coroutines.flow.MutableStateFlow<UpdateState?>(null)
|
||||||
@@ -41,11 +61,61 @@ class HomeViewModel(
|
|||||||
/** Helnamn från profilen — trevligare hälsning än användarnamnet. */
|
/** Helnamn från profilen — trevligare hälsning än användarnamnet. */
|
||||||
val displayName = kotlinx.coroutines.flow.MutableStateFlow<String?>(null)
|
val displayName = kotlinx.coroutines.flow.MutableStateFlow<String?>(null)
|
||||||
|
|
||||||
|
val kcalToday = kotlinx.coroutines.flow.MutableStateFlow(KcalToday())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
displayName.value = repo.displayName()
|
displayName.value = repo.displayName()
|
||||||
updateChecker.check()?.let { updateState.value = UpdateState.Available(it) }
|
updateChecker.check()?.let { updateState.value = UpdateState.Available(it) }
|
||||||
}
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
refreshKcal()
|
||||||
|
// Passivdelen tickar med tiden — räkna om lokalt varje minut
|
||||||
|
// (nätverket rörs bara vid laddning).
|
||||||
|
while (true) {
|
||||||
|
kotlinx.coroutines.delay(60_000)
|
||||||
|
tickPassive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun secondsIntoToday(): Long = java.time.Duration.between(
|
||||||
|
java.time.LocalDate.now().atStartOfDay(), java.time.LocalDateTime.now()
|
||||||
|
).seconds.coerceAtLeast(0)
|
||||||
|
|
||||||
|
private fun tickPassive() {
|
||||||
|
val current = kcalToday.value
|
||||||
|
kcalToday.value = current.copy(
|
||||||
|
passiveKcalSoFar = current.bmrPerDay?.let { it * secondsIntoToday() / 86_400.0 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshKcal() {
|
||||||
|
val today = java.time.LocalDate.now()
|
||||||
|
val active = runCatching {
|
||||||
|
repo.fetchHistory(limit = 15)
|
||||||
|
.filter { it.date.take(10) == today.toString() }
|
||||||
|
.sumOf { it.calories ?: 0.0 }
|
||||||
|
}.getOrElse { kcalToday.value.activeKcal }
|
||||||
|
|
||||||
|
val (bmr, hasBodyData) = runCatching {
|
||||||
|
val p = gymApi.myProfile()
|
||||||
|
val bmr = eu.brassepc.fitnessdroid.data.bmrKcalPerDay(
|
||||||
|
weightKg = p.bodyWeightKg,
|
||||||
|
heightCm = p.heightCm,
|
||||||
|
birthYear = p.birthYear,
|
||||||
|
isFemale = p.sex?.let { it == "female" },
|
||||||
|
)
|
||||||
|
bmr to (bmr != null)
|
||||||
|
}.getOrElse { kcalToday.value.bmrPerDay to kcalToday.value.hasBodyData }
|
||||||
|
|
||||||
|
kcalToday.value = KcalToday(
|
||||||
|
activeKcal = active,
|
||||||
|
passiveKcalSoFar = bmr?.let { it * secondsIntoToday() / 86_400.0 },
|
||||||
|
bmrPerDay = bmr,
|
||||||
|
phoneKcal = null,
|
||||||
|
hasBodyData = hasBodyData,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startUpdate() {
|
fun startUpdate() {
|
||||||
@@ -99,7 +169,7 @@ class HomeViewModel(
|
|||||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
val c = appContainer()
|
val c = appContainer()
|
||||||
HomeViewModel(c.gymRepository, c.authRepository, c.updateChecker)
|
HomeViewModel(c.gymRepository, c.authRepository, c.updateChecker, c.gymApi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import eu.brassepc.fitnessdroid.data.Profile
|
|||||||
import eu.brassepc.fitnessdroid.ui.appContainer
|
import eu.brassepc.fitnessdroid.ui.appContainer
|
||||||
import eu.brassepc.fitnessdroid.ui.common.compact
|
import eu.brassepc.fitnessdroid.ui.common.compact
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ProfileViewModel(
|
class ProfileViewModel(
|
||||||
@@ -60,15 +61,57 @@ class ProfileViewModel(
|
|||||||
val message = MutableStateFlow<String?>(null)
|
val message = MutableStateFlow<String?>(null)
|
||||||
val settings = settingsStore.settings
|
val settings = settingsStore.settings
|
||||||
|
|
||||||
|
/** Kroppsdata sparas lokalt (för vågen/offline) och synkas till servern. */
|
||||||
fun saveBodyData(heightCm: Double?, birthYear: Int?, isFemale: Boolean?) {
|
fun saveBodyData(heightCm: Double?, birthYear: Int?, isFemale: Boolean?) {
|
||||||
viewModelScope.launch { settingsStore.setBodyData(heightCm, birthYear, isFemale) }
|
viewModelScope.launch {
|
||||||
|
settingsStore.setBodyData(heightCm, birthYear, isFemale)
|
||||||
|
try {
|
||||||
|
// Tomt fält = rensa på servern (-1/"" är serverns konvention)
|
||||||
|
gymApi.updateProfile(
|
||||||
|
heightCm = heightCm ?: -1.0,
|
||||||
|
birthYear = birthYear ?: -1,
|
||||||
|
sex = when (isFemale) {
|
||||||
|
true -> "female"
|
||||||
|
false -> "male"
|
||||||
|
null -> ""
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
message.value = "Kroppsdata sparad lokalt — kunde inte nå servern"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Håll lokala kroppsdatan i synk med servern: serverns värden vinner,
|
||||||
|
* men finns de bara lokalt (satta innan servern fick fälten) pushas de upp.
|
||||||
|
*/
|
||||||
|
private suspend fun syncBodyData(p: Profile) {
|
||||||
|
val local = settingsStore.settings.first()
|
||||||
|
val serverHas = p.heightCm != null || p.birthYear != null || p.sex != null
|
||||||
|
val localHas = local.heightCm != null || local.birthYear != null || local.isFemale != null
|
||||||
|
if (serverHas) {
|
||||||
|
settingsStore.setBodyData(p.heightCm, p.birthYear, p.sex?.let { it == "female" })
|
||||||
|
} else if (localHas) {
|
||||||
|
runCatching {
|
||||||
|
gymApi.updateProfile(
|
||||||
|
heightCm = local.heightCm,
|
||||||
|
birthYear = local.birthYear,
|
||||||
|
sex = local.isFemale?.let { if (it) "female" else "male" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init { load() }
|
init { load() }
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatching { profile.value = gymApi.myProfile() }
|
runCatching {
|
||||||
|
val p = gymApi.myProfile()
|
||||||
|
profile.value = p
|
||||||
|
syncBodyData(p)
|
||||||
|
}
|
||||||
runCatching { measurements.value = repo.fetchBodyMeasurements().reversed() }
|
runCatching { measurements.value = repo.fetchBodyMeasurements().reversed() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ frontend av `src/**` m.fl., FitnessDroid av varje push till main.)
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
- [ ] Etapp 1 — Profil & kalorigrund
|
- [x] Etapp 1 — Profil & kalorigrund *(kod klar 2026-08-23: backend a1cd4d3, webb 560894c, app — väntar på användartest + omdeploy av gym-api/frontend-containrarna)*
|
||||||
- [ ] Etapp 2 — Aktiviteter: bibliotek + manuell logg
|
- [ ] Etapp 2 — Aktiviteter: bibliotek + manuell logg
|
||||||
- [ ] Etapp 3 — Live-spårning (GPS)
|
- [ ] Etapp 3 — Live-spårning (GPS)
|
||||||
- [ ] Etapp 4 — Mål & mätare
|
- [ ] Etapp 4 — Mål & mätare
|
||||||
|
|||||||
Reference in New Issue
Block a user