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

@@ -19,8 +19,8 @@ android {
applicationId = "solutions.tretter.githugandroid" applicationId = "solutions.tretter.githugandroid"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 142 versionCode = 143
versionName = "0.1.141" versionName = "0.1.142"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

View File

@@ -11,7 +11,8 @@
android:theme="@style/Theme.GithugAndroid"> android:theme="@style/Theme.GithugAndroid">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

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 package solutions.tretter.githugandroid
import android.os.SystemClock
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
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.imePadding import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -26,6 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -37,6 +42,7 @@ fun GitHugApp() {
MaterialTheme(colorScheme = GitHugColorScheme) { MaterialTheme(colorScheme = GitHugColorScheme) {
val context = LocalContext.current val context = LocalContext.current
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val density = LocalDensity.current
val runtime = remember(context) { GitRepositoryRuntime(context.applicationContext) } val runtime = remember(context) { GitRepositoryRuntime(context.applicationContext) }
if (!runtime.isNativeGitAvailable()) { if (!runtime.isNativeGitAvailable()) {
MissingNativeGitScreen(message = runtime.unavailableMessage()) MissingNativeGitScreen(message = runtime.unavailableMessage())
@@ -76,9 +82,18 @@ fun GitHugApp() {
var showHelpOverlay by rememberSaveable { mutableStateOf(false) } var showHelpOverlay by rememberSaveable { mutableStateOf(false) }
var showHelpOnStart by rememberSaveable { mutableStateOf(true) } var showHelpOnStart by rememberSaveable { mutableStateOf(true) }
var helpPreferenceInitialized by rememberSaveable { mutableStateOf(false) } 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 exerciseBounds by remember { mutableStateOf<Rect?>(null) }
var promptBounds by remember { mutableStateOf<Rect?>(null) } var promptBounds by remember { mutableStateOf<Rect?>(null) }
val currentLevel = levels[currentLevelIndex] 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) { LaunchedEffect(solvedCelebrationTitle) {
if (solvedCelebrationTitle != null) { if (solvedCelebrationTitle != null) {
@@ -465,30 +480,50 @@ fun GitHugApp() {
output = output + runtime.commandReferenceLines() 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( Surface(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding), .padding(padding),
color = AppBackground, color = appBackground,
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(AppBackground) .background(appBackground)
.imePadding() .statusBarsPadding()
.verticalScroll(screenScrollState) .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), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
FixedHeader() FixedHeader(onTap = { registerHeaderTap() })
Box( Box(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
PaneWorkspace( PaneWorkspace(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
paneLayout = paneLayout, paneLayout = paneLayout,
diagnosticSkinEnabled = diagnosticSkinEnabled,
onMovePane = { paneId, delta -> onMovePane = { paneId, delta ->
updatePaneLayout(transform = { layout -> movePane(layout, 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 -> solvedCelebrationTitle?.let { title ->
SolvedCelebrationOverlay(title = title) SolvedCelebrationOverlay(title = title)
} }

View File

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

View File

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

View File

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