Auto commit Tue Mar 24 06:01:01 PM CDT 2026
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user