Fix setup UX: remove Hz wording and make setup scrollable
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user