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