Move build status to launcher rail footer

This commit is contained in:
xXJsonDeruloXx committed 2026-09-24 07:45:55 -04:00
1 parent 0cdb945d9c
commit 3e864fda17
1 file changed
+87 -86
@@ -54,12 +54,12 @@ 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.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -101,6 +101,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
@@ -291,82 +294,89 @@ private fun Rail(
) {
val colors = MaterialTheme.colorScheme
Column(
modifier = modifier.background(colors.surface).verticalScroll(rememberScrollState()).padding(horizontal = 10.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = modifier.background(colors.surface).padding(horizontal = 10.dp, vertical = 12.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 8.dp, bottom = 10.dp)) {
Image(painterResource(R.drawable.logo), null, modifier = Modifier.size(30.dp))
Spacer(Modifier.width(10.dp))
Column {
Text("DroidDeck", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = colors.onBackground)
val status = when {
s.busy -> if (s.percent >= 0) "${s.stage} ${s.percent}%" else s.stage
!s.ready -> "runtime not installed"
s.available != null && s.available != s.installed -> "runtime update available"
else -> null
Column(
modifier = Modifier.weight(1f).fillMaxWidth().verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 8.dp, bottom = 10.dp)) {
Image(painterResource(R.drawable.logo), null, modifier = Modifier.size(30.dp))
Spacer(Modifier.width(10.dp))
Column {
Text("DroidDeck", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = colors.onBackground)
val status = when {
s.busy -> if (s.percent >= 0) "${s.stage} ${s.percent}%" else s.stage
!s.ready -> "runtime not installed"
s.available != null && s.available != s.installed -> "runtime update available"
else -> null
}
if (status != null) Text(status, fontSize = 11.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
if (status != null) Text(status, fontSize = 11.sp, color = colors.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
AnimatedVisibility(s.busy, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut()) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp))
}
AnimatedVisibility(s.busy, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut()) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp))
}
var lastRunning by remember { mutableStateOf("") }
if (s.running != null) lastRunning = s.running
AnimatedVisibility(
s.running != null,
enter = expandVertically(Motion.sp(0.75f)) + fadeIn(Motion.tw(300)) + slideInVertically(Motion.sp(0.6f)) { -it / 2 },
exit = shrinkVertically(Motion.tw(220)) + fadeOut(Motion.tw(180)),
) { RunningTile(lastRunning, a.onResume) }
var lastRunning by remember { mutableStateOf("") }
if (s.running != null) lastRunning = s.running
AnimatedVisibility(
s.running != null,
enter = expandVertically(Motion.sp(0.75f)) + fadeIn(Motion.tw(300)) + slideInVertically(Motion.sp(0.6f)) { -it / 2 },
exit = shrinkVertically(Motion.tw(220)) + fadeOut(Motion.tw(180)),
) { RunningTile(lastRunning, a.onResume) }
val pal = LocalPalette.current
var navOrigin by remember { mutableStateOf(Offset.Zero) }
val positions = remember { mutableStateMapOf<String, Rect>() }
Box(modifier = Modifier.fillMaxWidth().onGloballyPositioned { val o = it.positionInRoot(); if (o != navOrigin) navOrigin = o }) {
val target = positions[selected]?.translate(-navOrigin)
val y by animateFloatAsState(target?.top ?: 0f, Motion.sp(0.72f, Spring.StiffnessLow), label = "indY")
val x by animateFloatAsState(target?.left ?: 0f, Motion.sp(0.9f), label = "indX")
val h by animateFloatAsState(target?.height ?: 0f, Motion.sp(1f, Spring.StiffnessMediumLow), label = "indH")
val w by animateFloatAsState(target?.width ?: 0f, Motion.sp(1f, Spring.StiffnessMediumLow), label = "indW")
val alpha by animateFloatAsState(if (target != null) 1f else 0f, Motion.tw(250), label = "indA")
val density = LocalDensity.current
Box(
modifier = Modifier
.offset { IntOffset(x.roundToInt(), y.roundToInt()) }
.width(with(density) { w.toDp() }).height(with(density) { h.toDp() })
.alpha(alpha)
.graphicsLayer { shadowElevation = 6.dp.toPx(); shape = Shape10; clip = false; ambientShadowColor = pal.signal; spotShadowColor = pal.signal }
.clip(Shape10)
.background(Brush.linearGradient(listOf(colors.primary, pal.primary2))),
val pal = LocalPalette.current
var navOrigin by remember { mutableStateOf(Offset.Zero) }
val positions = remember { mutableStateMapOf<String, Rect>() }
Box(modifier = Modifier.fillMaxWidth().onGloballyPositioned { val o = it.positionInRoot(); if (o != navOrigin) navOrigin = o }) {
val target = positions[selected]?.translate(-navOrigin)
val y by animateFloatAsState(target?.top ?: 0f, Motion.sp(0.72f, Spring.StiffnessLow), label = "indY")
val x by animateFloatAsState(target?.left ?: 0f, Motion.sp(0.9f), label = "indX")
val h by animateFloatAsState(target?.height ?: 0f, Motion.sp(1f, Spring.StiffnessMediumLow), label = "indH")
val w by animateFloatAsState(target?.width ?: 0f, Motion.sp(1f, Spring.StiffnessMediumLow), label = "indW")
val alpha by animateFloatAsState(if (target != null) 1f else 0f, Motion.tw(250), label = "indA")
val density = LocalDensity.current
Box(
modifier = Modifier
.offset { IntOffset(x.roundToInt(), y.roundToInt()) }
.width(with(density) { w.toDp() }).height(with(density) { h.toDp() })
.alpha(alpha)
.graphicsLayer { shadowElevation = 6.dp.toPx(); shape = Shape10; clip = false; ambientShadowColor = pal.signal; spotShadowColor = pal.signal }
.clip(Shape10)
.background(Brush.linearGradient(listOf(colors.primary, pal.primary2))),
)
val register: (String, LayoutCoordinates) -> Unit = { key, c ->
val o = c.positionInRoot(); val r = Rect(o.x, o.y, o.x + c.size.width, o.y + c.size.height)
if (positions[key] != r) positions[key] = r
}
val unregister: (String) -> Unit = { positions.remove(it) }
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(2.dp)) {
if (s.isHomeApp) {
NavItem("Android apps", "android-apps", selected == "android-apps", count = s.androidApps.size,
register = register, unregister = unregister) { onSelect("android-apps") }
}
NavItem("Steam", "steam", selected == "steam", count = s.steamGames.size, register = register, unregister = unregister) { onSelect("steam") }
NavItem("Desktop", "desktop", selected == "desktop", count = s.emulators.count { it.installed }, register = register, unregister = unregister) { onSelect("desktop") }
NavItem("Setup", "setup", selected == "setup", register = register, unregister = unregister) { onSelect("setup") }
Spacer(Modifier.height(6.dp))
}
}
val creditsSrc = remember { MutableInteractionSource() }
val creditsHot = rememberHot(creditsSrc)
Text(
"credits", fontSize = 11.sp, color = if (creditsHot) LocalPalette.current.signal else colors.onSurfaceVariant,
modifier = Modifier.clip(Shape10)
.border(2.dp, if (creditsHot) LocalPalette.current.signal else Color.Transparent, Shape10)
.hoverable(creditsSrc).clickable(interactionSource = creditsSrc, indication = null, onClick = a.onCredits)
.padding(horizontal = 10.dp, vertical = 8.dp),
)
val register: (String, LayoutCoordinates) -> Unit = { key, c ->
val o = c.positionInRoot(); val r = Rect(o.x, o.y, o.x + c.size.width, o.y + c.size.height)
if (positions[key] != r) positions[key] = r
}
val unregister: (String) -> Unit = { positions.remove(it) }
Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(2.dp)) {
if (s.isHomeApp) {
NavItem("Android apps", "android-apps", selected == "android-apps", count = s.androidApps.size,
register = register, unregister = unregister) { onSelect("android-apps") }
}
NavItem("Steam", "steam", selected == "steam", count = s.steamGames.size, register = register, unregister = unregister) { onSelect("steam") }
NavItem("Desktop", "desktop", selected == "desktop", count = s.emulators.count { it.installed }, register = register, unregister = unregister) { onSelect("desktop") }
NavItem("Setup", "setup", selected == "setup", register = register, unregister = unregister) { onSelect("setup") }
Spacer(Modifier.height(6.dp))
}
}
// Outlined when a controller is on it, like every other control; the ripple alone was
// a faint box on the dark rail.
val creditsSrc = remember { MutableInteractionSource() }
val creditsHot = rememberHot(creditsSrc)
Text(
"credits", fontSize = 11.sp, color = if (creditsHot) LocalPalette.current.signal else colors.onSurfaceVariant,
modifier = Modifier.clip(Shape10)
.border(2.dp, if (creditsHot) LocalPalette.current.signal else Color.Transparent, Shape10)
.hoverable(creditsSrc).clickable(interactionSource = creditsSrc, indication = null, onClick = a.onCredits)
.padding(horizontal = 10.dp, vertical = 8.dp),
BuildStatus(
label = s.buildLabel,
onCheckLatest = a.onCheckLatestBuild,
modifier = Modifier.align(Alignment.Start).padding(start = 8.dp, top = 4.dp, bottom = 2.dp),
)
}
}
@@ -471,13 +481,6 @@ private fun Pane(s: FrontEndState, selected: String, a: FrontEndActions, page: (
},
label = "pane",
) { key -> if (page != null && key == s.pageKey) page() else Content(s, key, a, Modifier.fillMaxSize(), onSelect) }
if (page == null && s.pageKey == null && selected == "steam") {
BuildStatus(
label = s.buildLabel,
onCheckLatest = a.onCheckLatestBuild,
modifier = Modifier.align(Alignment.TopEnd).padding(12.dp),
)
}
}
}
@@ -489,12 +492,10 @@ private fun BuildStatus(label: String, onCheckLatest: () -> Unit, modifier: Modi
val hot = rememberHot(interaction)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = modifier
.widthIn(max = 360.dp)
.clip(Shape12)
.background(colors.surface.copy(alpha = 0.96f))
.border(1.5.dp, if (hot) pal.signal else pal.line2, Shape12)
.clip(RoundedCornerShape(6.dp))
.semantics { contentDescription = "Build $label. Check for newest build." }
.hoverable(interaction)
.clickable(
interactionSource = interaction,
@@ -502,11 +503,11 @@ private fun BuildStatus(label: String, onCheckLatest: () -> Unit, modifier: Modi
role = Role.Button,
onClick = onCheckLatest,
)
.padding(horizontal = 11.dp, vertical = 7.dp),
.heightIn(min = 40.dp)
.padding(horizontal = 8.dp),
) {
Text(label, modifier = Modifier.weight(1f), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis)
Icon(Icons.Filled.Refresh, contentDescription = null, tint = if (hot) pal.signal else colors.onSurfaceVariant, modifier = Modifier.size(15.dp))
Text("Check latest", fontSize = 11.sp, fontWeight = FontWeight.SemiBold, color = if (hot) pal.signal else colors.onBackground, maxLines = 1)
Text(label, fontSize = 10.sp, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium, color = if (hot) colors.onSurfaceVariant else colors.onSurfaceVariant.copy(alpha = 0.72f), maxLines = 1, overflow = TextOverflow.Ellipsis)
Icon(Icons.Filled.Refresh, contentDescription = null, tint = if (hot) pal.signal else colors.onSurfaceVariant.copy(alpha = 0.72f), modifier = Modifier.size(12.dp))
}
}