diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d0aab14..512ae0a 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 = 20 - versionName = "0.1.19" + versionCode = 22 + versionName = "0.1.21" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt b/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt index 5f7131d..78b9a6a 100644 --- a/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt +++ b/app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt @@ -7,16 +7,19 @@ import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +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.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -35,6 +38,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextButton import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable @@ -50,6 +55,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalConfiguration @@ -66,6 +72,8 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.dp +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences @@ -119,10 +127,10 @@ private fun defaultPaneLayout(): PaneLayout = PaneLayout( order = listOf(PaneId.LEVELS, PaneId.VISUAL, PaneId.EXERCISE, PaneId.TERMINAL), collapsed = emptySet(), weights = mapOf( - PaneId.LEVELS to 1.1f, - PaneId.VISUAL to 1.3f, - PaneId.EXERCISE to 1.0f, - PaneId.TERMINAL to 1.8f, + PaneId.LEVELS to 0.9f, + PaneId.VISUAL to 1.0f, + PaneId.EXERCISE to 0.9f, + PaneId.TERMINAL to 1.4f, ), ) @@ -190,11 +198,14 @@ private class PaneLayoutStore(private val context: Context) { fun GitHugApp() { MaterialTheme(colorScheme = GitHugColorScheme) { val context = LocalContext.current + val configuration = LocalConfiguration.current val levels = remember { sampleLevels() } val runtime = remember(context) { GitRepositoryRuntime(context.applicationContext) } val paneLayoutStore = remember(context) { PaneLayoutStore(context.applicationContext) } val persistedPaneLayout by paneLayoutStore.layoutFlow.collectAsState(initial = defaultPaneLayout()) val scope = rememberCoroutineScope() + val screenScrollState = rememberScrollState() + val workspaceHeight = (configuration.screenHeightDp.dp * 0.72f).coerceAtLeast(420.dp) var paneLayout by remember { mutableStateOf(defaultPaneLayout()) } var currentLevelIndex by remember { mutableStateOf(0) } @@ -355,13 +366,18 @@ fun GitHugApp() { ) { Column( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .background(AppBackground) - .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + .verticalScroll(screenScrollState) + .imePadding() + .padding(horizontal = 10.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { FixedHeader() PaneWorkspace( + modifier = Modifier + .fillMaxWidth() + .height(workspaceHeight), paneLayout = paneLayout, onMovePane = { paneId, delta -> updatePaneLayout { layout -> movePane(layout, paneId, delta) } @@ -417,6 +433,7 @@ fun GitHugApp() { ) }, ) + Spacer(modifier = Modifier.height(220.dp)) } } } @@ -427,30 +444,28 @@ fun GitHugApp() { private fun FixedHeader() { Card( colors = CardDefaults.cardColors(containerColor = PanelPrimary), - shape = RoundedCornerShape(18.dp), + shape = RoundedCornerShape(14.dp), ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 14.dp), + .padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { Image( painter = painterResource(id = R.drawable.githug_android_logo), contentDescription = "GitHug Android logo", - modifier = Modifier.size(40.dp), + modifier = Modifier.size(28.dp), ) - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text("GitHug Android", style = MaterialTheme.typography.headlineSmall, color = TextPrimary) - Text("Customizable panes for Git learning on phone and tablet.", color = TextSecondary) - } + Text("GitHug Android", style = MaterialTheme.typography.titleLarge, color = TextPrimary) } } } @Composable private fun PaneWorkspace( + modifier: Modifier = Modifier, paneLayout: PaneLayout, onMovePane: (PaneId, Int) -> Unit, onTogglePane: (PaneId) -> Unit, @@ -460,7 +475,7 @@ private fun PaneWorkspace( exerciseContent: @Composable () -> Unit, terminalContent: @Composable () -> Unit, ) { - BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + BoxWithConstraints(modifier = modifier) { val heightBasis = maxHeight.value.takeIf { it > 0f } ?: 1f Column( @@ -592,7 +607,7 @@ private fun PaneHeader( modifier = Modifier .fillMaxWidth() .background(if (collapsed) PanelTertiary else PanelPrimary) - .padding(horizontal = 12.dp, vertical = 10.dp), + .padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { @@ -614,13 +629,13 @@ private fun HeaderActionButton( TextButton( onClick = onClick, enabled = enabled, - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp), + contentPadding = PaddingValues(horizontal = 6.dp, vertical = 0.dp), colors = ButtonDefaults.textButtonColors( contentColor = Accent, disabledContentColor = TextMuted, ), ) { - Text(label, fontSize = 12.sp) + Text(label, fontSize = 11.sp) } } @@ -636,7 +651,7 @@ private fun PaneDivider( Box( modifier = Modifier .fillMaxWidth() - .height(12.dp) + .height(6.dp) .draggable( state = dragState, orientation = Orientation.Vertical, @@ -647,7 +662,7 @@ private fun PaneDivider( Box( modifier = Modifier .fillMaxWidth() - .height(2.dp) + .height(1.dp) .background(if (enabled) Accent.copy(alpha = 0.45f) else PanelTertiary), ) } @@ -671,7 +686,7 @@ private fun LevelsPane( colors = CardDefaults.cardColors( containerColor = if (index == currentLevelIndex) PanelTertiary else PanelPrimary, ), - shape = RoundedCornerShape(12.dp), + shape = RoundedCornerShape(10.dp), ) { TextButton( onClick = { onSelect(index) }, @@ -800,13 +815,13 @@ private fun InfoPaneCard( Card( modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = PanelPrimary), - shape = RoundedCornerShape(14.dp), + shape = RoundedCornerShape(10.dp), ) { Column( modifier = Modifier .fillMaxSize() - .padding(12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text(title, color = TextPrimary, fontWeight = FontWeight.Bold) Text( @@ -818,6 +833,7 @@ private fun InfoPaneCard( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun TerminalPane( output: List, @@ -833,6 +849,7 @@ private fun TerminalPane( onHistoryDown: () -> Unit, ) { val focusRequester = remember { FocusRequester() } + val bringIntoViewRequester = remember { BringIntoViewRequester() } val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current val horizontalScroll = rememberScrollState() @@ -846,6 +863,10 @@ private fun TerminalPane( keyboardController?.show() } + LaunchedEffect(output.size) { + outputVerticalScroll.animateScrollTo(outputVerticalScroll.maxValue) + } + fun submitCommand() { focusManager.clearFocus(force = true) keyboardController?.hide() @@ -856,7 +877,7 @@ private fun TerminalPane( modifier = Modifier .fillMaxSize() .background(TerminalBackground, RoundedCornerShape(12.dp)) - .padding(12.dp) + .padding(8.dp) .horizontalScroll(horizontalScroll), ) { Column( @@ -871,7 +892,7 @@ private fun TerminalPane( .weight(1f) .fillMaxWidth() .background(PanelPrimary, RoundedCornerShape(10.dp)) - .padding(12.dp) + .padding(8.dp) .verticalScroll(outputVerticalScroll), ) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { @@ -894,9 +915,10 @@ private fun TerminalPane( ) Row( modifier = Modifier + .bringIntoViewRequester(bringIntoViewRequester) .fillMaxWidth() .background(PanelPrimary, RoundedCornerShape(8.dp)) - .padding(horizontal = 12.dp, vertical = 10.dp), + .padding(horizontal = 8.dp, vertical = 4.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -907,15 +929,30 @@ private fun TerminalPane( fontWeight = FontWeight.Bold, ) key(inputFieldVersion) { - BasicTextField( + TextField( value = commandInput, onValueChange = onValueChange, modifier = Modifier .weight(1f) - .focusRequester(focusRequester), + .focusRequester(focusRequester) + .onFocusChanged { state -> + if (state.isFocused) { + keyboardController?.show() + } + }, singleLine = true, textStyle = TextStyle(color = TextPrimary, fontFamily = FontFamily.Monospace), - cursorBrush = SolidColor(Accent), + colors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + cursorColor = Accent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + focusedTextColor = TextPrimary, + unfocusedTextColor = TextPrimary, + ), keyboardOptions = KeyboardOptions( autoCorrect = false, keyboardType = KeyboardType.Ascii, @@ -960,8 +997,8 @@ private fun TerminalKeyButton( ) { Button( onClick = onClick, - modifier = modifier.height(34.dp), - contentPadding = PaddingValues(horizontal = 10.dp, vertical = 4.dp), + modifier = modifier.height(28.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp), colors = ButtonDefaults.buttonColors( containerColor = PanelSecondary, contentColor = TextPrimary, @@ -971,7 +1008,7 @@ private fun TerminalKeyButton( text = label, fontFamily = fontFamily, fontWeight = FontWeight.Bold, - fontSize = 13.sp, + fontSize = 11.sp, ) } }