diff --git a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt index a61d210..b955d0a 100644 --- a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt +++ b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt @@ -20,6 +20,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Button @@ -165,7 +169,7 @@ fun App(vm: MainViewModel = viewModel()) { Column(Modifier.padding(12.dp)) { Text(p.name, style = MaterialTheme.typography.titleMedium) Text(p.description) - Text("${p.defaultDurationSec / 60} min • ${p.visualPatternType} • ${p.binauralDifferenceHz} Hz") + Text("${p.defaultDurationSec / 60} min • ${p.visualPatternType} • binaural ${p.binauralDifferenceHz}") } } } @@ -241,8 +245,16 @@ fun SafetyScreen(onAck: () -> Unit, onHolder: () -> Unit) { fun SetupScreen(vm: MainViewModel, onStart: () -> Unit, onHolder: () -> Unit) { val ui by vm.ui.collectAsStateWithLifecycle() val cfg = ui.config + val scrollState = rememberScrollState() + val containerModifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .then(if (setupScreenUsesScrollableContainer()) Modifier.verticalScroll(scrollState) else Modifier) + .safeDrawingPadding() + .padding(horizontal = 16.dp, vertical = 12.dp) + Column( - Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp), + containerModifier, verticalArrangement = Arrangement.spacedBy(10.dp) ) { Text(ui.selectedPreset.name, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground) @@ -259,24 +271,30 @@ fun SetupScreen(vm: MainViewModel, onStart: () -> Unit, onHolder: () -> Unit) { ) } } - Text("Flash interval: ${"%.2f".format(flashIntervalMsToSeconds(cfg.flashIntervalMs))} s", color = MaterialTheme.colorScheme.onBackground) + Text(setupScreenFlashIntervalLabel(cfg.flashIntervalMs), color = MaterialTheme.colorScheme.onBackground) Slider( value = flashIntervalMsToSeconds(cfg.flashIntervalMs), onValueChange = { vm.setFlashIntervalMs(flashIntervalSecondsToMs(it)) }, valueRange = 0.05f..2.0f ) Text("Controls how long each frame is shown in the red/green/black pattern.", color = MaterialTheme.colorScheme.onBackground) - Text("Carrier ${cfg.carrierFrequencyHz.roundToInt()} Hz", color = MaterialTheme.colorScheme.onBackground) + Text("Carrier frequency: ${cfg.carrierFrequencyHz.roundToInt()}", color = MaterialTheme.colorScheme.onBackground) Slider(value = cfg.carrierFrequencyHz, onValueChange = vm::setCarrier, valueRange = 80f..400f) - Text("Difference ${cfg.binauralDifferenceHz} Hz", color = MaterialTheme.colorScheme.onBackground) + Text("Binaural difference: ${cfg.binauralDifferenceHz}", color = MaterialTheme.colorScheme.onBackground) Slider(value = cfg.binauralDifferenceHz, onValueChange = vm::setDifference, valueRange = 0.5f..20f) Text("Brightness recommendation: keep screen comfortable and avoid eye strain.", color = MaterialTheme.colorScheme.onBackground) TextButton(onClick = onHolder) { Text("Holder Guidance") } if (ui.error != null) Text(ui.error!!, color = MaterialTheme.colorScheme.error) Button(onClick = onStart, modifier = Modifier.fillMaxWidth().height(52.dp)) { Text("Start") } + Spacer(Modifier.height(8.dp)) } } +internal fun setupScreenFlashIntervalLabel(flashIntervalMs: Int): String = + "Flash interval: ${"%.2f".format(flashIntervalMsToSeconds(flashIntervalMs))} s" + +internal fun setupScreenUsesScrollableContainer(): Boolean = true + @Composable fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: () -> Unit) { val ui by vm.ui.collectAsStateWithLifecycle() diff --git a/app/src/main/java/com/mindmachine/mvp/session/SessionValidator.kt b/app/src/main/java/com/mindmachine/mvp/session/SessionValidator.kt index 47985ca..b39c59c 100644 --- a/app/src/main/java/com/mindmachine/mvp/session/SessionValidator.kt +++ b/app/src/main/java/com/mindmachine/mvp/session/SessionValidator.kt @@ -7,8 +7,8 @@ object SessionValidator { fun validate(config: SessionConfig, headsetAvailable: Boolean): String? { if (config.durationSec !in 60..(30 * 60)) return "Duration must be 1 to 30 minutes." if (config.flashIntervalMs !in 50..2000) return "Flash interval must be 0.05 to 2.00 seconds." - if (config.carrierFrequencyHz !in 80f..400f) return "Carrier frequency must be 80 to 400 Hz." - if (config.binauralDifferenceHz !in 0.5f..20f) return "Binaural difference must be 0.5 to 20 Hz." + if (config.carrierFrequencyHz !in 80f..400f) return "Carrier frequency must be between 80 and 400." + if (config.binauralDifferenceHz !in 0.5f..20f) return "Binaural difference must be between 0.5 and 20." if (config.mode != SessionMode.VISUAL_ONLY && !headsetAvailable) { return "Stereo headphones are required for binaural audio. Connect headphones or switch to Visual-only." } diff --git a/app/src/test/java/com/mindmachine/mvp/SessionValidatorTest.kt b/app/src/test/java/com/mindmachine/mvp/SessionValidatorTest.kt index ddb6321..1f33b69 100644 --- a/app/src/test/java/com/mindmachine/mvp/SessionValidatorTest.kt +++ b/app/src/test/java/com/mindmachine/mvp/SessionValidatorTest.kt @@ -40,7 +40,7 @@ class SessionValidatorTest { fun out_of_range_values_are_blocked() { assertEquals("Duration must be 1 to 30 minutes.", SessionValidator.validate(valid.copy(durationSec = 59), true)) assertEquals("Flash interval must be 0.05 to 2.00 seconds.", SessionValidator.validate(valid.copy(flashIntervalMs = 49), true)) - assertEquals("Carrier frequency must be 80 to 400 Hz.", SessionValidator.validate(valid.copy(carrierFrequencyHz = 79f), true)) - assertEquals("Binaural difference must be 0.5 to 20 Hz.", SessionValidator.validate(valid.copy(binauralDifferenceHz = 0.1f), true)) + assertEquals("Carrier frequency must be between 80 and 400.", SessionValidator.validate(valid.copy(carrierFrequencyHz = 79f), true)) + assertEquals("Binaural difference must be between 0.5 and 20.", SessionValidator.validate(valid.copy(binauralDifferenceHz = 0.1f), true)) } } diff --git a/app/src/test/java/com/mindmachine/mvp/SetupScreenUiLogicTest.kt b/app/src/test/java/com/mindmachine/mvp/SetupScreenUiLogicTest.kt new file mode 100644 index 0000000..bce303f --- /dev/null +++ b/app/src/test/java/com/mindmachine/mvp/SetupScreenUiLogicTest.kt @@ -0,0 +1,18 @@ +package com.mindmachine.mvp + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class SetupScreenUiLogicTest { + @Test + fun flash_interval_label_uses_seconds_text_source() { + assertEquals("Flash interval: 0.17 s", setupScreenFlashIntervalLabel(167)) + assertEquals("Flash interval: 2.00 s", setupScreenFlashIntervalLabel(2000)) + } + + @Test + fun setup_screen_layout_uses_scrollable_container() { + assertTrue(setupScreenUsesScrollableContainer()) + } +}