Scorekeeper: show an entered 0, tap a past score to edit it
Zeros are now stored so a typed 0 shows as 0 while a blank cell stays blank. Tapping a score in an earlier round opens that round for correction with the cursor in the tapped cell, instead of needing the row number first. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
26ae3fecaa
commit
b98d464954
@@ -21,9 +21,9 @@ data class ScorePlayer(val id: Int, val name: String = "")
|
|||||||
/**
|
/**
|
||||||
* What each player scored in one round, keyed by [ScorePlayer.id].
|
* What each player scored in one round, keyed by [ScorePlayer.id].
|
||||||
*
|
*
|
||||||
* A player absent from the map scored nothing, which is the same as zero — so
|
* A player absent from the map had nothing entered, which counts as zero in the
|
||||||
* a round where only one person scored carries one entry rather than a column
|
* totals — so a round where only one person scored carries one entry rather
|
||||||
* of noughts.
|
* than a column of noughts. A 0 that was typed is kept, so it can be shown.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ScoreRound(val deltas: Map<Int, Int> = emptyMap()) {
|
data class ScoreRound(val deltas: Map<Int, Int> = emptyMap()) {
|
||||||
@@ -114,12 +114,12 @@ data class ScoreSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A round holding only what is worth storing: entries for players who are
|
* A round holding only entries for players who are still at the table.
|
||||||
* still at the table, and only where somebody actually scored.
|
* Zeros stay: a typed 0 is different from a cell left blank.
|
||||||
*/
|
*/
|
||||||
private fun roundOf(deltas: Map<Int, Int>): ScoreRound {
|
private fun roundOf(deltas: Map<Int, Int>): ScoreRound {
|
||||||
val ids = players.map { it.id }.toSet()
|
val ids = players.map { it.id }.toSet()
|
||||||
return ScoreRound(deltas.filterKeys { it in ids }.filterValues { it != 0 })
|
return ScoreRound(deltas.filterKeys { it in ids })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Drops points recorded against anybody no longer at the table. */
|
/** Drops points recorded against anybody no longer at the table. */
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -193,9 +195,21 @@ private fun Sheet(modifier: Modifier, sheet: ScoreSheet, onChange: (ScoreSheet)
|
|||||||
var renaming by remember { mutableStateOf(-1) }
|
var renaming by remember { mutableStateOf(-1) }
|
||||||
// Which cell the ± button flips: the last one typed into.
|
// Which cell the ± button flips: the last one typed into.
|
||||||
var lastTyped by remember { mutableStateOf(-1) }
|
var lastTyped by remember { mutableStateOf(-1) }
|
||||||
|
// The cell to put the cursor in when a round is opened by tapping it.
|
||||||
|
var focusPlayer by remember { mutableStateOf(-1) }
|
||||||
|
|
||||||
val editing = if (correcting >= 0) correction else pending
|
val editing = if (correcting >= 0) correction else pending
|
||||||
|
|
||||||
|
// Opens a round for correction. Both the round number and any of its cells
|
||||||
|
// come through here, so tapping a score edits that score directly.
|
||||||
|
val startCorrection: (Int, Int) -> Unit = { index, focus ->
|
||||||
|
correcting = index
|
||||||
|
correction.clear()
|
||||||
|
sheet.rounds[index].deltas.forEach { (id, points) -> correction[id] = points.toString() }
|
||||||
|
lastTyped = -1
|
||||||
|
focusPlayer = focus
|
||||||
|
}
|
||||||
|
|
||||||
// Keep the row being filled in in view when a round is written down.
|
// Keep the row being filled in in view when a round is written down.
|
||||||
LaunchedEffect(sheet.rounds.size) { rows.animateScrollTo(rows.maxValue) }
|
LaunchedEffect(sheet.rounds.size) { rows.animateScrollTo(rows.maxValue) }
|
||||||
|
|
||||||
@@ -264,15 +278,7 @@ private fun Sheet(modifier: Modifier, sheet: ScoreSheet, onChange: (ScoreSheet)
|
|||||||
if (underCorrection) {
|
if (underCorrection) {
|
||||||
correcting = -1
|
correcting = -1
|
||||||
} else {
|
} else {
|
||||||
correcting = index
|
startCorrection(index, -1)
|
||||||
correction.clear()
|
|
||||||
sheet.players.forEach { player ->
|
|
||||||
val points = round.delta(player.id)
|
|
||||||
if (points != 0) {
|
|
||||||
correction[player.id] = points.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastTyped = -1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -282,13 +288,17 @@ private fun Sheet(modifier: Modifier, sheet: ScoreSheet, onChange: (ScoreSheet)
|
|||||||
if (underCorrection) {
|
if (underCorrection) {
|
||||||
PointsField(
|
PointsField(
|
||||||
text = correction[player.id].orEmpty(),
|
text = correction[player.id].orEmpty(),
|
||||||
|
autoFocus = focusPlayer == player.id,
|
||||||
onText = {
|
onText = {
|
||||||
correction[player.id] = it
|
correction[player.id] = it
|
||||||
lastTyped = player.id
|
lastTyped = player.id
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PointsCell(text = signed(round.delta(player.id)))
|
PointsCell(
|
||||||
|
text = signed(round.deltas[player.id]),
|
||||||
|
onClick = { startCorrection(index, player.id) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,17 +368,20 @@ private fun Sheet(modifier: Modifier, sheet: ScoreSheet, onChange: (ScoreSheet)
|
|||||||
pending.clear()
|
pending.clear()
|
||||||
}
|
}
|
||||||
lastTyped = -1
|
lastTyped = -1
|
||||||
|
focusPlayer = -1
|
||||||
},
|
},
|
||||||
onDelete = {
|
onDelete = {
|
||||||
onChange(sheet.withoutRound(correcting))
|
onChange(sheet.withoutRound(correcting))
|
||||||
correcting = -1
|
correcting = -1
|
||||||
correction.clear()
|
correction.clear()
|
||||||
lastTyped = -1
|
lastTyped = -1
|
||||||
|
focusPlayer = -1
|
||||||
},
|
},
|
||||||
onCancel = {
|
onCancel = {
|
||||||
correcting = -1
|
correcting = -1
|
||||||
correction.clear()
|
correction.clear()
|
||||||
lastTyped = -1
|
lastTyped = -1
|
||||||
|
focusPlayer = -1
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -583,11 +596,12 @@ private fun RoundLabel(text: String, selected: Boolean, onClick: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PointsCell(text: String, muted: Boolean = false) {
|
private fun PointsCell(text: String, muted: Boolean = false, onClick: (() -> Unit)? = null) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.border(Dp.Hairline, MaterialTheme.colorScheme.outlineVariant),
|
.border(Dp.Hairline, MaterialTheme.colorScheme.outlineVariant)
|
||||||
|
.then(if (onClick != null) Modifier.clickable { onClick() } else Modifier),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -603,9 +617,11 @@ private fun PointsCell(text: String, muted: Boolean = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A cell being typed into. Blank means nothing scored, which is the same as nought. */
|
/** A cell being typed into. Blank means nothing entered; a typed 0 is kept and shown. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun PointsField(text: String, onText: (String) -> Unit) {
|
private fun PointsField(text: String, onText: (String) -> Unit, autoFocus: Boolean = false) {
|
||||||
|
val focus = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(autoFocus) { if (autoFocus) focus.requestFocus() }
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = text,
|
value = text,
|
||||||
onValueChange = { onText(sanitiseAmount(it)) },
|
onValueChange = { onText(sanitiseAmount(it)) },
|
||||||
@@ -616,7 +632,7 @@ private fun PointsField(text: String, onText: (String) -> Unit) {
|
|||||||
),
|
),
|
||||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize().focusRequester(focus),
|
||||||
decorationBox = { field ->
|
decorationBox = { field ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -758,14 +774,13 @@ private fun flipSign(text: String): String {
|
|||||||
return (-value).toString()
|
return (-value).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The typed cells as points. Blanks, a lone minus and noughts all drop out. */
|
/** The typed cells as points. Blanks and a lone minus drop out; a typed 0 stays. */
|
||||||
private fun amountsAsPoints(amounts: Map<Int, String>): Map<Int, Int> =
|
private fun amountsAsPoints(amounts: Map<Int, String>): Map<Int, Int> =
|
||||||
amounts.mapNotNull { (id, text) -> text.toIntOrNull()?.takeIf { it != 0 }?.let { id to it } }
|
amounts.mapNotNull { (id, text) -> text.toIntOrNull()?.let { id to it } }.toMap()
|
||||||
.toMap()
|
|
||||||
|
|
||||||
/** Nothing scored shows as an empty cell rather than a nought in every column. */
|
/** Not entered shows as an empty cell; an entered 0 shows as 0. */
|
||||||
private fun signed(points: Int): String = when {
|
private fun signed(points: Int?): String = when {
|
||||||
|
points == null -> ""
|
||||||
points > 0 -> "+$points"
|
points > 0 -> "+$points"
|
||||||
points < 0 -> points.toString()
|
else -> points.toString()
|
||||||
else -> ""
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,4 +182,13 @@ class ScoreSheetTest {
|
|||||||
assertEquals(0, sheet.totals()[sheet.players[0].id])
|
assertEquals(0, sheet.totals()[sheet.players[0].id])
|
||||||
assertEquals(4, sheet.totals()[sheet.players[1].id])
|
assertEquals(4, sheet.totals()[sheet.players[1].id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a zero that was entered is kept, unlike a cell left blank`() {
|
||||||
|
val sheet = sheetOf("Alice", "Bob").let { it.withRound(mapOf(it.players[0].id to 0)) }
|
||||||
|
val round = sheet.rounds.first()
|
||||||
|
assertEquals(0, round.deltas[sheet.players[0].id])
|
||||||
|
assertEquals(null, round.deltas[sheet.players[1].id])
|
||||||
|
assertEquals(0, sheet.total(sheet.players[0].id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user