From 0f62741043c682c0906ced4fd09cad60bdcbd6a6 Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 24 Jul 2026 18:39:29 +0200 Subject: [PATCH] =?UTF-8?q?R=C3=A4tta=20loggade=20set:=20tryck=20p=C3=A5?= =?UTF-8?q?=20ett=20klart=20set=20f=C3=B6r=20att=20=C3=A4ndra=20eller=20ta?= =?UTF-8?q?=20bort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Samma steppers som vid loggning. Ändring på osynkat set bärs av den väntande add-opsen; synkat set får en update-op. Borttagning: lokalt direkt, remove-op mot servern när setet redan var synkat. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP --- .../fitnessdroid/data/GymRepository.kt | 17 +++ .../brassepc/fitnessdroid/data/SyncEngine.kt | 10 ++ .../fitnessdroid/ui/session/SessionScreen.kt | 124 +++++++++++++++++- .../ui/session/SessionViewModel.kt | 20 +++ 4 files changed, 169 insertions(+), 2 deletions(-) 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 ff4f0d4..ed939f2 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/data/GymRepository.kt @@ -124,6 +124,23 @@ class GymRepository( if (set.serverId != null) enqueue(OpKind.UPDATE_SET, set.id) } + suspend fun deleteSet(setId: Long) { + val set = db.sessionDao().set(setId) ?: return + db.sessionDao().deleteSet(setId) + // Osynkat set: den väntande ADD-opsen blir en no-op när raden är borta. + // Synkat set: ta bort på servern också (server-id:t i op:en, raden finns inte kvar). + set.serverId?.let { serverId -> + db.opDao().enqueue( + PendingOp( + kind = OpKind.REMOVE_SET, + targetLocalId = serverId.toLong(), + createdAtEpochMs = System.currentTimeMillis(), + ) + ) + sync.kick() + } + } + suspend fun completeSession(sessionId: Long) { val session = db.sessionDao().session(sessionId) ?: return db.sessionDao().updateSession(session.copy(status = "completed")) diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/data/SyncEngine.kt b/app/src/main/java/eu/brassepc/fitnessdroid/data/SyncEngine.kt index 74b4872..7bae1df 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/data/SyncEngine.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/data/SyncEngine.kt @@ -21,6 +21,8 @@ object OpKind { const val ADD_EXERCISE = "add_exercise" const val ADD_SET = "add_set" const val UPDATE_SET = "update_set" + /** targetLocalId bär server-id:t — den lokala raden är redan borta */ + const val REMOVE_SET = "remove_set" const val COMPLETE_SESSION = "complete_session" } @@ -169,6 +171,14 @@ class SyncEngine( } } + OpKind.REMOVE_SET -> { + client.execute( + "mutation(\$id: Int!){removeSessionSet(id:\$id)}", + buildJsonObject { put("id", op.targetLocalId.toInt()) }, + auth.bearerToken(), + ) + } + OpKind.COMPLETE_SESSION -> { val session = dao.session(op.targetLocalId) ?: return val sessionServerId = session.serverId diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionScreen.kt index e99dcea..e0f330f 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionScreen.kt @@ -66,6 +66,7 @@ fun SessionScreen( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() var confirmComplete by remember { mutableStateOf(false) } + var editTarget by remember { mutableStateOf?>(null) } // Passtimern (uppe till höger) tickar en gång i sekunden. var nowMs by remember { mutableStateOf(System.currentTimeMillis()) } @@ -138,6 +139,7 @@ fun SessionScreen( pageCount = uiState.pages.size, onDraft = viewModel::updateDraft, onLog = viewModel::logSet, + onEditSet = { page, set -> editTarget = page to set }, ) } FilledTonalButton( @@ -162,6 +164,22 @@ fun SessionScreen( } } + editTarget?.let { (page, set) -> + EditSetDialog( + page = page, + set = set, + onSave = { draft -> + viewModel.saveEditedSet(set.id, draft) + editTarget = null + }, + onDelete = { + viewModel.deleteSet(set.id) + editTarget = null + }, + onDismiss = { editTarget = null }, + ) + } + if (confirmComplete) { AlertDialog( onDismissRequest = { confirmComplete = false }, @@ -243,6 +261,7 @@ private fun ExercisePageContent( pageCount: Int, onDraft: (Long, (SetDraft) -> SetDraft) -> Unit, onLog: (Long) -> Unit, + onEditSet: (ExercisePage, LocalSet) -> Unit, ) { Column( modifier = Modifier @@ -263,7 +282,7 @@ private fun ExercisePageContent( ) } - page.doneSets.forEach { set -> DoneSetRow(set) } + page.doneSets.forEach { set -> DoneSetRow(set, onClick = { onEditSet(page, set) }) } Card { Column( @@ -356,8 +375,9 @@ private fun ExercisePageContent( } @Composable -private fun DoneSetRow(set: LocalSet) { +private fun DoneSetRow(set: LocalSet, onClick: () -> Unit) { Surface( + onClick = onClick, shape = MaterialTheme.shapes.medium, color = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth(), @@ -423,6 +443,106 @@ private fun Stepper(label: String, value: String, onMinus: () -> Unit, onPlus: ( } } +/** Rätta ett redan loggat set: samma steppers som vid loggning + ta bort. */ +@Composable +private fun EditSetDialog( + page: ExercisePage, + set: LocalSet, + onSave: (SetDraft) -> Unit, + onDelete: () -> Unit, + onDismiss: () -> Unit, +) { + var draft by remember { + mutableStateOf( + SetDraft( + weight = set.weight, + reps = set.reps, + durationSeconds = set.durationSeconds, + distanceMeters = set.distanceMeters, + rpe = set.rpe, + ) + ) + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Rätta set ${set.order}") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + if (page.type?.tracksWeight != false) { + Stepper( + label = "kg", + value = draft.weight?.compact() ?: "—", + onMinus = { draft = draft.copy(weight = ((draft.weight ?: 0.0) - 2.5).coerceAtLeast(0.0)) }, + onPlus = { draft = draft.copy(weight = (draft.weight ?: 0.0) + 2.5) }, + ) + } + if (page.type?.tracksReps != false) { + Stepper( + label = "reps", + value = draft.reps?.toString() ?: "—", + onMinus = { draft = draft.copy(reps = ((draft.reps ?: 1) - 1).coerceAtLeast(1)) }, + onPlus = { draft = draft.copy(reps = (draft.reps ?: 0) + 1) }, + ) + } + if (page.type?.tracksDuration == true) { + Stepper( + label = "sekunder", + value = draft.durationSeconds?.toString() ?: "—", + onMinus = { draft = draft.copy(durationSeconds = ((draft.durationSeconds ?: 15) - 15).coerceAtLeast(5)) }, + onPlus = { draft = draft.copy(durationSeconds = (draft.durationSeconds ?: 0) + 15) }, + ) + } + if (page.type?.tracksDistance == true) { + Stepper( + label = "meter", + value = draft.distanceMeters?.compact() ?: "—", + onMinus = { draft = draft.copy(distanceMeters = ((draft.distanceMeters ?: 100.0) - 100).coerceAtLeast(0.0)) }, + onPlus = { draft = draft.copy(distanceMeters = (draft.distanceMeters ?: 0.0) + 100) }, + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + "RPE", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + listOf(7.0, 8.0, 9.0, 10.0).forEach { rpe -> + val selected = draft.rpe == rpe + Surface( + onClick = { draft = draft.copy(rpe = if (selected) null else rpe) }, + shape = MaterialTheme.shapes.small, + color = if (selected) { + MaterialTheme.colorScheme.secondaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant + }, + ) { + Text( + rpe.compact(), + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + style = MaterialTheme.typography.labelLarge, + ) + } + } + } + TextButton(onClick = onDelete) { + Text("Ta bort setet", color = MaterialTheme.colorScheme.error) + } + } + }, + confirmButton = { + Button(onClick = { onSave(draft) }) { Text("Spara") } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text("Avbryt") } + }, + ) +} + /* ---------- Vilotimer, två presentationer ---------- */ @Composable diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionViewModel.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionViewModel.kt index 8780630..a720caf 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionViewModel.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/session/SessionViewModel.kt @@ -154,6 +154,26 @@ class SessionViewModel( fun adjustRest(delta: Int) = restTimer.adjust(delta) fun skipRest() = restTimer.dismiss() + /** Rätta ett redan loggat set (synkas som update, eller bärs av väntande add). */ + fun saveEditedSet(setId: Long, draft: SetDraft) { + val set = uiState.value.pages.flatMap { it.doneSets }.firstOrNull { it.id == setId } ?: return + viewModelScope.launch { + repo.updateSet( + set.copy( + weight = draft.weight, + reps = draft.reps, + durationSeconds = draft.durationSeconds, + distanceMeters = draft.distanceMeters, + rpe = draft.rpe, + ) + ) + } + } + + fun deleteSet(setId: Long) { + viewModelScope.launch { repo.deleteSet(setId) } + } + fun completeSession(onDone: () -> Unit) { viewModelScope.launch { val id = uiState.value.sessionId ?: sessionFlow.first()?.id ?: return@launch