Auto commit Tue Mar 24 06:01:01 PM CDT 2026

This commit is contained in:
Tretzi
2026-03-24 18:01:01 -05:00
parent 2a548edc9a
commit 1d4a599dd6
4 changed files with 158 additions and 25 deletions

View File

@@ -18,23 +18,35 @@ 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.windowInsetsBottomHeight
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
@@ -87,11 +99,13 @@ 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 val MindMachineColors = darkColorScheme(
primary = Color(0xFF8AB4FF),
@@ -177,17 +191,30 @@ fun App(vm: MainViewModel = viewModel()) {
)
}
composable("home") {
Column(Modifier.fillMaxSize()) {
var confirmDeleteProgramId by remember { mutableStateOf<String?>(null) }
var confirmDeleteProgramName by remember { mutableStateOf("") }
Column(
Modifier
.fillMaxSize()
.safeDrawingPadding()
) {
TopAppBar(title = { Text("MindMachine") }, actions = {
TextButton(onClick = {
vm.createNewProgramDraft()
nav.navigate("setup")
}) { Text("Add") }
TextButton(onClick = { nav.navigate("settings") }) { Text("Settings") }
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("Holder Guidance") }
TextButton(onClick = { nav.navigate("holder") }) { Text("User Guidance/Disclaimer") }
}
items(ui.presets) { p ->
Card(
@@ -208,14 +235,66 @@ fun App(vm: MainViewModel = viewModel()) {
Column(Modifier.weight(1f)) {
Text(p.name, style = MaterialTheme.typography.titleMedium)
Text(p.description)
Text("${p.defaultDurationSec / 60} min${p.visualPatternType} • binaural ${p.binauralDifferenceHz}")
Text("${formatDuration(p.defaultDurationSec)}${p.visualPatternType} • binaural ${formatMaxTwoDecimals(p.binauralDifferenceHz)}")
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null,
tint = Color(0xFF4CAF50),
modifier = Modifier.size(30.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()
)
}
}
TextButton(onClick = { vm.deleteProgram(p.id) }) { Text("Delete") }
}
}
}
}
}
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(
@@ -226,7 +305,6 @@ fun App(vm: MainViewModel = viewModel()) {
nav.navigate("active")
}
},
onHolder = { nav.navigate("holder") },
)
}
composable("active") {
@@ -262,7 +340,11 @@ fun App(vm: MainViewModel = viewModel()) {
@Composable
fun SimpleScreen(title: String, subtitle: String, actions: @Composable ColumnScope.() -> Unit) {
Column(
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp),
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)
@@ -274,7 +356,13 @@ fun SimpleScreen(title: String, subtitle: String, actions: @Composable ColumnSco
@Composable
fun SafetyScreen(onAck: () -> Unit, onHolder: () -> Unit) {
var checked by remember { mutableStateOf(false) }
Column(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp)) {
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(
@@ -294,15 +382,15 @@ fun SafetyScreen(onAck: () -> Unit, onHolder: () -> Unit) {
fun SetupScreen(
vm: MainViewModel,
onStart: () -> Unit,
onHolder: () -> 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)
.safeDrawingPadding()
.padding(horizontal = 16.dp, vertical = 12.dp)
var programName by remember(ui.selectedPreset.id) { mutableStateOf(ui.selectedPreset.name) }
@@ -342,7 +430,6 @@ fun SetupScreen(
onCurveGranularityChanged = vm::setCurveGranularitySec,
)
TextButton(onClick = onHolder) { Text("Holder Guidance") }
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
if (hasUnsavedChanges) {
OutlinedButton(onClick = { vm.saveCurrentProgram(programName) }, modifier = Modifier.weight(1f).height(52.dp)) {
@@ -360,6 +447,9 @@ fun SetupScreen(
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,
@@ -436,6 +526,7 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup:
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.then(if (immersiveFullscreen) Modifier else Modifier.safeDrawingPadding())
.pointerInput(immersiveFullscreen) {
detectTapGestures {
brightnessOverlayVisible = true
@@ -636,8 +727,14 @@ private fun rememberSplitFlashFrame(
@Composable
fun SettingsScreen(vm: MainViewModel) {
val ui by vm.ui.collectAsStateWithLifecycle()
val scrollState = rememberScrollState()
Column(
Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp),
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)
@@ -671,7 +768,11 @@ fun SettingsScreen(vm: MainViewModel) {
@Composable
fun HolderScreen() {
Column(
Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background).padding(16.dp),
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.safeDrawingPadding()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("Holder Guidance", style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onBackground)