diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2995b69..9e48fed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.kawomi.githugandroid" minSdk = 26 targetSdk = 34 - versionCode = 5 - versionName = "0.1.4" + versionCode = 6 + versionName = "0.1.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -70,7 +70,6 @@ dependencies { implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.foundation:foundation") - implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material3:material3:1.2.1") implementation("com.google.android.material:material:1.12.0") diff --git a/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt b/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt index 820fdba..cc16f38 100644 --- a/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt +++ b/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt @@ -8,19 +8,12 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.ArrowForward -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material.icons.filled.KeyboardTab import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -39,6 +32,8 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -47,6 +42,8 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay private val AppBackground = Color(0xFF000000) private val PanelPrimary = Color(0xFF121212) @@ -83,6 +80,7 @@ fun GitHugApp() { var repo by remember { mutableStateOf(levels.first().setup()) } var commandInput by remember { mutableStateOf(TextFieldValue("")) } var inputFieldVersion by remember { mutableStateOf(0) } + var suppressedImeEcho by remember { mutableStateOf(null) } var output by remember { mutableStateOf(listOf("Welcome to GitHug Android.")) } var hintIndex by remember { mutableStateOf(0) } var completedLevels by remember { mutableStateOf(setOf()) } @@ -91,20 +89,21 @@ fun GitHugApp() { var historyDraft by remember { mutableStateOf("") } val currentLevel = levels[currentLevelIndex] - val solved = currentLevel.validator(repo) - LaunchedEffect(currentLevelIndex) { screenScrollState.scrollTo(0) } - fun clearCommandInput() { + fun clearCommandInput(recreateField: Boolean = false) { commandInput = TextFieldValue(text = "", selection = TextRange.Zero) - inputFieldVersion += 1 + if (recreateField) { + inputFieldVersion += 1 + } } fun resetCurrentLevel(message: String = "Level reset.") { repo = currentLevel.setup() clearCommandInput() + suppressedImeEcho = null output = listOf(message) hintIndex = 0 historyIndex = -1 @@ -164,10 +163,12 @@ fun GitHugApp() { } fun runCommand() { - val raw = commandInput.text.trim() + val submittedText = commandInput.text + val raw = submittedText.trim() if (raw.isBlank()) return - clearCommandInput() + suppressedImeEcho = submittedText + clearCommandInput(recreateField = true) if (commandHistory.lastOrNull() != raw) { commandHistory = commandHistory + raw @@ -196,11 +197,13 @@ fun GitHugApp() { repo = nextLevel.setup() output = listOf("Loaded level: ${nextLevel.title}") clearCommandInput() + suppressedImeEcho = null hintIndex = 0 } else { repo = newRepo output = listOf("🏁 All available MVP levels completed.") clearCommandInput() + suppressedImeEcho = null } } else { repo = newRepo @@ -227,6 +230,7 @@ fun GitHugApp() { repo = levels[index].setup() output = listOf("Loaded level: ${levels[index].title}") clearCommandInput() + suppressedImeEcho = null hintIndex = 0 historyIndex = -1 historyDraft = "" @@ -248,7 +252,17 @@ fun GitHugApp() { output = output, inputFieldVersion = inputFieldVersion, commandInput = commandInput, - onValueChange = { commandInput = it }, + onValueChange = { + val blockedEcho = suppressedImeEcho + if (blockedEcho != null && commandInput.text.isEmpty()) { + val blockedTrimmed = blockedEcho.trim() + if (it.text == blockedEcho || (blockedTrimmed.isNotEmpty() && it.text == blockedTrimmed)) { + return@TerminalPanel + } + } + suppressedImeEcho = null + commandInput = it + }, onRun = { runCommand() }, onTab = { tabComplete() }, onCursorLeft = { moveCursor(-1) }, @@ -376,9 +390,20 @@ private fun TerminalPanel( modifier: Modifier = Modifier, ) { val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current LaunchedEffect(inputFieldVersion) { + if (inputFieldVersion == 0) return@LaunchedEffect + delay(75) focusRequester.requestFocus() + keyboardController?.show() + } + + fun submitCommand() { + focusManager.clearFocus(force = true) + keyboardController?.hide() + onRun() } Card(modifier = modifier, colors = CardDefaults.cardColors(containerColor = TerminalBackground)) { @@ -427,7 +452,7 @@ private fun TerminalPanel( keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done, ), - keyboardActions = KeyboardActions(onDone = { onRun() }) + keyboardActions = KeyboardActions(onDone = { submitCommand() }) ) } } @@ -448,29 +473,35 @@ private fun SpecialKeyBar( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - TerminalKeyButton(icon = { Icon(Icons.Filled.KeyboardTab, contentDescription = "Tab") }, onClick = onTab, modifier = Modifier.weight(1.3f)) - TerminalKeyButton(icon = { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Move cursor left") }, onClick = onCursorLeft) - TerminalKeyButton(icon = { Icon(Icons.AutoMirrored.Filled.ArrowForward, contentDescription = "Move cursor right") }, onClick = onCursorRight) - TerminalKeyButton(icon = { Icon(Icons.Filled.KeyboardArrowUp, contentDescription = "Previous command") }, onClick = onHistoryUp) - TerminalKeyButton(icon = { Icon(Icons.Filled.KeyboardArrowDown, contentDescription = "Next command") }, onClick = onHistoryDown) + TerminalKeyButton(label = "TAB", onClick = onTab, modifier = Modifier.width(68.dp)) + TerminalKeyButton(label = "←", onClick = onCursorLeft, modifier = Modifier.width(48.dp)) + TerminalKeyButton(label = "→", onClick = onCursorRight, modifier = Modifier.width(48.dp)) + TerminalKeyButton(label = "↑", onClick = onHistoryUp, modifier = Modifier.width(48.dp)) + TerminalKeyButton(label = "↓", onClick = onHistoryDown, modifier = Modifier.width(48.dp)) } } @Composable private fun TerminalKeyButton( - icon: @Composable () -> Unit, + label: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Button( onClick = onClick, - modifier = modifier, + modifier = modifier.height(34.dp), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp), colors = ButtonDefaults.buttonColors( containerColor = PanelSecondary, contentColor = TextPrimary ) ) { - icon() + Text( + text = label, + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + fontSize = 13.sp + ) } }