Add hidden diagnostic skin toggle

- Open a hidden diagnostics dialog after tapping the header seven times within fifteen seconds.
- Let the diagnostics dialog toggle the pane color/stripe skin on demand.
- Restore the normal app background and pane colors by default.
- Stop Scaffold from preserving the bottom navigation-bar inset while the keyboard is visible.
- Apply bottom navigation padding only when the IME is not open.
This commit is contained in:
Joe Tretter
2026-05-13 18:35:36 -05:00
parent bd6568357f
commit 618e6e36bd
7 changed files with 142 additions and 21 deletions

View File

@@ -0,0 +1,42 @@
package solutions.tretter.githugandroid
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun DiagnosticSkinDialog(
diagnosticSkinEnabled: Boolean,
onDiagnosticSkinEnabledChange: (Boolean) -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Diagnostics") },
text = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text("Pane color skin")
Switch(
checked = diagnosticSkinEnabled,
onCheckedChange = onDiagnosticSkinEnabledChange,
)
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text("Close")
}
},
)
}

View File

@@ -1,14 +1,18 @@
package solutions.tretter.githugandroid
import android.os.SystemClock
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
@@ -26,6 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
@@ -37,6 +42,7 @@ fun GitHugApp() {
MaterialTheme(colorScheme = GitHugColorScheme) {
val context = LocalContext.current
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val runtime = remember(context) { GitRepositoryRuntime(context.applicationContext) }
if (!runtime.isNativeGitAvailable()) {
MissingNativeGitScreen(message = runtime.unavailableMessage())
@@ -76,9 +82,18 @@ fun GitHugApp() {
var showHelpOverlay by rememberSaveable { mutableStateOf(false) }
var showHelpOnStart by rememberSaveable { mutableStateOf(true) }
var helpPreferenceInitialized by rememberSaveable { mutableStateOf(false) }
var showDiagnosticDialog by rememberSaveable { mutableStateOf(false) }
var diagnosticSkinEnabled by rememberSaveable { mutableStateOf(false) }
var headerTapTimes by remember { mutableStateOf(emptyList<Long>()) }
var exerciseBounds by remember { mutableStateOf<Rect?>(null) }
var promptBounds by remember { mutableStateOf<Rect?>(null) }
val currentLevel = levels[currentLevelIndex]
val appBackground = if (diagnosticSkinEnabled) DiagnosticAppBackground else AppBackground
val imeBottom = WindowInsets.ime.getBottom(density)
val navigationBottom = WindowInsets.navigationBars.getBottom(density)
val bottomSystemPadding = with(density) {
if (imeBottom > navigationBottom) 0.dp else navigationBottom.toDp()
}
LaunchedEffect(solvedCelebrationTitle) {
if (solvedCelebrationTitle != null) {
@@ -465,30 +480,50 @@ fun GitHugApp() {
output = output + runtime.commandReferenceLines()
}
Scaffold(containerColor = AppBackground) { padding ->
fun registerHeaderTap() {
val now = SystemClock.elapsedRealtime()
val recentTaps = (headerTapTimes + now).filter { tapTime -> now - tapTime <= 15_000L }
if (recentTaps.size >= 7) {
headerTapTimes = emptyList()
showDiagnosticDialog = true
} else {
headerTapTimes = recentTaps
}
}
Scaffold(
containerColor = appBackground,
contentWindowInsets = WindowInsets(0.dp),
) { padding ->
Surface(
modifier = Modifier
.fillMaxSize()
.padding(padding),
color = AppBackground,
color = appBackground,
) {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.background(AppBackground)
.imePadding()
.background(appBackground)
.statusBarsPadding()
.verticalScroll(screenScrollState)
.padding(horizontal = 10.dp, vertical = 8.dp),
.padding(
start = 10.dp,
top = 8.dp,
end = 10.dp,
bottom = 8.dp + bottomSystemPadding,
),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
FixedHeader()
FixedHeader(onTap = { registerHeaderTap() })
Box(
modifier = Modifier.fillMaxWidth(),
) {
PaneWorkspace(
modifier = Modifier.fillMaxWidth(),
paneLayout = paneLayout,
diagnosticSkinEnabled = diagnosticSkinEnabled,
onMovePane = { paneId, delta ->
updatePaneLayout(transform = { layout -> movePane(layout, paneId, delta) })
},
@@ -623,6 +658,14 @@ fun GitHugApp() {
)
}
if (showDiagnosticDialog) {
DiagnosticSkinDialog(
diagnosticSkinEnabled = diagnosticSkinEnabled,
onDiagnosticSkinEnabledChange = { diagnosticSkinEnabled = it },
onDismiss = { showDiagnosticDialog = false },
)
}
solvedCelebrationTitle?.let { title ->
SolvedCelebrationOverlay(title = title)
}

View File

@@ -6,7 +6,8 @@ import androidx.compose.ui.graphics.Color
val PanelPrimary = Color(0xFF121212)
val PanelSecondary = Color(0xFF1E1E1E)
val PanelTertiary = Color(0xFF262626)
val AppBackground = Color(0xFF2D1745)
val AppBackground = PanelSecondary
val DiagnosticAppBackground = Color(0xFF2D1745)
val TerminalBackground = Color(0xFF050505)
val TextPrimary = Color(0xFFFFFFFF)
val TextSecondary = Color(0xFFE6E6E6)

View File

@@ -1,6 +1,7 @@
package solutions.tretter.githugandroid
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -18,8 +19,11 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@Composable
fun FixedHeader() {
fun FixedHeader(onTap: () -> Unit = {}) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onTap),
colors = CardDefaults.cardColors(containerColor = PanelPrimary),
shape = RoundedCornerShape(14.dp),
) {
@@ -38,4 +42,4 @@ fun FixedHeader() {
Text("GitHug Android", style = MaterialTheme.typography.titleLarge, color = TextPrimary)
}
}
}
}

View File

@@ -29,6 +29,7 @@ import androidx.compose.ui.unit.sp
fun PaneWorkspace(
modifier: Modifier = Modifier,
paneLayout: PaneLayout,
diagnosticSkinEnabled: Boolean = false,
onMovePane: (PaneId, Int) -> Unit,
onTogglePane: (PaneId) -> Unit,
levelsContent: @Composable () -> Unit,
@@ -47,6 +48,7 @@ fun PaneWorkspace(
if (isCollapsed) {
CollapsedPaneCard(
paneId = paneId,
diagnosticSkinEnabled = diagnosticSkinEnabled,
canMoveUp = index > 0,
canMoveDown = index < paneLayout.order.lastIndex,
onMoveUp = { onMovePane(paneId, -1) },
@@ -56,6 +58,7 @@ fun PaneWorkspace(
} else {
PaneCard(
paneId = paneId,
diagnosticSkinEnabled = diagnosticSkinEnabled,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
@@ -80,6 +83,7 @@ fun PaneWorkspace(
@Composable
private fun PaneCard(
paneId: PaneId,
diagnosticSkinEnabled: Boolean,
modifier: Modifier = Modifier,
canMoveUp: Boolean,
canMoveDown: Boolean,
@@ -90,18 +94,20 @@ private fun PaneCard(
) {
Card(
modifier = modifier,
colors = CardDefaults.cardColors(containerColor = paneDiagnosticBase(paneId)),
colors = CardDefaults.cardColors(
containerColor = if (diagnosticSkinEnabled) paneDiagnosticBase(paneId) else PanelSecondary,
),
shape = RoundedCornerShape(16.dp),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(paneDiagnosticBase(paneId))
.paneDiagnosticPattern(paneId),
.diagnosticPaneBackground(diagnosticSkinEnabled, paneId, paneDiagnosticBase(paneId)),
) {
PaneHeader(
paneId = paneId,
diagnosticSkinEnabled = diagnosticSkinEnabled,
collapsed = false,
canMoveUp = canMoveUp,
canMoveDown = canMoveDown,
@@ -113,8 +119,7 @@ private fun PaneCard(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.background(paneDiagnosticContent(paneId))
.paneDiagnosticPattern(paneId)
.diagnosticPaneBackground(diagnosticSkinEnabled, paneId, paneDiagnosticContent(paneId))
.padding(horizontal = 12.dp, vertical = 10.dp),
) {
content()
@@ -126,6 +131,7 @@ private fun PaneCard(
@Composable
private fun CollapsedPaneCard(
paneId: PaneId,
diagnosticSkinEnabled: Boolean,
canMoveUp: Boolean,
canMoveDown: Boolean,
onMoveUp: () -> Unit,
@@ -134,11 +140,14 @@ private fun CollapsedPaneCard(
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = paneDiagnosticBase(paneId)),
colors = CardDefaults.cardColors(
containerColor = if (diagnosticSkinEnabled) paneDiagnosticBase(paneId) else PanelSecondary,
),
shape = RoundedCornerShape(16.dp),
) {
PaneHeader(
paneId = paneId,
diagnosticSkinEnabled = diagnosticSkinEnabled,
collapsed = true,
canMoveUp = canMoveUp,
canMoveDown = canMoveDown,
@@ -152,6 +161,7 @@ private fun CollapsedPaneCard(
@Composable
private fun PaneHeader(
paneId: PaneId,
diagnosticSkinEnabled: Boolean,
collapsed: Boolean,
canMoveUp: Boolean,
canMoveDown: Boolean,
@@ -162,8 +172,16 @@ private fun PaneHeader(
Row(
modifier = Modifier
.fillMaxWidth()
.background(if (collapsed) paneDiagnosticCollapsed(paneId) else paneDiagnosticHeader(paneId))
.paneDiagnosticPattern(paneId)
.background(
if (diagnosticSkinEnabled) {
if (collapsed) paneDiagnosticCollapsed(paneId) else paneDiagnosticHeader(paneId)
} else if (collapsed) {
PanelTertiary
} else {
PanelPrimary
}
)
.diagnosticPanePattern(diagnosticSkinEnabled, paneId)
.padding(horizontal = 10.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
@@ -200,6 +218,18 @@ private fun paneDiagnosticHeader(paneId: PaneId): Color = when (paneId) {
private fun paneDiagnosticCollapsed(paneId: PaneId): Color = paneDiagnosticContent(paneId)
private fun Modifier.diagnosticPaneBackground(enabled: Boolean, paneId: PaneId, color: Color): Modifier {
return if (enabled) {
background(color).diagnosticPanePattern(enabled = true, paneId = paneId)
} else {
this
}
}
private fun Modifier.diagnosticPanePattern(enabled: Boolean, paneId: PaneId): Modifier {
return if (enabled) paneDiagnosticPattern(paneId) else this
}
private fun Modifier.paneDiagnosticPattern(paneId: PaneId): Modifier = drawBehind {
val stripeColor = when (paneId) {
PaneId.EXERCISE -> Color(0x6684C5FF)