Etapp 2: aktiviteter — bibliotek, manuell loggning, kcal via dailySummary
Some checks failed
release / build-release (push) Failing after 1h2m7s
Some checks failed
release / build-release (push) Failing after 1h2m7s
- GymApi: activityTypes/activities/add/update/delete + dailySummary - Aktivitetsskärm (Hem → Aktiviteter ❤️): logga med typväljare grupperad per kategori, datum/tid-pickers, varaktighet, distans+höjdmeter för distansbaserade, RPE-slider (talk test) för tidsbaserade; historik med tempo och kcal; rätta/ta bort - Kcal-mätaren använder serverns dailySummary (gym + aktiviteter + BMR) med fallback till lokal beräkning mot äldre server - Version 0.6.0 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C2GA94E1f3cmrvdQLQhYdg
This commit is contained in:
@@ -3,9 +3,50 @@ package eu.brassepc.fitnessdroid.data
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
@Serializable
|
||||
data class ActivityType(
|
||||
val id: Int,
|
||||
val key: String,
|
||||
val nameSv: String,
|
||||
val nameEn: String = "",
|
||||
val met: Double = 4.0,
|
||||
val category: String = "OTHER",
|
||||
val isDistanceBased: Boolean = false,
|
||||
val isCardio: Boolean = false,
|
||||
val iconKey: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Activity(
|
||||
val id: String,
|
||||
val startedAt: String,
|
||||
val durationSeconds: Int,
|
||||
val distanceMeters: Double? = null,
|
||||
val elevationGainMeters: Double? = null,
|
||||
val estimatedKcal: Double? = null,
|
||||
val rpe: Double? = null,
|
||||
val source: String = "manual",
|
||||
val notes: String? = null,
|
||||
val activityType: ActivityType,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DailySummary(
|
||||
val gymKcal: Double = 0.0,
|
||||
val activityKcal: Double = 0.0,
|
||||
val activeKcal: Double = 0.0,
|
||||
val gymSessionCount: Int = 0,
|
||||
val activityCount: Int = 0,
|
||||
val distanceMeters: Double = 0.0,
|
||||
val bmrKcalPerDay: Double? = null,
|
||||
val phoneKcal: Double? = null,
|
||||
val steps: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Profile(
|
||||
val username: String,
|
||||
@@ -51,7 +92,114 @@ class GymApi(
|
||||
return client.json.decodeFromJsonElement(profile)
|
||||
}
|
||||
|
||||
/* ---------- Aktiviteter ---------- */
|
||||
|
||||
suspend fun activityTypes(): List<ActivityType> {
|
||||
val data = client.execute(
|
||||
"query{activityTypes{id key nameSv nameEn met category isDistanceBased isCardio iconKey}}",
|
||||
token = auth.bearerToken(),
|
||||
)
|
||||
return data["activityTypes"]!!.jsonArray.map { client.json.decodeFromJsonElement<ActivityType>(it) }
|
||||
}
|
||||
|
||||
suspend fun activities(limit: Int = 50): List<Activity> {
|
||||
val data = client.execute(
|
||||
"query(\$l:Int){activities(limit:\$l){$ACTIVITY_FIELDS}}",
|
||||
buildJsonObject { put("l", limit) },
|
||||
auth.bearerToken(),
|
||||
)
|
||||
return data["activities"]!!.jsonArray.map { client.json.decodeFromJsonElement<Activity>(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Logga aktivitet. Servern beräknar kcal (MET/tempo/RPE) om estimatedKcal
|
||||
* utelämnas. null = utelämna fält.
|
||||
*/
|
||||
suspend fun addActivity(
|
||||
activityTypeId: Int,
|
||||
startedAtIso: String,
|
||||
durationSeconds: Int,
|
||||
distanceMeters: Double? = null,
|
||||
elevationGainMeters: Double? = null,
|
||||
rpe: Double? = null,
|
||||
estimatedKcal: Double? = null,
|
||||
source: String? = null,
|
||||
routePolyline: String? = null,
|
||||
notes: String? = null,
|
||||
): Activity {
|
||||
val data = client.execute(
|
||||
"mutation(\$input: AddActivityInput!){addActivity(input:\$input){$ACTIVITY_FIELDS}}",
|
||||
buildJsonObject {
|
||||
put("input", buildJsonObject {
|
||||
put("activityTypeId", activityTypeId)
|
||||
put("startedAt", startedAtIso)
|
||||
put("durationSeconds", durationSeconds)
|
||||
distanceMeters?.let { put("distanceMeters", it) }
|
||||
elevationGainMeters?.let { put("elevationGainMeters", it) }
|
||||
rpe?.let { put("rpe", it) }
|
||||
estimatedKcal?.let { put("estimatedKcal", it) }
|
||||
source?.let { put("source", it) }
|
||||
routePolyline?.let { put("routePolyline", it) }
|
||||
notes?.let { put("notes", it) }
|
||||
})
|
||||
},
|
||||
auth.bearerToken(),
|
||||
)
|
||||
return client.json.decodeFromJsonElement(data["addActivity"]!!)
|
||||
}
|
||||
|
||||
/** null = rör ej, -1/"" = rensa (serverns konvention). */
|
||||
suspend fun updateActivity(
|
||||
id: String,
|
||||
activityTypeId: Int? = null,
|
||||
startedAtIso: String? = null,
|
||||
durationSeconds: Int? = null,
|
||||
distanceMeters: Double? = null,
|
||||
elevationGainMeters: Double? = null,
|
||||
rpe: Double? = null,
|
||||
notes: String? = null,
|
||||
): Activity {
|
||||
val data = client.execute(
|
||||
"mutation(\$input: UpdateActivityInput!){updateActivity(input:\$input){$ACTIVITY_FIELDS}}",
|
||||
buildJsonObject {
|
||||
put("input", buildJsonObject {
|
||||
put("id", id)
|
||||
activityTypeId?.let { put("activityTypeId", it) }
|
||||
startedAtIso?.let { put("startedAt", it) }
|
||||
durationSeconds?.let { put("durationSeconds", it) }
|
||||
distanceMeters?.let { put("distanceMeters", it) }
|
||||
elevationGainMeters?.let { put("elevationGainMeters", it) }
|
||||
rpe?.let { put("rpe", it) }
|
||||
notes?.let { put("notes", it) }
|
||||
})
|
||||
},
|
||||
auth.bearerToken(),
|
||||
)
|
||||
return client.json.decodeFromJsonElement(data["updateActivity"]!!)
|
||||
}
|
||||
|
||||
suspend fun deleteActivity(id: String) {
|
||||
client.execute(
|
||||
"mutation(\$id: UUID!){deleteActivity(id:\$id)}",
|
||||
buildJsonObject { put("id", id) },
|
||||
auth.bearerToken(),
|
||||
)
|
||||
}
|
||||
|
||||
/** Dagens sammanställning; [dayStartIso] = lokala midnatt som instant. */
|
||||
suspend fun dailySummary(dayStartIso: String): DailySummary {
|
||||
val data = client.execute(
|
||||
"query(\$d:DateTime){dailySummary(date:\$d){gymKcal activityKcal activeKcal gymSessionCount activityCount distanceMeters bmrKcalPerDay phoneKcal steps}}",
|
||||
buildJsonObject { put("d", dayStartIso) },
|
||||
auth.bearerToken(),
|
||||
)
|
||||
return client.json.decodeFromJsonElement(data["dailySummary"]!!)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTIVITY_FIELDS =
|
||||
"id startedAt durationSeconds distanceMeters elevationGainMeters estimatedKcal rpe source notes " +
|
||||
"activityType{id key nameSv nameEn met category isDistanceBased isCardio iconKey}"
|
||||
private const val MY_PROFILE =
|
||||
"query{myProfile{username displayName email bodyWeightKg heightCm birthYear sex}}"
|
||||
private const val MY_PROFILE_LEGACY =
|
||||
|
||||
Reference in New Issue
Block a user