Auto-commit after successful build: update app gameplay/UI, improve build setup

Changed files:\napp/build.gradle.kts
app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt
This commit is contained in:
Joe Tretter
2026-04-23 12:54:06 -05:00
parent 785ab046dc
commit 99b868912e
2 changed files with 83 additions and 10 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId = "com.kawomi.githugandroid"
minSdk = 26
targetSdk = 34
versionCode = 26
versionName = "0.1.25"
versionCode = 27
versionName = "0.1.26"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true

View File

@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
@@ -205,6 +206,7 @@ fun GitHugApp() {
val scope = rememberCoroutineScope()
val screenScrollState = rememberScrollState()
val workspaceHeight = (configuration.screenHeightDp.dp * 1.05f).coerceAtLeast(560.dp)
val screenHeightDp = configuration.screenHeightDp
var paneLayout by remember { mutableStateOf(defaultPaneLayout()) }
var currentLevelIndex by remember { mutableStateOf(0) }
@@ -214,6 +216,7 @@ fun GitHugApp() {
var suppressedImeEcho by remember { mutableStateOf<String?>(null) }
var output by remember { mutableStateOf(listOf(runtime.startupBanner())) }
var hintIndex by remember { mutableStateOf(0) }
var suggestionsExpanded by remember { mutableStateOf(false) }
var completedLevels by remember { mutableStateOf(setOf<String>()) }
var commandHistory by remember { mutableStateOf(listOf<String>()) }
var historyIndex by remember { mutableStateOf(-1) }
@@ -239,14 +242,32 @@ fun GitHugApp() {
}
}
fun applyRecommendedPaneWeights(persist: Boolean = true) {
updatePaneLayout(
transform = { layout ->
layout.copy(
weights = layout.weights + recommendedPaneWeights(
level = currentLevel,
levelCount = levels.size,
outputLineCount = output.size,
screenHeightDp = screenHeightDp,
)
)
},
persist = persist,
)
}
fun resetCurrentLevel(message: String = "Level reset.") {
repo = runtime.prepareLevel(currentLevel)
clearCommandInput()
suppressedImeEcho = null
output = listOf(message)
hintIndex = 0
suggestionsExpanded = false
historyIndex = -1
historyDraft = ""
applyRecommendedPaneWeights(persist = false)
}
fun loadLevel(index: Int) {
@@ -256,8 +277,17 @@ fun GitHugApp() {
clearCommandInput()
suppressedImeEcho = null
hintIndex = 0
suggestionsExpanded = false
historyIndex = -1
historyDraft = ""
paneLayout = paneLayout.copy(
weights = paneLayout.weights + recommendedPaneWeights(
level = levels[index],
levelCount = levels.size,
outputLineCount = 1,
screenHeightDp = screenHeightDp,
)
)
}
fun setCommandText(text: String) {
@@ -351,6 +381,7 @@ fun GitHugApp() {
} else {
repo = newRepo
output = newOutput
applyRecommendedPaneWeights(persist = false)
}
}
@@ -403,6 +434,8 @@ fun GitHugApp() {
exerciseContent = {
ExercisePane(
level = currentLevel,
suggestionsExpanded = suggestionsExpanded,
onToggleSuggestions = { suggestionsExpanded = !suggestionsExpanded },
onHint = {
val hint = currentLevel.hints.getOrNull(hintIndex) ?: "No more hints for this level."
output = output + "hint> $hint"
@@ -766,9 +799,7 @@ private fun LevelsPane(
onSelect: (Int) -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
levels.forEachIndexed { index, level ->
@@ -859,13 +890,13 @@ private fun VisualPane(repo: RepoState) {
@Composable
private fun ExercisePane(
level: Level,
suggestionsExpanded: Boolean,
onToggleSuggestions: () -> Unit,
onHint: () -> Unit,
onReset: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(level.title, style = MaterialTheme.typography.titleLarge, color = TextPrimary)
@@ -880,6 +911,12 @@ private fun ExercisePane(
) {
Text("Hint")
}
TextButton(
onClick = onToggleSuggestions,
colors = ButtonDefaults.textButtonColors(contentColor = Accent),
) {
Text("Suggestions")
}
TextButton(
onClick = onReset,
colors = ButtonDefaults.textButtonColors(contentColor = Accent),
@@ -887,7 +924,7 @@ private fun ExercisePane(
Text("Reset level")
}
}
if (level.commandSuggestions.isNotEmpty()) {
if (suggestionsExpanded && level.commandSuggestions.isNotEmpty()) {
Text("Suggestions", color = TextPrimary, fontWeight = FontWeight.Bold)
level.commandSuggestions.forEach { suggestion ->
Text(suggestion, color = TextSecondary, fontFamily = FontFamily.Monospace)
@@ -938,6 +975,7 @@ private fun TerminalPane(
onHistoryUp: () -> Unit,
onHistoryDown: () -> Unit,
) {
val configuration = LocalConfiguration.current
val focusRequester = remember { FocusRequester() }
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val focusManager = LocalFocusManager.current
@@ -945,16 +983,26 @@ private fun TerminalPane(
val horizontalScroll = rememberScrollState()
val outputVerticalScroll = rememberScrollState()
val terminalMinWidth = 80.dp * 7.2f
val maxOutputHeight = configuration.screenHeightDp.dp * 0.7f
val outputLineHeight = 20.dp
val desiredOutputHeight = (output.size.coerceAtLeast(6) * outputLineHeight.value).dp.coerceAtMost(maxOutputHeight)
LaunchedEffect(inputFieldVersion) {
focusRequester.requestFocus()
keyboardController?.show()
horizontalScroll.scrollTo(0)
}
LaunchedEffect(output.size) {
outputVerticalScroll.animateScrollTo(outputVerticalScroll.maxValue)
}
LaunchedEffect(output.size, commandInput.text) {
if (output.isNotEmpty() && commandInput.text.isEmpty()) {
horizontalScroll.scrollTo(0)
}
}
fun submitCommand() {
focusManager.clearFocus(force = true)
keyboardController?.hide()
@@ -976,7 +1024,8 @@ private fun TerminalPane(
) {
Box(
modifier = Modifier
.weight(1f)
.heightIn(min = 120.dp, max = maxOutputHeight)
.height(desiredOutputHeight)
.fillMaxWidth()
.background(PanelPrimary, RoundedCornerShape(10.dp))
.padding(8.dp)
@@ -1114,6 +1163,30 @@ private fun resizePanes(
)
}
private fun recommendedPaneWeights(
level: Level,
levelCount: Int,
outputLineCount: Int,
screenHeightDp: Int,
): Map<PaneId, Float> {
val screenHeight = screenHeightDp.coerceAtLeast(560)
val levelsHeight = (56 + levelCount * 44).coerceAtMost((screenHeight * 0.35f).toInt())
val descriptionLines = (level.description.length / 42).coerceAtLeast(2) + 2
val suggestionsLines = level.commandSuggestions.size
val exerciseHeight = (120 + descriptionLines * 24 + suggestionsLines * 22).coerceAtLeast(180)
val terminalHeight = (120 + outputLineCount.coerceAtLeast(1) * 20).coerceAtMost((screenHeight * 0.7f).toInt())
val visualHeight = (screenHeight * 0.24f).toInt().coerceAtLeast(170)
fun weightFor(height: Int): Float = (height.toFloat() / screenHeight.toFloat()).coerceAtLeast(0.6f)
return mapOf(
PaneId.EXERCISE to weightFor(exerciseHeight),
PaneId.TERMINAL to weightFor(terminalHeight),
PaneId.VISUAL to weightFor(visualHeight),
PaneId.LEVELS to weightFor(levelsHeight),
)
}
private fun commonPrefix(values: List<String>): String {
if (values.isEmpty()) return ""
var prefix = values.first()