diff --git a/README.md b/README.md index c88e4e3..3144b88 100644 --- a/README.md +++ b/README.md @@ -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) 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å -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`). ## Signeringsnyckeln 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 43bc19b..c569bc2 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt @@ -302,7 +302,7 @@ class GymRepository( /** PB-lista med detaljer per rekord. mode: all | competition | training. */ suspend fun personalBestsDetailed(mode: String = "all"): List { 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) }, auth.bearerToken(), ) @@ -311,6 +311,7 @@ class GymRepository( PbEntry( exerciseName = po["exerciseTypeName"]?.jsonPrimitive?.contentOrNull() ?: "Övning", exerciseTypeId = po["exerciseTypeId"]?.jsonPrimitive?.intOrNull ?: 0, + isFavorite = po["isFavorite"]?.jsonPrimitive?.booleanOrNull ?: false, records = po["repRecords"]!!.jsonArray.mapNotNull { r -> val ro = r.jsonObject PbRecord( @@ -326,7 +327,7 @@ class GymRepository( ) }.sortedBy { it.reps }, ) - }.sortedBy { it.exerciseName } + }.sortedWith(compareByDescending { it.isFavorite }.thenBy { it.exerciseName }) } /** Topplista för övning × reps, alla statusar (så bortplockade kan återställas). */ @@ -944,7 +945,13 @@ data class PbRecord( val sessionName: String? = null, val est1Rm: Double = 0.0, ) -data class PbEntry(val exerciseName: String, val records: List, val exerciseTypeId: Int = 0) +data class PbEntry( + val exerciseName: String, + val records: List, + 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). */ data class LiftDetail( diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt index 2a68a54..1fc04a4 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons 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.Box 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 { it.isFavorite }.thenBy { it.exerciseName }) + repo.toggleFavoriteExercise(entry.exerciseTypeId, next) + } + companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { @@ -408,8 +419,18 @@ fun StatsScreen(viewModel: StatsViewModel = viewModel(factory = StatsViewModel.F } items(pbs, key = { "${it.exerciseTypeId}-${it.exerciseName}" }) { pb -> Card(modifier = Modifier.fillMaxWidth()) { - Column(modifier = Modifier.padding(14.dp)) { - Text(pb.exerciseName, style = MaterialTheme.typography.titleSmall) + Column(modifier = Modifier.padding(start = 14.dp, end = 6.dp, top = 4.dp, bottom = 14.dp)) { + 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( modifier = Modifier .padding(top = 6.dp)