Implement Phase 6: undo/redo with command pattern

- UndoableAction interface with AddStrokeAction and DeleteStrokeAction
- UndoManager: undo/redo stacks (depth 50), canUndo/canRedo StateFlows
- EditorViewModel: stroke operations routed through UndoManager,
  visual callbacks sync canvas view without full DB reload
- Toolbar: undo/redo buttons with enabled state
- 9 unit tests for UndoManager

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 14:40:24 -07:00
parent 7cf779934d
commit 5eeedff464
9 changed files with 326 additions and 13 deletions

View File

@@ -0,0 +1,43 @@
package net.metacircular.engpad.undo
import net.metacircular.engpad.data.model.Stroke
import net.metacircular.engpad.data.repository.PageRepository
class AddStrokeAction(
private val stroke: Stroke,
private val repository: PageRepository,
private val onExecute: (Stroke) -> Unit,
private val onUndo: (Long) -> Unit,
) : UndoableAction {
override val description = "Add stroke"
private var insertedId: Long = 0
override suspend fun execute() {
insertedId = repository.addStroke(stroke)
onExecute(stroke.copy(id = insertedId))
}
override suspend fun undo() {
repository.deleteStroke(insertedId)
onUndo(insertedId)
}
}
class DeleteStrokeAction(
private val stroke: Stroke,
private val repository: PageRepository,
private val onExecute: (Long) -> Unit,
private val onUndo: (Stroke) -> Unit,
) : UndoableAction {
override val description = "Delete stroke"
override suspend fun execute() {
repository.deleteStroke(stroke.id)
onExecute(stroke.id)
}
override suspend fun undo() {
val id = repository.addStroke(stroke)
onUndo(stroke.copy(id = id))
}
}