Statistik: favoritövningar överst i PB-listan (stjärna på korten, återanvänder toggleFavoriteExercise)
All checks were successful
release / build-release (push) Successful in 4m31s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFvSx1pF9pymyEuK7MFkEG
This commit is contained in:
2026-09-06 22:13:09 +02:00
parent c2161869bb
commit 8f7cd9a419
3 changed files with 35 additions and 6 deletions

View File

@@ -53,7 +53,8 @@ ett rekord så öppnas ett ark med vikt, est. 1RM, datum, pass och anteckning, d
märkas som **Träning**, **Tävling** eller **Räknas ej** (fusk, felregistrerat, ej godkänt) märkas som **Träning**, **Tävling** eller **Räknas ej** (fusk, felregistrerat, ej godkänt)
med en kommentar. Under arket visas topplistan för samma övning × reps ("näst i tur"), med en kommentar. Under arket visas topplistan för samma övning × reps ("näst i tur"),
så ett bortplockat lyft kan väljas och återställas. Samma ark öppnas när man trycker på så ett bortplockat lyft kan väljas och återställas. Samma ark öppnas när man trycker på
ett set i passhistoriken. "Räknas ej" tas ur rekord, 1RM och PB-höjdpunkter på servern ett set i passhistoriken. Stjärnan på ett PB-kort gör övningen till **favorit** (samma
favoriter som övningsväljaren) så att den ligger överst i PB-listan i både app och webb. "Räknas ej" tas ur rekord, 1RM och PB-höjdpunkter på servern
(kod: `ui/common/LiftStatusSheet.kt`, API: `setLiftStatus`/`setSessionSetStatus`). (kod: `ui/common/LiftStatusSheet.kt`, API: `setLiftStatus`/`setSessionSetStatus`).
## Signeringsnyckeln ## Signeringsnyckeln

View File

@@ -302,7 +302,7 @@ class GymRepository(
/** PB-lista med detaljer per rekord. mode: all | competition | training. */ /** PB-lista med detaljer per rekord. mode: all | competition | training. */
suspend fun personalBestsDetailed(mode: String = "all"): List<PbEntry> { suspend fun personalBestsDetailed(mode: String = "all"): List<PbEntry> {
val pb = client.execute( val pb = client.execute(
"query(\$m:String){personalBests(mode:\$m){exerciseTypeId exerciseTypeName repRecords{reps weight date liftId notes status statusNote sessionName estimated1Rm}}}", "query(\$m:String){personalBests(mode:\$m){exerciseTypeId exerciseTypeName isFavorite repRecords{reps weight date liftId notes status statusNote sessionName estimated1Rm}}}",
buildJsonObject { put("m", mode) }, buildJsonObject { put("m", mode) },
auth.bearerToken(), auth.bearerToken(),
) )
@@ -311,6 +311,7 @@ class GymRepository(
PbEntry( PbEntry(
exerciseName = po["exerciseTypeName"]?.jsonPrimitive?.contentOrNull() ?: "Övning", exerciseName = po["exerciseTypeName"]?.jsonPrimitive?.contentOrNull() ?: "Övning",
exerciseTypeId = po["exerciseTypeId"]?.jsonPrimitive?.intOrNull ?: 0, exerciseTypeId = po["exerciseTypeId"]?.jsonPrimitive?.intOrNull ?: 0,
isFavorite = po["isFavorite"]?.jsonPrimitive?.booleanOrNull ?: false,
records = po["repRecords"]!!.jsonArray.mapNotNull { r -> records = po["repRecords"]!!.jsonArray.mapNotNull { r ->
val ro = r.jsonObject val ro = r.jsonObject
PbRecord( PbRecord(
@@ -326,7 +327,7 @@ class GymRepository(
) )
}.sortedBy { it.reps }, }.sortedBy { it.reps },
) )
}.sortedBy { it.exerciseName } }.sortedWith(compareByDescending<PbEntry> { it.isFavorite }.thenBy { it.exerciseName })
} }
/** Topplista för övning × reps, alla statusar (så bortplockade kan återställas). */ /** Topplista för övning × reps, alla statusar (så bortplockade kan återställas). */
@@ -944,7 +945,13 @@ data class PbRecord(
val sessionName: String? = null, val sessionName: String? = null,
val est1Rm: Double = 0.0, val est1Rm: Double = 0.0,
) )
data class PbEntry(val exerciseName: String, val records: List<PbRecord>, val exerciseTypeId: Int = 0) data class PbEntry(
val exerciseName: String,
val records: List<PbRecord>,
val exerciseTypeId: Int = 0,
/** Favoritövning (samma FavoriteExercise som övningsväljaren) ligger överst i PB-listan. */
val isFavorite: Boolean = false,
)
/** Ett lyft med allt PB-arket behöver (pbCandidates / liftDetail). */ /** Ett lyft med allt PB-arket behöver (pbCandidates / liftDetail). */
data class LiftDetail( data class LiftDetail(

View File

@@ -5,6 +5,8 @@ import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.StarOutline
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -145,6 +147,15 @@ class StatsViewModel(
} }
} }
/** Favorit av/på för en övning: optimistisk omsortering, servern uppdateras i bakgrunden. */
fun togglePbFavorite(entry: eu.brassepc.fitnessdroid.data.PbEntry) {
val next = !entry.isFavorite
pbs.value = pbs.value
.map { if (it.exerciseTypeId == entry.exerciseTypeId) it.copy(isFavorite = next) else it }
.sortedWith(compareByDescending<eu.brassepc.fitnessdroid.data.PbEntry> { it.isFavorite }.thenBy { it.exerciseName })
repo.toggleFavoriteExercise(entry.exerciseTypeId, next)
}
companion object { companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
@@ -408,8 +419,18 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F
} }
items(pbs, key = { "${it.exerciseTypeId}-${it.exerciseName}" }) { pb -> items(pbs, key = { "${it.exerciseTypeId}-${it.exerciseName}" }) { pb ->
Card(modifier = Modifier.fillMaxWidth()) { Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(14.dp)) { Column(modifier = Modifier.padding(start = 14.dp, end = 6.dp, top = 4.dp, bottom = 14.dp)) {
Text(pb.exerciseName, style = MaterialTheme.typography.titleSmall) Row(verticalAlignment = Alignment.CenterVertically) {
Text(pb.exerciseName, style = MaterialTheme.typography.titleSmall, modifier = Modifier.weight(1f))
// Favorit = ligger överst (samma favoriter som övningsväljaren)
androidx.compose.material3.IconButton(onClick = { viewModel.togglePbFavorite(pb) }) {
androidx.compose.material3.Icon(
if (pb.isFavorite) Icons.Filled.Star else Icons.Outlined.StarOutline,
contentDescription = if (pb.isFavorite) "Ta bort favorit" else "Gör till favorit",
tint = if (pb.isFavorite) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Row( Row(
modifier = Modifier modifier = Modifier
.padding(top = 6.dp) .padding(top = 6.dp)