diff --git a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt index 5050182c6..889b895ac 100644 --- a/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/SessionActivity.kt @@ -355,9 +355,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { onComponentsRefresh = { refreshDrawerComponents() }, onComponentSwap = { pid, comp, value -> swapDrawerComponent(pid, comp, value) }, )) - if (SessionState.suspended) SessionPausedOverlay { - SessionService.resume(this@SessionActivity) - } + if (SessionState.suspended) SessionPausedOverlay( + title = pausedTitle(), + onResume = { SessionService.resume(this@SessionActivity) }, + // As the drawer's Stop does; stopping resumes the paused processes first. + onStop = { SessionService.stop(this@SessionActivity); finish() }, + ) } } } @@ -475,6 +478,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback { if (SessionState.running) SessionState.mode else intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM + private fun pausedTitle(): String = when (SessionState.mode) { + SessionService.MODE_DESKTOP -> "The desktop is paused" + SessionService.MODE_RUN -> (com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: "The program") + " is paused" + else -> "Steam is paused" + } + private fun loadingTitle(): String = when (loadingMode()) { SessionService.MODE_DESKTOP -> if (!SessionState.running && intent.getStringExtra(SessionService.EXTRA_STEAM_UI) != null) "Starting Steam on the desktop" diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index 9c43a67a9..f49148977 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -1733,6 +1733,7 @@ internal fun PrimaryButton( enabled: Boolean = true, main: Boolean = false, compact: Boolean = false, + modifier: Modifier = Modifier, onClick: () -> Unit, ) { val colors = MaterialTheme.colorScheme @@ -1751,7 +1752,7 @@ internal fun PrimaryButton( val lift by animateFloatAsState(if (hot) 14f else 6f, Motion.tw(300), label = "btnLift") Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = track + modifier = modifier.then(track) .then(if (frontFocus != null) Modifier.focusRequester(frontFocus.primary).onFocusChanged { if (it.isFocused) frontFocus.last = FrontFocus.PRIMARY } else Modifier) .graphicsLayer { scaleX = scale; scaleY = scale; shadowElevation = if (enabled) lift.dp.toPx() else 0f; shape = Shape12; clip = false; ambientShadowColor = pal.signal; spotShadowColor = pal.signal } .clip(Shape12) @@ -1768,7 +1769,7 @@ internal fun PrimaryButton( } @Composable -internal fun SecondaryButton(text: String, enabled: Boolean = true, compact: Boolean = false, onClick: () -> Unit) { +internal fun SecondaryButton(text: String, enabled: Boolean = true, compact: Boolean = false, modifier: Modifier = Modifier, onClick: () -> Unit) { val colors = MaterialTheme.colorScheme val src = remember { MutableInteractionSource() } val hot = rememberHot(src) && enabled @@ -1778,7 +1779,7 @@ internal fun SecondaryButton(text: String, enabled: Boolean = true, compact: Boo val edge by animateColorAsState(if (hot) pal.signal else pal.line2, Motion.tw(250), label = "secEdge") val fill by animateColorAsState(if (hot) pal.signal.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.03f), Motion.tw(250), label = "secFill") Box( - modifier = Modifier.paneItem("btn:$text").downToFirstTile().graphicsLayer { scaleX = scale; scaleY = scale }.clip(Shape12).background(fill).border(1.dp, edge, Shape12) + modifier = modifier.paneItem("btn:$text").downToFirstTile().graphicsLayer { scaleX = scale; scaleY = scale }.clip(Shape12).background(fill).border(1.dp, edge, Shape12) .alpha(if (enabled) 1f else 0.5f) .hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, enabled = enabled, onClick = onClick) .controllerConfirm(enabled = enabled, onClick = onClick) diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt index 532d93524..4a3244d3c 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.offset import androidx.compose.material.icons.Icons import androidx.compose.foundation.layout.heightIn +import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.outlined.Menu import androidx.compose.material.icons.outlined.MoreHoriz import androidx.compose.material.icons.outlined.PowerSettingsNew @@ -291,10 +292,17 @@ private fun StageRow(label: String, state: StageMark, detail: String, percent: I } } +/** + * A session paused in the background (the Manual background behaviour): what is paused, that it + * picks up where it left off, and the two ways on - Resume first, for the pad. + */ @Composable -fun SessionPausedOverlay(onResume: () -> Unit) { +fun SessionPausedOverlay(title: String = "Steam is paused", onResume: () -> Unit, onStop: (() -> Unit)? = null) { + val colors = MaterialTheme.colorScheme + val pal = LocalPalette.current val interactionSource = remember { MutableInteractionSource() } val resumeFocus = remember { FocusRequester() } + var confirmStop by remember { mutableStateOf(false) } LaunchedEffect(Unit) { androidx.compose.runtime.withFrameNanos { } runCatching { resumeFocus.requestFocus() } @@ -302,22 +310,55 @@ fun SessionPausedOverlay(onResume: () -> Unit) { Box( modifier = Modifier .fillMaxSize() - .background(Color(0xF20B0D10)) + .background(pal.background.copy(alpha = 0.94f)) .clickable(interactionSource = interactionSource, indication = null) {}, contentAlignment = Alignment.Center, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(32.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.widthIn(max = 440.dp).padding(32.dp), ) { - OutlinedButton( - onClick = onResume, - modifier = Modifier.focusRequester(resumeFocus).controllerConfirm(onClick = onResume), - ) { - Text("Resume session") + Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(bottom = 6.dp).size(64.dp).border(2.dp, pal.line2, CircleShape)) { + Icon(Icons.Filled.Pause, contentDescription = null, tint = colors.onBackground, modifier = Modifier.size(30.dp)) + } + Text(title, fontSize = 28.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, textAlign = TextAlign.Center) + Text( + "It stopped while DroidDeck was in the background and picks up where it left off.", + fontSize = 15.sp, lineHeight = 21.sp, color = colors.onSurfaceVariant, textAlign = TextAlign.Center, + ) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(top = 14.dp)) { + PrimaryButton("Resume", modifier = Modifier.focusRequester(resumeFocus).controllerConfirm(onClick = onResume), onClick = onResume) + if (onStop != null) SecondaryButton("Stop session") { confirmStop = true } } } } + if (confirmStop && onStop != null) { + val cancelFocus = remember { FocusRequester() } + val cancel = { confirmStop = false } + val stop = { confirmStop = false; onStop() } + LaunchedEffect(Unit) { + androidx.compose.runtime.withFrameNanos { } + runCatching { cancelFocus.requestFocus() } + } + // Opens on Cancel, as the menu's does: a stray A press never ends the session. + AlertDialog( + onDismissRequest = cancel, + modifier = Modifier.controllerBack(onBack = cancel), + title = { Text("Stop session?") }, + text = { Text("Everything running in this session closes.") }, + confirmButton = { + TextButton( + onClick = stop, + modifier = Modifier.controllerConfirm(onClick = stop), + colors = ButtonDefaults.textButtonColors(contentColor = colors.error), + ) { Text("Stop") } + }, + dismissButton = { + TextButton(onClick = cancel, modifier = Modifier.focusRequester(cancelFocus).controllerConfirm(onClick = cancel)) { Text("Cancel") } + }, + ) + } } /** Everything the drawer shows and does. */