feat(session): explain paused sessions and offer stop

This commit is contained in:
xXJsonDeruloXx committed 2026-09-26 16:42:28 -04:00
1 parent 5ebafaabda
commit 5d5b45a012
3 files changed
+65 -14

No files matched your search

@@ -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"
@@ -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)
@@ -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. */