diff --git a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt index 33baa09..fac951b 100644 --- a/app/src/main/java/com/mindmachine/mvp/MainActivity.kt +++ b/app/src/main/java/com/mindmachine/mvp/MainActivity.kt @@ -3,6 +3,7 @@ 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 @@ -85,9 +86,11 @@ 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 @@ -107,6 +110,9 @@ 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), @@ -241,12 +247,22 @@ fun App(vm: MainViewModel = viewModel()) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Icon( - imageVector = Icons.Filled.PlayArrow, - contentDescription = null, - tint = Color(0xFF4CAF50), - modifier = Modifier.size(30.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) @@ -302,31 +318,32 @@ fun App(vm: MainViewModel = viewModel()) { onStart = { val started = vm.startSession() if (started) { - nav.navigate("active") + nav.navigate("active/$SESSION_ENTRY_SETUP") } }, ) } - composable("active") { + 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("complete") { popUpTo("setup") } }, - onBackToSetup = { nav.popBackStack("setup", inclusive = false) } - ) - } - composable("complete") { - SimpleScreen( - if (ui.endedEarly) "Session ended" else "Session complete.", - ui.selectedPreset.name - ) { - Button(onClick = { - val started = vm.startSession() - if (started) { - nav.navigate("active") + onFinish = { + nav.navigate(entryPoint) { + popUpTo("active/$entryPoint") { inclusive = true } + launchSingleTop = true } - }) { Text("Repeat Session") } - OutlinedButton(onClick = { nav.navigate("home") { popUpTo(0) } }) { Text("Return Home") } - } + }, + onBackToEntry = { + vm.stop() + nav.navigate(entryPoint) { + popUpTo("active/$entryPoint") { inclusive = true } + launchSingleTop = true + } + } + ) } composable("settings") { SettingsScreen(vm) @@ -462,13 +479,12 @@ internal fun setupScreenHasUnsavedChanges( internal fun setupScreenUsesScrollableContainer(): Boolean = true @Composable -fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: () -> Unit) { +fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry: () -> Unit) { val ui by vm.ui.collectAsStateWithLifecycle() if (ui.runtimeState == RuntimeState.COMPLETED) onFinish() BackHandler { - vm.stop() - onBackToSetup() + onBackToEntry() } val isVisualMode = true @@ -506,6 +522,8 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: brightnessOverlayVisible = false } + ApplyKeepScreenOn(enabled = true) + ApplyScreenBrightnessOverride( enabled = true, brightnessPercent = ui.settings.immersiveBrightnessPercent, @@ -629,6 +647,23 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup: } } +@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 diff --git a/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt b/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt index a7a0256..26f1431 100644 --- a/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt +++ b/app/src/main/java/com/mindmachine/mvp/session/MainViewModel.kt @@ -142,25 +142,13 @@ class MainViewModel( fun acknowledgeSafety() = viewModelScope.launch { settingsRepository.acknowledgeSafety(SAFETY_VERSION) } - fun choosePreset(id: String) = viewModelScope.launch { - val state = _ui.value - val preset = state.presets.first { it.id == id } - settingsRepository.updateLastPreset(id) - _ui.update { - val cfg = preset.toConfig(SessionMode.AUDIO_VISUAL) - val timeline = timelineForProgramId(preset.id, cachedPrograms) - val savedSignature = timelineProgramSignature(timeline) - it.copy( - selectedPreset = preset, - config = cfg.copy(durationSec = cfg.durationSec.coerceIn(60, 8 * 60 * 60)), - timeline = timeline, - savedProgramName = preset.name, - savedTimelineSignature = savedSignature, - hasUnsavedChanges = false, - error = null, - ) - } - elapsedBeforePauseSec = 0f + fun choosePreset(id: String) { + applyPresetSelection(id) + } + + fun startSessionFromProgramSelection(id: String): Boolean { + applyPresetSelection(id) + return startSession() } fun createNewProgramDraft() { @@ -442,6 +430,31 @@ class MainViewModel( return settings.forceAudioWithoutHeadphones || headsetAvailable } + private fun applyPresetSelection(id: String) { + val state = _ui.value + val preset = state.presets.firstOrNull { it.id == id } ?: return + + _ui.update { + val cfg = preset.toConfig(SessionMode.AUDIO_VISUAL) + val timeline = timelineForProgramId(preset.id, cachedPrograms) + val savedSignature = timelineProgramSignature(timeline) + it.copy( + selectedPreset = preset, + config = cfg.copy(durationSec = cfg.durationSec.coerceIn(60, 8 * 60 * 60)), + timeline = timeline, + savedProgramName = preset.name, + savedTimelineSignature = savedSignature, + hasUnsavedChanges = false, + error = null, + ) + } + elapsedBeforePauseSec = 0f + + viewModelScope.launch { + settingsRepository.updateLastPreset(id) + } + } + private fun currentElapsedSec(): Float { val state = _ui.value val total = state.timeline.durationSec.toFloat().coerceAtLeast(1f)