diff --git a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt index 1b411bd..4cf6e8d 100644 --- a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt +++ b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt @@ -535,8 +535,9 @@ fun SetupScreen( ) { Text("Save") } + } else { + Button(onClick = onStart, modifier = Modifier.weight(1f).height(52.dp)) { Text("Start") } } - Button(onClick = onStart, modifier = Modifier.weight(1f).height(52.dp)) { Text("Start") } } if (ui.error != null) Text(ui.error!!, color = if (ui.error!!.startsWith("Saved")) Color(0xFF72E39A) else MaterialTheme.colorScheme.error) Spacer(Modifier.height(8.dp)) diff --git a/app/src/main/java/com/mindmachine/mvp/session/FlashSequenceEditor.kt b/app/src/main/java/com/mindmachine/mvp/session/FlashSequenceEditor.kt index 4dce4e7..39bd401 100644 --- a/app/src/main/java/com/mindmachine/mvp/session/FlashSequenceEditor.kt +++ b/app/src/main/java/com/mindmachine/mvp/session/FlashSequenceEditor.kt @@ -6,7 +6,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add @@ -16,11 +18,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @Composable @@ -30,6 +34,8 @@ fun FlashSequenceEditor( modifier: Modifier = Modifier, ) { val rows = sequence.ifEmpty { defaultFlashSequence() } + val splitColWidth = 56.dp + val deleteColWidth = 40.dp Column( modifier = modifier @@ -45,24 +51,40 @@ fun FlashSequenceEditor( color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.72f), ) + // Table header row. + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Box(modifier = Modifier.width(splitColWidth), contentAlignment = Alignment.CenterStart) { + Text("Split", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface) + } + Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { + Text("Left", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface) + } + Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) { + Text("Right", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurface) + } + Box(modifier = Modifier.width(deleteColWidth)) + } + rows.forEachIndexed { index, row -> Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) { + Box(modifier = Modifier.width(splitColWidth), contentAlignment = Alignment.CenterStart) { Checkbox( checked = row.splitScreen, onCheckedChange = { checked -> onSequenceChanged(rows.toMutableList().also { it[index] = row.copy(splitScreen = checked) }) } ) - Text("Split screen", color = MaterialTheme.colorScheme.onSurface) } ColorCycleButton( - label = if (row.splitScreen) "Left" else "Color", color = row.leftColor, onClick = { onSequenceChanged(rows.toMutableList().also { @@ -74,7 +96,6 @@ fun FlashSequenceEditor( if (row.splitScreen) { ColorCycleButton( - label = "Right", color = row.rightColor, onClick = { onSequenceChanged(rows.toMutableList().also { @@ -90,7 +111,8 @@ fun FlashSequenceEditor( IconButton( onClick = { onSequenceChanged(rows.filterIndexed { i, _ -> i != index }.ifEmpty { defaultFlashSequence() }) - } + }, + modifier = Modifier.width(deleteColWidth), ) { Icon(Icons.Default.Delete, contentDescription = "Remove row", tint = MaterialTheme.colorScheme.onSurface) } @@ -109,19 +131,26 @@ fun FlashSequenceEditor( @Composable private fun ColorCycleButton( - label: String, color: FlashPaletteColor, onClick: () -> Unit, modifier: Modifier = Modifier, ) { - OutlinedButton(onClick = onClick, modifier = modifier) { + OutlinedButton( + onClick = onClick, + modifier = modifier.heightIn(min = 40.dp), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp), + ) { Box( modifier = Modifier .padding(end = 8.dp) .background(color.previewColor(), RoundedCornerShape(6.dp)) - .padding(horizontal = 10.dp, vertical = 10.dp) + .padding(horizontal = 10.dp, vertical = 8.dp) + ) + Text( + text = color.displayName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) - Text("$label: ${color.displayName}") } } diff --git a/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt b/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt index 1a3353c..06b895e 100644 --- a/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt +++ b/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt @@ -57,6 +57,7 @@ data class UiState( data class TimelineProgramSignature( val durationSec: Int, val curveGranularitySec: Int, + val flashSequence: List, val visualLeft: List, val visualRight: List, val audioLeft: List, @@ -66,6 +67,7 @@ data class TimelineProgramSignature( private fun timelineProgramSignature(timeline: TimelineEditorState): TimelineProgramSignature = TimelineProgramSignature( durationSec = timeline.durationSec, curveGranularitySec = timeline.curveGranularitySec, + flashSequence = timeline.flashSequence, visualLeft = timeline.visualLeft.points, visualRight = timeline.visualRight.points, audioLeft = timeline.audioLeft.points, diff --git a/app/src/test/java/com/mindmachine/mvp/DurationSliderRangeTest.kt b/app/src/test/java/com/mindmachine/mvp/DurationSliderRangeTest.kt index 92e4dad..129f7b7 100644 --- a/app/src/test/java/com/mindmachine/mvp/DurationSliderRangeTest.kt +++ b/app/src/test/java/com/mindmachine/mvp/DurationSliderRangeTest.kt @@ -6,9 +6,9 @@ import org.junit.Test class DurationSliderRangeTest { @Test - fun timeline_duration_clamps_to_one_minute_and_eight_hours() { + fun timeline_duration_clamps_to_one_minute_and_sixty_minutes() { assertEquals(60, TimelineEditorState.default(30).durationSec) - assertEquals(8 * 60 * 60, TimelineEditorState.default(9 * 60 * 60).durationSec) + assertEquals(60 * 60, TimelineEditorState.default(9 * 60 * 60).durationSec) } @Test diff --git a/app/src/test/java/com/mindmachine/mvp/SessionValidatorTest.kt b/app/src/test/java/com/mindmachine/mvp/SessionValidatorTest.kt index 0a6e327..2eca350 100644 --- a/app/src/test/java/com/mindmachine/mvp/SessionValidatorTest.kt +++ b/app/src/test/java/com/mindmachine/mvp/SessionValidatorTest.kt @@ -27,7 +27,7 @@ class SessionValidatorTest { @Test fun out_of_range_values_are_blocked() { - assertEquals("Duration must be 1 minute to 8 hours.", SessionValidator.validate(valid.copy(durationSec = 59))) + assertEquals("Duration must be 1 minute to 60 minutes.", SessionValidator.validate(valid.copy(durationSec = 59))) assertEquals("Flash interval must be 0.05 to 2.00 seconds.", SessionValidator.validate(valid.copy(flashIntervalMs = 49))) assertEquals("Carrier frequency must be between 200 and 1200 Hz.", SessionValidator.validate(valid.copy(carrierFrequencyHz = 0f))) assertEquals("Carrier frequency must be between 200 and 1200 Hz.", SessionValidator.validate(valid.copy(carrierFrequencyHz = 20001f)))