package com.mindmachine.mvp import android.app.Activity import android.content.pm.ActivityInfo import android.os.Bundle import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.background import com.mindmachine.mvp.session.DurationSliderCard import com.mindmachine.mvp.session.SetupTimelineEditor import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding 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.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.mindmachine.mvp.audio.BinauralAudioEngine import com.mindmachine.mvp.audio.HeadsetMonitor import com.mindmachine.mvp.data.SettingsRepository import com.mindmachine.mvp.data.UserProgramRepository import com.mindmachine.mvp.domain.RuntimeState import com.mindmachine.mvp.session.FlashColor import com.mindmachine.mvp.session.MainViewModel import com.mindmachine.mvp.session.SplitFlashFrame import com.mindmachine.mvp.session.SplitFlashSequencer import com.mindmachine.mvp.session.flashIntervalMsToSeconds import com.mindmachine.mvp.session.flashIntervalSecondsToMs import com.mindmachine.mvp.session.formatDuration import com.mindmachine.mvp.session.sessionProgressFraction import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault import com.mindmachine.mvp.session.shouldUseImmersiveFullscreen import kotlinx.coroutines.delay import kotlin.math.roundToInt import java.util.Locale private const val SESSION_ENTRY_HOME = "home" private const val SESSION_ENTRY_SETUP = "setup" private val MindMachineColors = darkColorScheme( primary = Color(0xFF8AB4FF), onPrimary = Color(0xFF001B3D), primaryContainer = Color(0xFF004689), onPrimaryContainer = Color(0xFFD7E3FF), secondary = Color(0xFFFFD166), onSecondary = Color(0xFF2E2300), background = Color(0xFF05070D), onBackground = Color(0xFFF3F6FF), surface = Color(0xFF121621), onSurface = Color(0xFFF3F6FF), error = Color(0xFFFF6B6B), onError = Color(0xFF3A0002) ) class MainActivity : ComponentActivity() { private val vm: MainViewModel by viewModels { MainViewModel.Factory( SettingsRepository(applicationContext), UserProgramRepository(applicationContext), HeadsetMonitor(applicationContext), BinauralAudioEngine(), ) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme(colorScheme = MindMachineColors) { App(vm) } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun App(vm: MainViewModel = viewModel()) { val nav = rememberNavController() val ui by vm.ui.collectAsStateWithLifecycle() val lifecycle = LocalLifecycleOwner.current.lifecycle val activity = LocalContext.current as? Activity DisposableEffect(lifecycle, activity, ui.runtimeState) { val observer = LifecycleEventObserver { _, event -> val appActuallyBackgrounded = activity?.isChangingConfigurations != true if ( event == Lifecycle.Event.ON_STOP && ui.runtimeState == RuntimeState.RUNNING && appActuallyBackgrounded ) { vm.pause("Session paused because app moved to background.") } } lifecycle.addObserver(observer) onDispose { lifecycle.removeObserver(observer) } } if (!ui.settingsInitialized) { Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) ) return } val startRoute = if (ui.settings.safetyAcknowledged) "home" else "welcome" NavHost(navController = nav, startDestination = startRoute, modifier = Modifier.background(MaterialTheme.colorScheme.background)) { composable("welcome") { SimpleScreen("MindMachine", "Blinking light + binaural audio. Stereo headphones required for binaural mode. Not a medical device.") { Button(onClick = { nav.navigate("safety") }) { Text("Continue") } } } composable("safety") { SafetyScreen( onAck = { vm.acknowledgeSafety() nav.navigate("home") { popUpTo(0) } }, onHolder = { nav.navigate("holder") } ) } composable("home") { var confirmDeleteProgramId by remember { mutableStateOf(null) } var confirmDeleteProgramName by remember { mutableStateOf("") } Column( Modifier .fillMaxSize() .safeDrawingPadding() ) { TopAppBar(title = { Text("MindMachine") }, actions = { IconButton( onClick = { vm.createNewProgramDraft() nav.navigate("setup") } ) { Icon(Icons.Filled.Add, contentDescription = "Add") } IconButton(onClick = { nav.navigate("settings") }) { Icon(Icons.Filled.Settings, contentDescription = "Settings") } }) LazyColumn(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { item { TextButton(onClick = { nav.navigate("holder") }) { Text("User Guidance/Disclaimer") } } items(ui.presets) { p -> Card( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.onSurface ), modifier = Modifier.fillMaxWidth().clickable { vm.choosePreset(p.id) nav.navigate("setup") }.padding(4.dp) ) { Row( modifier = Modifier.fillMaxWidth().padding(12.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { Column(Modifier.weight(1f)) { Text(p.name, style = MaterialTheme.typography.titleMedium) Text(p.description) Text("${formatDuration(p.defaultDurationSec)} • ${p.visualPatternType} • binaural ${formatMaxTwoDecimals(p.binauralDifferenceHz)}") } Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { IconButton( onClick = { val started = vm.startSessionFromProgramSelection(p.id) if (started) { nav.navigate("active/$SESSION_ENTRY_HOME") } }, modifier = Modifier.size(48.dp) ) { Icon( imageVector = Icons.Filled.PlayArrow, contentDescription = "Play ${p.name}", tint = Color(0xFF4CAF50), modifier = Modifier.size(38.dp) ) } Box( modifier = Modifier .size(24.dp) .clickable { confirmDeleteProgramId = p.id confirmDeleteProgramName = p.name }, contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Filled.Delete, contentDescription = "Delete ${p.name}", tint = Color(0xFFFF5252), modifier = Modifier.fillMaxSize() ) } } } } } } } if (confirmDeleteProgramId != null) { AlertDialog( onDismissRequest = { confirmDeleteProgramId = null confirmDeleteProgramName = "" }, title = { Text("Delete program?") }, text = { Text("Delete \"$confirmDeleteProgramName\"?") }, confirmButton = { TextButton(onClick = { confirmDeleteProgramId?.let(vm::deleteProgram) confirmDeleteProgramId = null confirmDeleteProgramName = "" }) { Text("Delete", color = Color(0xFFFF5252)) } }, dismissButton = { TextButton(onClick = { confirmDeleteProgramId = null confirmDeleteProgramName = "" }) { Text("Cancel") } } ) } } composable("setup") { SetupScreen( vm = vm, onStart = { val started = vm.startSession() if (started) { nav.navigate("active/$SESSION_ENTRY_SETUP") } }, ) } composable( route = "active/{entryPoint}", arguments = listOf(navArgument("entryPoint") { type = NavType.StringType }) ) { backStackEntry -> val entryPoint = backStackEntry.arguments?.getString("entryPoint") ?: SESSION_ENTRY_SETUP ActiveSessionScreen( vm = vm, onFinish = { nav.navigate(entryPoint) { popUpTo("active/$entryPoint") { inclusive = true } launchSingleTop = true } }, onBackToEntry = { vm.stop() nav.navigate(entryPoint) { popUpTo("active/$entryPoint") { inclusive = true } launchSingleTop = true } } ) } composable("settings") { SettingsScreen(vm) } composable("holder") { PositioningScreen() } } } @Composable fun SimpleScreen(title: String, subtitle: String, actions: @Composable ColumnScope.() -> Unit) { Column( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .safeDrawingPadding() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { Text(title, style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onBackground) Text(subtitle, color = MaterialTheme.colorScheme.onBackground) actions() } } @Composable fun SafetyScreen(onAck: () -> Unit, onHolder: () -> Unit) { var checked by remember { mutableStateOf(false) } Column( Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .safeDrawingPadding() .padding(16.dp) ) { Text("Read before using MindMachine", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground) Spacer(Modifier.height(12.dp)) Text( "Flashing lights may be unsafe for people with epilepsy, seizure sensitivity, or migraine triggers.\n\nDo not use while driving, walking, cycling, or operating machinery.\n\nFor immersive visual sessions, keep your eyes closed. Do not stare at the screen.\n\nDo not place the phone directly on your eyes. Avoid pressure on face/eyes and ensure comfortable breathing/airflow.\n\nUse at your own risk. This app is a prototype and not a medical device.\n\nStop immediately for discomfort, dizziness, headache, nausea, anxiety, or eye strain.\n\nBinaural mode requires stereo headphones.", color = MaterialTheme.colorScheme.onBackground ) Row(verticalAlignment = Alignment.CenterVertically) { Checkbox(checked = checked, onCheckedChange = { checked = it }) Text("I understand the risks and will stop immediately if I feel discomfort.", color = MaterialTheme.colorScheme.onBackground) } Button(onClick = onAck, enabled = checked, modifier = Modifier.semantics { contentDescription = "I Understand" }) { Text("I Understand") } TextButton(onClick = onHolder) { Text("Positioning Guidance") } } } @Composable fun SetupScreen( vm: MainViewModel, onStart: () -> Unit, ) { val ui by vm.ui.collectAsStateWithLifecycle() val scrollState = rememberScrollState() val containerModifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .windowInsetsPadding(WindowInsets.statusBars) .windowInsetsPadding(WindowInsets.navigationBars) .then(if (setupScreenUsesScrollableContainer()) Modifier.verticalScroll(scrollState) else Modifier) .padding(horizontal = 16.dp, vertical = 12.dp) var programName by remember(ui.selectedPreset.id) { mutableStateOf(ui.selectedPreset.name) } val hasUnsavedChanges = setupScreenHasUnsavedChanges( hasUnsavedTimelineChanges = ui.hasUnsavedChanges, editedProgramName = programName, savedProgramName = ui.savedProgramName, ) Column( modifier = containerModifier, verticalArrangement = Arrangement.spacedBy(14.dp), ) { Text( ui.selectedPreset.name, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground, ) TextField( value = programName, onValueChange = { programName = it }, label = { Text("Program name") }, modifier = Modifier.fillMaxWidth(), ) DurationSliderCard( durationSec = ui.timeline.durationSec, onDurationSecChanged = { vm.setDurationSec(it) }, ) // Integrated timeline editor (no separate screen). SetupTimelineEditor( state = ui.timeline, onStateChanged = vm::updateTimeline, onCurveGranularityChanged = vm::setCurveGranularitySec, ) Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { if (hasUnsavedChanges) { OutlinedButton(onClick = { vm.saveCurrentProgram(programName) }, modifier = Modifier.weight(1f).height(52.dp)) { Text("Save") } } else { 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)) } } internal fun setupScreenFlashIntervalLabel(flashIntervalMs: Int): String = "Flash interval: ${"%.2f".format(flashIntervalMsToSeconds(flashIntervalMs))} s" internal fun formatMaxTwoDecimals(value: Float): String = String.format(Locale.US, "%.2f", value).trimEnd('0').trimEnd('.') internal fun setupScreenHasUnsavedChanges( hasUnsavedTimelineChanges: Boolean, editedProgramName: String, savedProgramName: String, ): Boolean { val nameChanged = editedProgramName.trim() != savedProgramName.trim() return hasUnsavedTimelineChanges || nameChanged } internal fun setupScreenUsesScrollableContainer(): Boolean = true @Composable fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry: () -> Unit) { val ui by vm.ui.collectAsStateWithLifecycle() if (ui.runtimeState == RuntimeState.COMPLETED) onFinish() BackHandler { onBackToEntry() } val isVisualMode = true val context = LocalContext.current DisposableEffect(isVisualMode) { val activity = context as? Activity val previous = activity?.requestedOrientation if (isVisualMode) { activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE } onDispose { if (isVisualMode) { activity?.requestedOrientation = previous ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } } } val immersiveFullscreen = shouldUseImmersiveFullscreen(isVisualMode, ui.runtimeState) var revealSystemBarsSignal by remember { mutableIntStateOf(0) } ApplyImmersiveMode( enabled = immersiveFullscreen, revealSignal = revealSystemBarsSignal, ) var showOverlay by remember(ui.runtimeState) { mutableStateOf(shouldShowActiveControlsByDefault(ui.runtimeState)) } LaunchedEffect(ui.runtimeState) { showOverlay = shouldShowActiveControlsByDefault(ui.runtimeState) } var brightnessOverlayVisible by remember { mutableStateOf(false) } var brightnessOverlayToken by remember { mutableIntStateOf(0) } LaunchedEffect(brightnessOverlayToken) { if (!brightnessOverlayVisible) return@LaunchedEffect delay(2500) brightnessOverlayVisible = false } ApplyKeepScreenOn(enabled = true) ApplyScreenBrightnessOverride( enabled = true, brightnessPercent = ui.settings.immersiveBrightnessPercent, ) val flashFrame by rememberSplitFlashFrame( isRunning = ui.runtimeState == RuntimeState.RUNNING, flashOnMs = ui.runtimeParams.flashOnMs, flashOffMs = ui.runtimeParams.flashOffMs, ) val progress = sessionProgressFraction( durationSec = ui.timeline.durationSec, remainingSec = ui.remainingSec, ) Box( modifier = Modifier .fillMaxSize() .background(Color.Black) .then(if (immersiveFullscreen) Modifier else Modifier.safeDrawingPadding()) .pointerInput(immersiveFullscreen) { detectTapGestures { brightnessOverlayVisible = true brightnessOverlayToken++ if (!immersiveFullscreen) { showOverlay = !showOverlay } } } ) { if (isVisualMode) { Row(Modifier.fillMaxSize()) { Box( modifier = Modifier .weight(1f) .fillMaxHeight() .background(flashFrame.left.toComposeColor()) ) Box( modifier = Modifier .weight(1f) .fillMaxHeight() .background(flashFrame.right.toComposeColor()) ) } } if (ui.runtimeState == RuntimeState.COUNTDOWN) { Text( "${ui.countdownSec}", modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.displayLarge, color = Color.White ) } if (immersiveFullscreen && ui.settings.showImmersiveProgressBar) { LinearProgressIndicator( progress = { progress }, modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() .height(5.dp), trackColor = Color.White.copy(alpha = 0.2f), color = Color.White, ) } if (brightnessOverlayVisible) { Column( Modifier .align(Alignment.TopCenter) .padding(top = 24.dp) .fillMaxWidth(0.45f) .background(Color.Black.copy(alpha = 0.72f)) .padding(horizontal = 12.dp, vertical = 10.dp) ) { Text( text = "Brightness ${ui.settings.immersiveBrightnessPercent}%", color = Color.White, style = MaterialTheme.typography.bodyMedium, ) Slider( value = ui.settings.immersiveBrightnessPercent.toFloat(), onValueChange = { vm.updateImmersiveBrightnessPercent(it.roundToInt()) brightnessOverlayVisible = true brightnessOverlayToken++ }, valueRange = 5f..100f, ) } } if (showOverlay) { Column( Modifier .align(Alignment.BottomCenter) .fillMaxWidth() .background(Color.Black.copy(alpha = 0.72f)) .padding(16.dp) ) { Text("${ui.selectedPreset.name} • ${ui.remainingSec}s", color = Color.White) if (ui.runtimeState == RuntimeState.RUNNING) { Button(onClick = { vm.pause() }, modifier = Modifier.fillMaxWidth()) { Text("Pause") } } else { Button(onClick = { vm.resume() }, modifier = Modifier.fillMaxWidth()) { Text("Resume") } } OutlinedButton( onClick = { vm.stop() onFinish() }, modifier = Modifier.fillMaxWidth() ) { Text("Stop") } if (ui.interruptionReason != null) { Text(ui.interruptionReason!!, color = Color.White) if (ui.runtimeState == RuntimeState.INTERRUPTED) { OutlinedButton(onClick = { vm.switchToVisualOnlyAndResume() }, modifier = Modifier.fillMaxWidth()) { Text("Resume Visual-only") } } } } } } } @Composable private fun ApplyKeepScreenOn(enabled: Boolean) { val activity = LocalContext.current as? Activity ?: return val window = activity.window DisposableEffect(window, enabled) { if (enabled) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } else { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } onDispose { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } } } @Composable private fun ApplyScreenBrightnessOverride(enabled: Boolean, brightnessPercent: Int) { val activity = LocalContext.current as? Activity ?: return val window = activity.window DisposableEffect(window) { onDispose { val reset = window.attributes reset.screenBrightness = -1f window.attributes = reset } } LaunchedEffect(enabled, brightnessPercent) { val params = window.attributes params.screenBrightness = if (enabled) { (brightnessPercent.coerceIn(5, 100) / 100f) } else { -1f } window.attributes = params } } @Composable private fun ApplyImmersiveMode(enabled: Boolean, revealSignal: Int) { val context = LocalContext.current val activity = context as? Activity ?: return val window = activity.window val controller = remember(window) { WindowInsetsControllerCompat(window, window.decorView) } DisposableEffect(window) { onDispose { controller.show(WindowInsetsCompat.Type.systemBars()) WindowCompat.setDecorFitsSystemWindows(window, true) } } LaunchedEffect(enabled) { if (enabled) { WindowCompat.setDecorFitsSystemWindows(window, false) controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE controller.hide(WindowInsetsCompat.Type.systemBars()) } else { controller.show(WindowInsetsCompat.Type.systemBars()) WindowCompat.setDecorFitsSystemWindows(window, true) } } LaunchedEffect(revealSignal, enabled) { if (!enabled || revealSignal == 0) return@LaunchedEffect controller.show(WindowInsetsCompat.Type.systemBars()) delay(2500) if (enabled) { controller.hide(WindowInsetsCompat.Type.systemBars()) } } } private fun FlashColor.toComposeColor(): Color = when (this) { FlashColor.RED -> Color.Red FlashColor.GREEN -> Color.Green FlashColor.BLACK -> Color.Black } @Composable private fun rememberSplitFlashFrame( isRunning: Boolean, flashOnMs: Int, flashOffMs: Int, ): androidx.compose.runtime.State { val latestFlashOnMs = rememberUpdatedState(flashOnMs) val latestFlashOffMs = rememberUpdatedState(flashOffMs) return produceState(initialValue = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK), isRunning) { if (!isRunning) { value = SplitFlashFrame(FlashColor.BLACK, FlashColor.BLACK) return@produceState } val sequencer = SplitFlashSequencer( startTimeNanos = System.nanoTime(), flashOnMs = latestFlashOnMs.value, flashOffMs = latestFlashOffMs.value, ) while (true) { withFrameNanos { frameTimeNanos -> sequencer.updateIntervals(latestFlashOnMs.value, latestFlashOffMs.value) value = sequencer.frameAt(frameTimeNanos) } } } } @Composable fun SettingsScreen(vm: MainViewModel) { val ui by vm.ui.collectAsStateWithLifecycle() val scrollState = rememberScrollState() Column( Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .safeDrawingPadding() .verticalScroll(scrollState) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { Text("Settings", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground) Text("Countdown: ${ui.settings.countdownSeconds}s", color = MaterialTheme.colorScheme.onBackground) Slider( value = ui.settings.countdownSeconds.toFloat(), onValueChange = { vm.updateCountdownSeconds(it.roundToInt()) }, valueRange = 0f..10f, steps = 9, ) Row(verticalAlignment = Alignment.CenterVertically) { Checkbox( checked = ui.settings.forceAudioWithoutHeadphones, onCheckedChange = vm::updateForceAudioWithoutHeadphones, ) Text("force audio without headphones", color = MaterialTheme.colorScheme.onBackground) } Row(verticalAlignment = Alignment.CenterVertically) { Checkbox(checked = ui.settings.showImmersiveProgressBar, onCheckedChange = vm::updateShowImmersiveProgressBar) Text("Show immersive bottom progress bar", color = MaterialTheme.colorScheme.onBackground) } HorizontalDivider() Text( "About/Disclaimer: MindMachine is a prototype and not a medical device. Use at your own risk. For immersive visual sessions, keep your eyes closed and do not place the phone directly on your eyes.", color = MaterialTheme.colorScheme.onBackground ) } } @Composable fun PositioningScreen() { Column( Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .safeDrawingPadding() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Text("Positioning Guidance", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground) Text("• Lie on your back in a safe, comfortable place.", color = MaterialTheme.colorScheme.onBackground) Text("• Rest the phone across the upper nose/forehead area so it stays stable.", color = MaterialTheme.colorScheme.onBackground) Text("• Keep your eyes closed during immersive visual sessions.", color = MaterialTheme.colorScheme.onBackground) Text("• Do not place the phone directly on your eyes or apply pressure.", color = MaterialTheme.colorScheme.onBackground) Text("• Ensure comfortable breathing/airflow and stop if you feel discomfort.", color = MaterialTheme.colorScheme.onBackground) } }