Fix setup UX: remove Hz wording and make setup scrollable

This commit is contained in:
Tretzi
2026-03-10 23:42:55 -05:00
parent e5117d2346
commit fecd29ac3d
4 changed files with 45 additions and 9 deletions

View File

@@ -20,6 +20,10 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -165,7 +169,7 @@ fun App(vm: MainViewModel = viewModel()) {
Column(Modifier.padding(12.dp)) { Column(Modifier.padding(12.dp)) {
Text(p.name, style = MaterialTheme.typography.titleMedium) Text(p.name, style = MaterialTheme.typography.titleMedium)
Text(p.description) 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) { fun SetupScreen(vm: MainViewModel, onStart: () -> Unit, onHolder: () -> Unit) {
val ui by vm.ui.collectAsStateWithLifecycle() val ui by vm.ui.collectAsStateWithLifecycle()
val cfg = ui.config 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( Column(
Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp), containerModifier,
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text(ui.selectedPreset.name, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground) 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( Slider(
value = flashIntervalMsToSeconds(cfg.flashIntervalMs), value = flashIntervalMsToSeconds(cfg.flashIntervalMs),
onValueChange = { vm.setFlashIntervalMs(flashIntervalSecondsToMs(it)) }, onValueChange = { vm.setFlashIntervalMs(flashIntervalSecondsToMs(it)) },
valueRange = 0.05f..2.0f valueRange = 0.05f..2.0f
) )
Text("Controls how long each frame is shown in the red/green/black pattern.", color = MaterialTheme.colorScheme.onBackground) 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) 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) 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) Text("Brightness recommendation: keep screen comfortable and avoid eye strain.", color = MaterialTheme.colorScheme.onBackground)
TextButton(onClick = onHolder) { Text("Holder Guidance") } TextButton(onClick = onHolder) { Text("Holder Guidance") }
if (ui.error != null) Text(ui.error!!, color = MaterialTheme.colorScheme.error) if (ui.error != null) Text(ui.error!!, color = MaterialTheme.colorScheme.error)
Button(onClick = onStart, modifier = Modifier.fillMaxWidth().height(52.dp)) { Text("Start") } 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 @Composable
fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: () -> Unit) { fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: () -> Unit) {
val ui by vm.ui.collectAsStateWithLifecycle() val ui by vm.ui.collectAsStateWithLifecycle()

View File

@@ -7,8 +7,8 @@ object SessionValidator {
fun validate(config: SessionConfig, headsetAvailable: Boolean): String? { fun validate(config: SessionConfig, headsetAvailable: Boolean): String? {
if (config.durationSec !in 60..(30 * 60)) return "Duration must be 1 to 30 minutes." 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.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.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 0.5 to 20 Hz." if (config.binauralDifferenceHz !in 0.5f..20f) return "Binaural difference must be between 0.5 and 20."
if (config.mode != SessionMode.VISUAL_ONLY && !headsetAvailable) { if (config.mode != SessionMode.VISUAL_ONLY && !headsetAvailable) {
return "Stereo headphones are required for binaural audio. Connect headphones or switch to Visual-only." return "Stereo headphones are required for binaural audio. Connect headphones or switch to Visual-only."
} }

View File

@@ -40,7 +40,7 @@ class SessionValidatorTest {
fun out_of_range_values_are_blocked() { fun out_of_range_values_are_blocked() {
assertEquals("Duration must be 1 to 30 minutes.", SessionValidator.validate(valid.copy(durationSec = 59), true)) 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("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("Carrier frequency must be between 80 and 400.", 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("Binaural difference must be between 0.5 and 20.", SessionValidator.validate(valid.copy(binauralDifferenceHz = 0.1f), true))
} }
} }

View File

@@ -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())
}
}