2026-03-10 22:50:50 -05:00
package com.mindmachine.mvp
2026-03-10 23:24:49 -05:00
import android.app.Activity
import android.content.pm.ActivityInfo
2026-03-10 22:50:50 -05:00
import android.os.Bundle
import androidx.activity.ComponentActivity
2026-03-10 23:34:07 -05:00
import androidx.activity.compose.BackHandler
2026-03-10 22:50:50 -05:00
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.background
2026-03-19 20:01:01 -05:00
import com.mindmachine.mvp.session.DurationSliderCard
import com.mindmachine.mvp.session.SetupTimelineEditor
2026-03-10 22:50:50 -05:00
import androidx.compose.foundation.clickable
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
2026-03-10 23:24:49 -05:00
import androidx.compose.foundation.layout.fillMaxHeight
2026-03-10 22:50:50 -05:00
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
2026-03-10 23:42:55 -05:00
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
2026-03-10 22:50:50 -05:00
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Card
2026-03-10 23:14:33 -05:00
import androidx.compose.material3.CardDefaults
2026-03-10 22:50:50 -05:00
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
2026-03-21 23:01:02 -05:00
import androidx.compose.material3.LinearProgressIndicator
2026-03-10 22:50:50 -05:00
import androidx.compose.material3.OutlinedButton
2026-03-10 23:24:49 -05:00
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
2026-03-10 22:50:50 -05:00
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
2026-03-21 22:01:01 -05:00
import androidx.compose.material3.TextField
2026-03-10 22:50:50 -05:00
import androidx.compose.material3.TopAppBar
2026-03-10 23:14:33 -05:00
import androidx.compose.material3.darkColorScheme
2026-03-10 22:50:50 -05:00
import androidx.compose.runtime.Composable
2026-03-10 23:24:49 -05:00
import androidx.compose.runtime.DisposableEffect
2026-03-10 23:34:07 -05:00
import androidx.compose.runtime.LaunchedEffect
2026-03-10 22:50:50 -05:00
import androidx.compose.runtime.getValue
2026-03-10 23:51:55 -05:00
import androidx.compose.runtime.mutableIntStateOf
2026-03-10 22:50:50 -05:00
import androidx.compose.runtime.mutableStateOf
2026-03-10 23:14:33 -05:00
import androidx.compose.runtime.produceState
2026-03-10 22:50:50 -05:00
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
2026-03-10 23:14:33 -05:00
import androidx.compose.runtime.withFrameNanos
2026-03-10 22:50:50 -05:00
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
2026-03-10 23:24:49 -05:00
import androidx.compose.ui.platform.LocalContext
2026-03-10 22:50:50 -05:00
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
2026-03-10 23:51:55 -05:00
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
2026-03-10 22:50:50 -05:00
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.mindmachine.mvp.audio.BinauralAudioEngine
import com.mindmachine.mvp.audio.HeadsetMonitor
import com.mindmachine.mvp.data.SettingsRepository
2026-03-21 22:01:01 -05:00
import com.mindmachine.mvp.data.UserProgramRepository
2026-03-10 22:50:50 -05:00
import com.mindmachine.mvp.domain.RuntimeState
import com.mindmachine.mvp.domain.SessionMode
2026-03-10 23:24:49 -05:00
import com.mindmachine.mvp.session.FlashColor
2026-03-10 22:50:50 -05:00
import com.mindmachine.mvp.session.MainViewModel
2026-03-10 23:24:49 -05:00
import com.mindmachine.mvp.session.SplitFlashFrame
import com.mindmachine.mvp.session.computeSplitFlashFrame
2026-03-10 23:34:07 -05:00
import com.mindmachine.mvp.session.flashIntervalMsToSeconds
import com.mindmachine.mvp.session.flashIntervalSecondsToMs
2026-03-21 23:01:02 -05:00
import com.mindmachine.mvp.session.sessionProgressFraction
2026-03-10 23:34:07 -05:00
import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault
2026-03-10 23:51:55 -05:00
import com.mindmachine.mvp.session.shouldUseImmersiveFullscreen
import kotlinx.coroutines.delay
2026-03-10 22:50:50 -05:00
import kotlin.math.roundToInt
2026-03-10 23:14:33 -05:00
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 )
)
2026-03-10 22:50:50 -05:00
class MainActivity : ComponentActivity ( ) {
private val vm : MainViewModel by viewModels {
2026-03-21 22:01:01 -05:00
MainViewModel . Factory (
SettingsRepository ( applicationContext ) ,
UserProgramRepository ( applicationContext ) ,
HeadsetMonitor ( applicationContext ) ,
BinauralAudioEngine ( ) ,
)
2026-03-10 22:50:50 -05:00
}
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
2026-03-10 23:14:33 -05:00
setContent {
MaterialTheme ( colorScheme = MindMachineColors ) {
App ( vm )
}
}
2026-03-10 22:50:50 -05:00
}
}
2026-03-10 23:03:46 -05:00
@OptIn ( ExperimentalMaterial3Api :: class )
2026-03-10 22:50:50 -05:00
@Composable
fun App ( vm : MainViewModel = viewModel ( ) ) {
val nav = rememberNavController ( )
val ui by vm . ui . collectAsStateWithLifecycle ( )
val lifecycle = LocalLifecycleOwner . current . lifecycle
2026-03-10 23:24:49 -05:00
DisposableEffect ( lifecycle ) {
2026-03-10 22:50:50 -05:00
val observer = LifecycleEventObserver { _ , event ->
if ( event == Lifecycle . Event . ON _STOP && ui . runtimeState == RuntimeState . RUNNING ) {
vm . pause ( " Session paused because app moved to background. " )
}
}
lifecycle . addObserver ( observer )
onDispose { lifecycle . removeObserver ( observer ) }
}
val startRoute = if ( ui . settings . safetyAcknowledged ) " home " else " welcome "
2026-03-10 23:14:33 -05:00
NavHost ( navController = nav , startDestination = startRoute , modifier = Modifier . background ( MaterialTheme . colorScheme . background ) ) {
2026-03-10 22:50:50 -05:00
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 " ) {
Column ( Modifier . fillMaxSize ( ) ) {
TopAppBar ( title = { Text ( " MindMachine " ) } , actions = {
2026-03-21 22:01:01 -05:00
TextButton ( onClick = {
vm . createNewProgramDraft ( )
nav . navigate ( " setup " )
} ) { Text ( " Add " ) }
2026-03-10 22:50:50 -05:00
TextButton ( onClick = { nav . navigate ( " settings " ) } ) { Text ( " Settings " ) }
} )
LazyColumn ( Modifier . padding ( 12. dp ) , verticalArrangement = Arrangement . spacedBy ( 8. dp ) ) {
item {
TextButton ( onClick = { nav . navigate ( " holder " ) } ) { Text ( " Holder Guidance " ) }
}
items ( ui . presets ) { p ->
2026-03-10 23:14:33 -05:00
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 )
) {
2026-03-21 22:01:01 -05:00
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 ( " ${p.defaultDurationSec / 60} min • ${p.visualPatternType} • binaural ${p.binauralDifferenceHz} " )
}
2026-03-21 23:01:02 -05:00
TextButton ( onClick = { vm . deleteProgram ( p . id ) } ) { Text ( " Delete " ) }
2026-03-10 22:50:50 -05:00
}
}
}
}
}
}
composable ( " setup " ) {
2026-03-19 15:56:50 -05:00
SetupScreen (
vm = vm ,
onStart = {
vm . startSession ( )
nav . navigate ( " active " )
} ,
onHolder = { nav . navigate ( " holder " ) } ,
)
2026-03-10 22:50:50 -05:00
}
composable ( " active " ) {
2026-03-10 23:34:07 -05:00
ActiveSessionScreen (
vm = vm ,
onFinish = { nav . navigate ( " complete " ) { popUpTo ( " setup " ) } } ,
onBackToSetup = { nav . popBackStack ( " setup " , inclusive = false ) }
)
2026-03-10 22:50:50 -05:00
}
composable ( " complete " ) {
SimpleScreen (
if ( ui . endedEarly ) " Session ended " else " Session complete. " ,
ui . selectedPreset . name
) {
Button ( onClick = {
vm . startSession ( )
nav . navigate ( " active " )
} ) { Text ( " Repeat Session " ) }
OutlinedButton ( onClick = { nav . navigate ( " home " ) { popUpTo ( 0 ) } } ) { Text ( " Return Home " ) }
}
}
composable ( " settings " ) {
SettingsScreen ( vm ) { nav . popBackStack ( ) }
}
composable ( " holder " ) {
HolderScreen { nav . popBackStack ( ) }
}
}
}
@Composable
fun SimpleScreen ( title : String , subtitle : String , actions : @Composable ColumnScope . ( ) -> Unit ) {
Column (
2026-03-10 23:14:33 -05:00
modifier = Modifier . fillMaxSize ( ) . background ( MaterialTheme . colorScheme . background ) . padding ( 16. dp ) ,
2026-03-10 22:50:50 -05:00
verticalArrangement = Arrangement . spacedBy ( 12. dp ) ,
) {
2026-03-10 23:14:33 -05:00
Text ( title , style = MaterialTheme . typography . headlineMedium , color = MaterialTheme . colorScheme . onBackground )
Text ( subtitle , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
actions ( )
}
}
@Composable
fun SafetyScreen ( onAck : ( ) -> Unit , onHolder : ( ) -> Unit ) {
var checked by remember { mutableStateOf ( false ) }
2026-03-10 23:14:33 -05:00
Column ( Modifier . fillMaxSize ( ) . background ( MaterialTheme . colorScheme . background ) . padding ( 16. dp ) ) {
Text ( " Read before using MindMachine " , style = MaterialTheme . typography . headlineSmall , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
Spacer ( Modifier . height ( 12. dp ) )
2026-03-10 23:14:33 -05:00
Text (
" Flashing lights may be unsafe for people with epilepsy, seizure sensitivity, or migraine triggers. \n \n Do not use while driving, walking, cycling, or operating machinery. \n \n Stop immediately for discomfort, dizziness, headache, nausea, anxiety, or eye strain. \n \n Binaural mode requires stereo headphones. \n \n This app is not a medical device. " ,
color = MaterialTheme . colorScheme . onBackground
)
2026-03-10 22:50:50 -05:00
Row ( verticalAlignment = Alignment . CenterVertically ) {
Checkbox ( checked = checked , onCheckedChange = { checked = it } )
2026-03-10 23:14:33 -05:00
Text ( " I understand the risks and will stop immediately if I feel discomfort. " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
}
Button ( onClick = onAck , enabled = checked , modifier = Modifier . semantics { contentDescription = " I Understand " } ) { Text ( " I Understand " ) }
TextButton ( onClick = onHolder ) { Text ( " Holder Guidance " ) }
}
}
@Composable
2026-03-19 15:56:50 -05:00
fun SetupScreen (
vm : MainViewModel ,
onStart : ( ) -> Unit ,
onHolder : ( ) -> Unit ,
) {
2026-03-10 22:50:50 -05:00
val ui by vm . ui . collectAsStateWithLifecycle ( )
val cfg = ui . config
2026-03-10 23:42:55 -05:00
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 )
2026-03-21 22:01:01 -05:00
var programName by remember ( ui . selectedPreset . id ) { mutableStateOf ( ui . selectedPreset . name ) }
2026-03-10 23:14:33 -05:00
Column (
2026-03-19 20:01:01 -05:00
modifier = containerModifier ,
verticalArrangement = Arrangement . spacedBy ( 14. dp ) ,
2026-03-10 23:14:33 -05:00
) {
2026-03-19 20:01:01 -05:00
Text (
ui . selectedPreset . name ,
style = MaterialTheme . typography . headlineSmall ,
color = MaterialTheme . colorScheme . onBackground ,
)
2026-03-21 22:01:01 -05:00
TextField (
value = programName ,
onValueChange = { programName = it } ,
label = { Text ( " Program name " ) } ,
modifier = Modifier . fillMaxWidth ( ) ,
)
2026-03-19 20:01:01 -05:00
DurationSliderCard (
durationSec = ui . timeline . durationSec ,
onDurationSecChanged = { vm . setDurationSec ( it ) } ,
)
// Integrated timeline editor (no separate screen).
SetupTimelineEditor (
state = ui . timeline ,
onStateChanged = vm :: updateTimeline ,
)
2026-03-10 22:50:50 -05:00
TextButton ( onClick = onHolder ) { Text ( " Holder Guidance " ) }
2026-03-21 22:01:01 -05:00
Row ( horizontalArrangement = Arrangement . spacedBy ( 8. dp ) , modifier = Modifier . fillMaxWidth ( ) ) {
OutlinedButton ( onClick = { vm . saveCurrentProgram ( programName ) } , modifier = Modifier . weight ( 1f ) . height ( 52. dp ) ) {
Text ( " Save " )
}
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 )
2026-03-10 23:42:55 -05:00
Spacer ( Modifier . height ( 8. dp ) )
2026-03-10 22:50:50 -05:00
}
}
2026-03-10 23:42:55 -05:00
internal fun setupScreenFlashIntervalLabel ( flashIntervalMs : Int ) : String =
" Flash interval: ${"%.2f".format(flashIntervalMsToSeconds(flashIntervalMs))} s "
internal fun setupScreenUsesScrollableContainer ( ) : Boolean = true
2026-03-10 22:50:50 -05:00
@Composable
2026-03-10 23:34:07 -05:00
fun ActiveSessionScreen ( vm : MainViewModel , onFinish : ( ) -> Unit , onBackToSetup : ( ) -> Unit ) {
2026-03-10 22:50:50 -05:00
val ui by vm . ui . collectAsStateWithLifecycle ( )
2026-03-10 23:34:07 -05:00
if ( ui . runtimeState == RuntimeState . COMPLETED ) onFinish ( )
BackHandler {
vm . stop ( )
onBackToSetup ( )
}
2026-03-10 22:50:50 -05:00
2026-03-10 23:24:49 -05:00
val isVisualMode = ui . config . mode != SessionMode . AUDIO _ONLY
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
}
}
}
2026-03-10 23:51:55 -05:00
val immersiveFullscreen = shouldUseImmersiveFullscreen ( isVisualMode , ui . runtimeState )
var revealSystemBarsSignal by remember { mutableIntStateOf ( 0 ) }
ApplyImmersiveMode (
enabled = immersiveFullscreen ,
revealSignal = revealSystemBarsSignal ,
)
2026-03-10 23:34:07 -05:00
var showOverlay by remember ( ui . runtimeState ) { mutableStateOf ( shouldShowActiveControlsByDefault ( ui . runtimeState ) ) }
LaunchedEffect ( ui . runtimeState ) {
showOverlay = shouldShowActiveControlsByDefault ( ui . runtimeState )
}
2026-03-10 23:24:49 -05:00
val flashFrame by rememberSplitFlashFrame (
2026-03-10 23:14:33 -05:00
isRunning = ui . runtimeState == RuntimeState . RUNNING ,
mode = ui . config . mode ,
2026-03-19 20:01:01 -05:00
flashOnMs = ui . runtimeParams . flashOnMs ,
flashOffMs = ui . runtimeParams . flashOffMs ,
2026-03-10 23:14:33 -05:00
)
2026-03-21 23:01:02 -05:00
val progress = sessionProgressFraction (
durationSec = ui . timeline . durationSec ,
remainingSec = ui . remainingSec ,
)
2026-03-10 22:50:50 -05:00
Box (
2026-03-10 23:14:33 -05:00
modifier = Modifier
. fillMaxSize ( )
. background ( Color . Black )
2026-03-10 23:51:55 -05:00
. clickable {
2026-03-21 23:01:02 -05:00
if ( ! immersiveFullscreen ) {
showOverlay = ! showOverlay
2026-03-10 23:51:55 -05:00
}
}
2026-03-10 22:50:50 -05:00
) {
2026-03-10 23:24:49 -05:00
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 ( ) )
)
}
}
2026-03-10 22:50:50 -05:00
if ( ui . runtimeState == RuntimeState . COUNTDOWN ) {
Text (
" ${ui.countdownSec} " ,
modifier = Modifier . align ( Alignment . Center ) ,
style = MaterialTheme . typography . displayLarge ,
2026-03-10 23:14:33 -05:00
color = Color . White
2026-03-10 22:50:50 -05:00
)
}
2026-03-21 23:01:02 -05:00
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 ,
)
}
2026-03-10 22:50:50 -05:00
if ( showOverlay ) {
2026-03-10 23:14:33 -05:00
Column (
Modifier
. align ( Alignment . BottomCenter )
. fillMaxWidth ( )
2026-03-10 23:24:49 -05:00
. background ( Color . Black . copy ( alpha = 0.72f ) )
2026-03-10 23:14:33 -05:00
. padding ( 16. dp )
) {
2026-03-10 22:50:50 -05:00
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 " ) }
}
2026-03-10 23:34:07 -05:00
OutlinedButton ( onClick = { vm . stop ( ) ; onFinish ( ) } , modifier = Modifier . fillMaxWidth ( ) ) { Text ( " Stop " ) }
2026-03-10 22:50:50 -05:00
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 " )
}
}
}
}
}
}
}
2026-03-10 23:51:55 -05:00
@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 ( ) )
}
}
}
2026-03-10 23:24:49 -05:00
private fun FlashColor . toComposeColor ( ) : Color = when ( this ) {
FlashColor . RED -> Color . Red
FlashColor . GREEN -> Color . Green
FlashColor . BLACK -> Color . Black
}
2026-03-10 23:14:33 -05:00
@Composable
2026-03-10 23:24:49 -05:00
private fun rememberSplitFlashFrame (
2026-03-10 23:14:33 -05:00
isRunning : Boolean ,
mode : SessionMode ,
2026-03-19 20:01:01 -05:00
flashOnMs : Int ,
flashOffMs : Int ,
) = produceState ( initialValue = SplitFlashFrame ( FlashColor . BLACK , FlashColor . BLACK ) , isRunning , mode , flashOnMs , flashOffMs ) {
2026-03-10 23:24:49 -05:00
if ( !is Running || mode == SessionMode . AUDIO _ONLY ) {
value = SplitFlashFrame ( FlashColor . BLACK , FlashColor . BLACK )
return @produceState
}
while ( true ) {
withFrameNanos { frameTimeNanos ->
2026-03-19 20:01:01 -05:00
value = computeSplitFlashFrame ( frameTimeNanos , flashOnMs , flashOffMs )
2026-03-10 23:14:33 -05:00
}
}
}
2026-03-10 22:50:50 -05:00
@Composable
fun SettingsScreen ( vm : MainViewModel , onBack : ( ) -> Unit ) {
val ui by vm . ui . collectAsStateWithLifecycle ( )
2026-03-10 23:14:33 -05:00
Column (
Modifier . fillMaxSize ( ) . background ( MaterialTheme . colorScheme . background ) . padding ( 16. dp ) ,
verticalArrangement = Arrangement . spacedBy ( 12. dp )
) {
Text ( " Settings " , style = MaterialTheme . typography . headlineSmall , color = MaterialTheme . colorScheme . onBackground )
2026-03-21 23:01:02 -05:00
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 ,
)
2026-03-10 23:14:33 -05:00
Text ( " Default mode " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
Row ( horizontalArrangement = Arrangement . spacedBy ( 8. dp ) ) {
SessionMode . values ( ) . forEach { mode ->
OutlinedButton ( onClick = { vm . updateDefaultMode ( mode ) } ) { Text ( mode . name ) }
}
}
2026-03-21 23:01:02 -05:00
2026-03-10 22:50:50 -05:00
Row ( verticalAlignment = Alignment . CenterVertically ) {
Checkbox ( checked = ui . settings . showHolderGuidanceBeforeSession , onCheckedChange = vm :: updateGuidance )
2026-03-10 23:14:33 -05:00
Text ( " Show holder guidance before session " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
}
2026-03-21 23:01:02 -05:00
Row ( verticalAlignment = Alignment . CenterVertically ) {
Checkbox ( checked = ui . settings . showImmersiveProgressBar , onCheckedChange = vm :: updateShowImmersiveProgressBar )
Text ( " Show immersive bottom progress bar " , color = MaterialTheme . colorScheme . onBackground )
}
2026-03-10 22:50:50 -05:00
HorizontalDivider ( )
2026-03-10 23:14:33 -05:00
Text ( " About/Disclaimer: MindMachine is a prototype and not a medical device. " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
OutlinedButton ( onClick = onBack ) { Text ( " Back " ) }
}
}
@Composable
fun HolderScreen ( onBack : ( ) -> Unit ) {
2026-03-10 23:14:33 -05:00
Column (
Modifier . fillMaxSize ( ) . background ( MaterialTheme . colorScheme . background ) . padding ( 16. dp ) ,
verticalArrangement = Arrangement . spacedBy ( 8. dp )
) {
Text ( " Holder Guidance " , style = MaterialTheme . typography . headlineSmall , color = MaterialTheme . colorScheme . onBackground )
Text ( " • Use a simple cardboard visor/holder. " , color = MaterialTheme . colorScheme . onBackground )
Text ( " • Keep phone stable and hands-free. " , color = MaterialTheme . colorScheme . onBackground )
Text ( " • Do not press device against eyes/face. " , color = MaterialTheme . colorScheme . onBackground )
Text ( " • Allow airflow and comfort. " , color = MaterialTheme . colorScheme . onBackground )
Text ( " • Test fit before session. " , color = MaterialTheme . colorScheme . onBackground )
Text ( " • Sit or lie down in a safe place. " , color = MaterialTheme . colorScheme . onBackground )
2026-03-10 22:50:50 -05:00
OutlinedButton ( onClick = onBack ) { Text ( " Back " ) }
}
}