Rätta loggade set: tryck på ett klart set för att ändra eller ta bort
All checks were successful
release / build-release (push) Successful in 4m46s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kf4MAbZ3c3B4Xy5XfuGsCP
This commit is contained in:
2026-07-24 18:39:29 +02:00
parent 93a1aa249a
commit 0f62741043
4 changed files with 169 additions and 2 deletions

View File

@@ -124,6 +124,23 @@ class GymRepository(
if (set.serverId != null) enqueue(OpKind.UPDATE_SET, set.id) 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) { suspend fun completeSession(sessionId: Long) {
val session = db.sessionDao().session(sessionId) ?: return val session = db.sessionDao().session(sessionId) ?: return
db.sessionDao().updateSession(session.copy(status = "completed")) db.sessionDao().updateSession(session.copy(status = "completed"))

View File

@@ -21,6 +21,8 @@ object OpKind {
const val ADD_EXERCISE = "add_exercise" const val ADD_EXERCISE = "add_exercise"
const val ADD_SET = "add_set" const val ADD_SET = "add_set"
const val UPDATE_SET = "update_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" 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 -> { OpKind.COMPLETE_SESSION -> {
val session = dao.session(op.targetLocalId) ?: return val session = dao.session(op.targetLocalId) ?: return
val sessionServerId = session.serverId val sessionServerId = session.serverId

View File

@@ -66,6 +66,7 @@ fun SessionScreen(
) { ) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var confirmComplete by remember { mutableStateOf(false) } var confirmComplete by remember { mutableStateOf(false) }
var editTarget by remember { mutableStateOf<Pair<ExercisePage, LocalSet>?>(null) }
// Passtimern (uppe till höger) tickar en gång i sekunden. // Passtimern (uppe till höger) tickar en gång i sekunden.
var nowMs by remember { mutableStateOf(System.currentTimeMillis()) } var nowMs by remember { mutableStateOf(System.currentTimeMillis()) }
@@ -138,6 +139,7 @@ fun SessionScreen(
pageCount = uiState.pages.size, pageCount = uiState.pages.size,
onDraft = viewModel::updateDraft, onDraft = viewModel::updateDraft,
onLog = viewModel::logSet, onLog = viewModel::logSet,
onEditSet = { page, set -> editTarget = page to set },
) )
} }
FilledTonalButton( 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) { if (confirmComplete) {
AlertDialog( AlertDialog(
onDismissRequest = { confirmComplete = false }, onDismissRequest = { confirmComplete = false },
@@ -243,6 +261,7 @@ private fun ExercisePageContent(
pageCount: Int, pageCount: Int,
onDraft: (Long, (SetDraft) -> SetDraft) -> Unit, onDraft: (Long, (SetDraft) -> SetDraft) -> Unit,
onLog: (Long) -> Unit, onLog: (Long) -> Unit,
onEditSet: (ExercisePage, LocalSet) -> Unit,
) { ) {
Column( Column(
modifier = Modifier 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 { Card {
Column( Column(
@@ -356,8 +375,9 @@ private fun ExercisePageContent(
} }
@Composable @Composable
private fun DoneSetRow(set: LocalSet) { private fun DoneSetRow(set: LocalSet, onClick: () -> Unit) {
Surface( Surface(
onClick = onClick,
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
modifier = Modifier.fillMaxWidth(), 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 ---------- */ /* ---------- Vilotimer, två presentationer ---------- */
@Composable @Composable

View File

@@ -154,6 +154,26 @@ class SessionViewModel(
fun adjustRest(delta: Int) = restTimer.adjust(delta) fun adjustRest(delta: Int) = restTimer.adjust(delta)
fun skipRest() = restTimer.dismiss() 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) { fun completeSession(onDone: () -> Unit) {
viewModelScope.launch { viewModelScope.launch {
val id = uiState.value.sessionId ?: sessionFlow.first()?.id ?: return@launch val id = uiState.value.sessionId ?: sessionFlow.first()?.id ?: return@launch