MindMachine: add immersive fullscreen and tap-to-reveal bars

This commit is contained in:
Tretzi
2026-03-10 23:51:55 -05:00
parent fecd29ac3d
commit a4d81f41b0
3 changed files with 89 additions and 1 deletions

View File

@@ -48,6 +48,7 @@ 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
@@ -61,6 +62,9 @@ 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
@@ -80,7 +84,10 @@ import com.mindmachine.mvp.session.SplitFlashFrame
import com.mindmachine.mvp.session.computeSplitFlashFrame
import com.mindmachine.mvp.session.flashIntervalMsToSeconds
import com.mindmachine.mvp.session.flashIntervalSecondsToMs
import com.mindmachine.mvp.session.shouldRevealSystemBarsOnTap
import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault
import com.mindmachine.mvp.session.shouldUseImmersiveFullscreen
import kotlinx.coroutines.delay
import kotlin.math.roundToInt
private val MindMachineColors = darkColorScheme(
@@ -320,6 +327,13 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup:
}
}
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)
@@ -334,7 +348,12 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup:
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.clickable { showOverlay = !showOverlay }
.clickable {
showOverlay = !showOverlay
if (shouldRevealSystemBarsOnTap(isVisualMode, ui.runtimeState)) {
revealSystemBarsSignal += 1
}
}
) {
if (isVisualMode) {
Row(Modifier.fillMaxSize()) {
@@ -389,6 +408,41 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToSetup:
}
}
@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

View File

@@ -0,0 +1,11 @@
package com.mindmachine.mvp.session
import com.mindmachine.mvp.domain.RuntimeState
fun shouldUseImmersiveFullscreen(isVisualMode: Boolean, runtimeState: RuntimeState): Boolean {
return isVisualMode && (runtimeState == RuntimeState.COUNTDOWN || runtimeState == RuntimeState.RUNNING)
}
fun shouldRevealSystemBarsOnTap(isVisualMode: Boolean, runtimeState: RuntimeState): Boolean {
return shouldUseImmersiveFullscreen(isVisualMode, runtimeState)
}

View File

@@ -1,7 +1,9 @@
package com.mindmachine.mvp
import com.mindmachine.mvp.domain.RuntimeState
import com.mindmachine.mvp.session.shouldRevealSystemBarsOnTap
import com.mindmachine.mvp.session.shouldShowActiveControlsByDefault
import com.mindmachine.mvp.session.shouldUseImmersiveFullscreen
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -21,4 +23,25 @@ class ActiveSessionUiTest {
assertTrue(shouldShowActiveControlsByDefault(RuntimeState.COMPLETED))
assertTrue(shouldShowActiveControlsByDefault(RuntimeState.STOPPED))
}
@Test
fun immersive_fullscreen_applies_only_to_visual_countdown_and_running_states() {
assertTrue(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.COUNTDOWN))
assertTrue(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.RUNNING))
assertFalse(shouldUseImmersiveFullscreen(isVisualMode = false, runtimeState = RuntimeState.COUNTDOWN))
assertFalse(shouldUseImmersiveFullscreen(isVisualMode = false, runtimeState = RuntimeState.RUNNING))
assertFalse(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.PAUSED))
assertFalse(shouldUseImmersiveFullscreen(isVisualMode = true, runtimeState = RuntimeState.INTERRUPTED))
}
@Test
fun tap_to_reveal_system_bars_is_only_enabled_during_active_visual_stimulation() {
assertTrue(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.COUNTDOWN))
assertTrue(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.RUNNING))
assertFalse(shouldRevealSystemBarsOnTap(isVisualMode = false, runtimeState = RuntimeState.RUNNING))
assertFalse(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.PAUSED))
assertFalse(shouldRevealSystemBarsOnTap(isVisualMode = true, runtimeState = RuntimeState.COMPLETED))
}
}