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 a1e7a2a19..07c70f0b4 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -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() } - 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() } + 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)) } }