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/Exercise.kt app/src/main/java/com/kawomi/githugandroid/GitHugApp.kt app/src/main/java/com/kawomi/githugandroid/Header.kt app/src/main/java/com/kawomi/githugandroid/Levels.kt app/src/main/java/com/kawomi/githugandroid/Panes.kt app/src/main/java/com/kawomi/githugandroid/Terminal.kt app/src/main/java/com/kawomi/githugandroid/Visual.kt
This commit is contained in:
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.kawomi.githugandroid"
|
applicationId = "com.kawomi.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 34
|
versionCode = 38
|
||||||
versionName = "0.1.33"
|
versionName = "0.1.37"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
68
app/src/main/java/com/kawomi/githugandroid/Exercise.kt
Normal file
68
app/src/main/java/com/kawomi/githugandroid/Exercise.kt
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package com.kawomi.githugandroid
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExercisePane(
|
||||||
|
level: Level,
|
||||||
|
visibleHint: String?,
|
||||||
|
suggestionsExpanded: Boolean,
|
||||||
|
onToggleSuggestions: () -> Unit,
|
||||||
|
onHint: () -> Unit,
|
||||||
|
onReset: () -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Text(level.title, style = MaterialTheme.typography.titleLarge, color = TextPrimary)
|
||||||
|
Text(level.description, color = TextSecondary)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Button(
|
||||||
|
onClick = onHint,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = Accent,
|
||||||
|
contentColor = Color.Black,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text("Hint")
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = onToggleSuggestions,
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = Accent),
|
||||||
|
) {
|
||||||
|
Text("Suggestions")
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = onReset,
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = Accent),
|
||||||
|
) {
|
||||||
|
Text("Reset level")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (suggestionsExpanded && level.commandSuggestions.isNotEmpty()) {
|
||||||
|
Text("Suggestions", color = TextPrimary, fontWeight = FontWeight.Bold)
|
||||||
|
level.commandSuggestions.forEach { suggestion ->
|
||||||
|
Text(suggestion, color = TextSecondary, fontFamily = FontFamily.Monospace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (visibleHint != null) {
|
||||||
|
Text("Hint", color = TextPrimary, fontWeight = FontWeight.Bold)
|
||||||
|
Text(visibleHint, color = TextSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,78 +1,33 @@
|
|||||||
package com.kawomi.githugandroid
|
package com.kawomi.githugandroid
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
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.heightIn
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
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.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
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
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.TextRange
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
|
||||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -393,660 +348,3 @@ fun GitHugApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun FixedHeader() {
|
|
||||||
Card(
|
|
||||||
colors = CardDefaults.cardColors(containerColor = PanelPrimary),
|
|
||||||
shape = RoundedCornerShape(14.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.drawable.githug_android_logo),
|
|
||||||
contentDescription = "GitHug Android logo",
|
|
||||||
modifier = Modifier.size(28.dp),
|
|
||||||
)
|
|
||||||
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,
|
|
||||||
onResize: (PaneId, PaneId, Float) -> Unit,
|
|
||||||
onResizeFinished: () -> Unit,
|
|
||||||
levelsContent: @Composable () -> Unit,
|
|
||||||
visualContent: @Composable () -> Unit,
|
|
||||||
exerciseContent: @Composable () -> Unit,
|
|
||||||
terminalContent: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
BoxWithConstraints(modifier = modifier) {
|
|
||||||
val heightBasis = maxHeight.value.takeIf { it > 0f } ?: 1f
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
|
||||||
) {
|
|
||||||
paneLayout.order.forEachIndexed { index, paneId ->
|
|
||||||
val isCollapsed = paneId in paneLayout.collapsed
|
|
||||||
if (isCollapsed) {
|
|
||||||
CollapsedPaneCard(
|
|
||||||
paneId = paneId,
|
|
||||||
canMoveUp = index > 0,
|
|
||||||
canMoveDown = index < paneLayout.order.lastIndex,
|
|
||||||
onMoveUp = { onMovePane(paneId, -1) },
|
|
||||||
onMoveDown = { onMovePane(paneId, 1) },
|
|
||||||
onToggleCollapse = { onTogglePane(paneId) },
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
PaneCard(
|
|
||||||
paneId = paneId,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight((paneLayout.weights[paneId] ?: 1f).coerceAtLeast(0.6f)),
|
|
||||||
canMoveUp = index > 0,
|
|
||||||
canMoveDown = index < paneLayout.order.lastIndex,
|
|
||||||
onMoveUp = { onMovePane(paneId, -1) },
|
|
||||||
onMoveDown = { onMovePane(paneId, 1) },
|
|
||||||
onToggleCollapse = { onTogglePane(paneId) },
|
|
||||||
) {
|
|
||||||
when (paneId) {
|
|
||||||
PaneId.LEVELS -> levelsContent()
|
|
||||||
PaneId.VISUAL -> visualContent()
|
|
||||||
PaneId.EXERCISE -> exerciseContent()
|
|
||||||
PaneId.TERMINAL -> terminalContent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < paneLayout.order.lastIndex) {
|
|
||||||
val upper = paneId
|
|
||||||
val lower = paneLayout.order[index + 1]
|
|
||||||
PaneDivider(
|
|
||||||
enabled = upper !in paneLayout.collapsed && lower !in paneLayout.collapsed,
|
|
||||||
onDrag = { deltaPx ->
|
|
||||||
val fraction = deltaPx / heightBasis
|
|
||||||
onResize(upper, lower, fraction)
|
|
||||||
},
|
|
||||||
onDragFinished = onResizeFinished,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PaneCard(
|
|
||||||
paneId: PaneId,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
canMoveUp: Boolean,
|
|
||||||
canMoveDown: Boolean,
|
|
||||||
onMoveUp: () -> Unit,
|
|
||||||
onMoveDown: () -> Unit,
|
|
||||||
onToggleCollapse: () -> Unit,
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = modifier,
|
|
||||||
colors = CardDefaults.cardColors(containerColor = PanelSecondary),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
|
||||||
PaneHeader(
|
|
||||||
paneId = paneId,
|
|
||||||
collapsed = false,
|
|
||||||
canMoveUp = canMoveUp,
|
|
||||||
canMoveDown = canMoveDown,
|
|
||||||
onMoveUp = onMoveUp,
|
|
||||||
onMoveDown = onMoveDown,
|
|
||||||
onToggleCollapse = onToggleCollapse,
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
|
||||||
) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun CollapsedPaneCard(
|
|
||||||
paneId: PaneId,
|
|
||||||
canMoveUp: Boolean,
|
|
||||||
canMoveDown: Boolean,
|
|
||||||
onMoveUp: () -> Unit,
|
|
||||||
onMoveDown: () -> Unit,
|
|
||||||
onToggleCollapse: () -> Unit,
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = PanelSecondary),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
) {
|
|
||||||
PaneHeader(
|
|
||||||
paneId = paneId,
|
|
||||||
collapsed = true,
|
|
||||||
canMoveUp = canMoveUp,
|
|
||||||
canMoveDown = canMoveDown,
|
|
||||||
onMoveUp = onMoveUp,
|
|
||||||
onMoveDown = onMoveDown,
|
|
||||||
onToggleCollapse = onToggleCollapse,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PaneHeader(
|
|
||||||
paneId: PaneId,
|
|
||||||
collapsed: Boolean,
|
|
||||||
canMoveUp: Boolean,
|
|
||||||
canMoveDown: Boolean,
|
|
||||||
onMoveUp: () -> Unit,
|
|
||||||
onMoveDown: () -> Unit,
|
|
||||||
onToggleCollapse: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(if (collapsed) PanelTertiary else PanelPrimary)
|
|
||||||
.padding(horizontal = 10.dp, vertical = 6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
Text(paneId.title, color = TextPrimary, fontWeight = FontWeight.Bold)
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
HeaderActionButton(label = if (collapsed) "Expand" else "Collapse", onClick = onToggleCollapse)
|
|
||||||
HeaderActionButton(label = "↑", enabled = canMoveUp, onClick = onMoveUp)
|
|
||||||
HeaderActionButton(label = "↓", enabled = canMoveDown, onClick = onMoveDown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun HeaderActionButton(
|
|
||||||
label: String,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
TextButton(
|
|
||||||
onClick = onClick,
|
|
||||||
enabled = enabled,
|
|
||||||
contentPadding = PaddingValues(horizontal = 6.dp, vertical = 0.dp),
|
|
||||||
colors = ButtonDefaults.textButtonColors(
|
|
||||||
contentColor = Accent,
|
|
||||||
disabledContentColor = TextMuted,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Text(label, fontSize = 11.sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun PaneDivider(
|
|
||||||
enabled: Boolean,
|
|
||||||
onDrag: (Float) -> Unit,
|
|
||||||
onDragFinished: () -> Unit,
|
|
||||||
) {
|
|
||||||
val dragState = rememberDraggableState { delta ->
|
|
||||||
if (enabled) onDrag(delta)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(18.dp)
|
|
||||||
.draggable(
|
|
||||||
state = dragState,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
enabled = enabled,
|
|
||||||
onDragStopped = { onDragFinished() },
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.CenterStart,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(1.dp)
|
|
||||||
.background(if (enabled) Accent.copy(alpha = 0.45f) else PanelTertiary),
|
|
||||||
)
|
|
||||||
Card(
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = if (enabled) PanelPrimary else PanelTertiary,
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(999.dp),
|
|
||||||
modifier = Modifier.padding(start = 28.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "↕",
|
|
||||||
color = if (enabled) Accent else TextMuted,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 1.dp),
|
|
||||||
fontSize = 13.sp,
|
|
||||||
fontWeight = FontWeight.ExtraBold,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun RowScope.TerminalInputField(
|
|
||||||
inputFieldVersion: Int,
|
|
||||||
commandInput: TextFieldValue,
|
|
||||||
onValueChange: (TextFieldValue) -> Unit,
|
|
||||||
focusRequester: FocusRequester,
|
|
||||||
bringIntoViewRequester: BringIntoViewRequester,
|
|
||||||
keyboardController: androidx.compose.ui.platform.SoftwareKeyboardController?,
|
|
||||||
onSubmit: () -> Unit,
|
|
||||||
) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
key(inputFieldVersion) {
|
|
||||||
BasicTextField(
|
|
||||||
value = commandInput,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.bringIntoViewRequester(bringIntoViewRequester)
|
|
||||||
.onFocusChanged { state ->
|
|
||||||
if (state.isFocused) {
|
|
||||||
keyboardController?.show()
|
|
||||||
scope.launch {
|
|
||||||
delay(150)
|
|
||||||
bringIntoViewRequester.bringIntoView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
textStyle = TextStyle(
|
|
||||||
color = TextPrimary,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 15.sp,
|
|
||||||
fontWeight = FontWeight.Normal,
|
|
||||||
),
|
|
||||||
cursorBrush = SolidColor(Accent),
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
autoCorrect = false,
|
|
||||||
keyboardType = KeyboardType.Ascii,
|
|
||||||
imeAction = ImeAction.Done,
|
|
||||||
),
|
|
||||||
keyboardActions = KeyboardActions(onDone = { onSubmit() }),
|
|
||||||
decorationBox = { innerTextField ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(Color.Transparent)
|
|
||||||
.padding(vertical = 2.dp),
|
|
||||||
contentAlignment = Alignment.CenterStart,
|
|
||||||
) {
|
|
||||||
if (commandInput.text.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = "Enter git command",
|
|
||||||
color = TextMuted.copy(alpha = 0.7f),
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 15.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
innerTextField()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LevelsPane(
|
|
||||||
levels: List<Level>,
|
|
||||||
currentLevelIndex: Int,
|
|
||||||
completedLevels: Set<String>,
|
|
||||||
onSelect: (Int) -> Unit,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
) {
|
|
||||||
levels.forEachIndexed { index, level ->
|
|
||||||
Card(
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = if (index == currentLevelIndex) PanelTertiary else PanelPrimary,
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
) {
|
|
||||||
TextButton(
|
|
||||||
onClick = { onSelect(index) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = ButtonDefaults.textButtonColors(
|
|
||||||
contentColor = if (index == currentLevelIndex) Accent else TextSecondary,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = buildString {
|
|
||||||
append(if (level.id in completedLevels) "✓ " else "• ")
|
|
||||||
append(level.title)
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun VisualPane(repo: RepoState) {
|
|
||||||
val configuration = LocalConfiguration.current
|
|
||||||
val isWide = configuration.screenWidthDp >= 840
|
|
||||||
|
|
||||||
if (isWide) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
InfoPaneCard(
|
|
||||||
title = "Workspace",
|
|
||||||
content = repo.files.joinToString("\n") {
|
|
||||||
"${if (it.staged) "[staged]" else "[file] "} ${it.name}"
|
|
||||||
}.ifBlank { "(empty)" },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
InfoPaneCard(
|
|
||||||
title = "Branches",
|
|
||||||
content = buildString {
|
|
||||||
appendLine("HEAD -> ${repo.headBranch}")
|
|
||||||
repo.branches.forEach { (name, _) -> appendLine(name) }
|
|
||||||
}.trim(),
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
InfoPaneCard(
|
|
||||||
title = "Commits",
|
|
||||||
content = repo.commits.reversed().joinToString("\n") { "${it.id} ${it.message}" }.ifBlank { "No commits yet" },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
) {
|
|
||||||
InfoPaneCard(
|
|
||||||
title = "Workspace",
|
|
||||||
content = repo.files.joinToString("\n") {
|
|
||||||
"${if (it.staged) "[staged]" else "[file] "} ${it.name}"
|
|
||||||
}.ifBlank { "(empty)" },
|
|
||||||
)
|
|
||||||
InfoPaneCard(
|
|
||||||
title = "Branches",
|
|
||||||
content = buildString {
|
|
||||||
appendLine("HEAD -> ${repo.headBranch}")
|
|
||||||
repo.branches.forEach { (name, _) -> appendLine(name) }
|
|
||||||
}.trim(),
|
|
||||||
)
|
|
||||||
InfoPaneCard(
|
|
||||||
title = "Commits",
|
|
||||||
content = repo.commits.reversed().joinToString("\n") { "${it.id} ${it.message}" }.ifBlank { "No commits yet" },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ExercisePane(
|
|
||||||
level: Level,
|
|
||||||
visibleHint: String?,
|
|
||||||
suggestionsExpanded: Boolean,
|
|
||||||
onToggleSuggestions: () -> Unit,
|
|
||||||
onHint: () -> Unit,
|
|
||||||
onReset: () -> Unit,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
Text(level.title, style = MaterialTheme.typography.titleLarge, color = TextPrimary)
|
|
||||||
Text(level.description, color = TextSecondary)
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Button(
|
|
||||||
onClick = onHint,
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = Accent,
|
|
||||||
contentColor = Color.Black,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Text("Hint")
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
onClick = onToggleSuggestions,
|
|
||||||
colors = ButtonDefaults.textButtonColors(contentColor = Accent),
|
|
||||||
) {
|
|
||||||
Text("Suggestions")
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
onClick = onReset,
|
|
||||||
colors = ButtonDefaults.textButtonColors(contentColor = Accent),
|
|
||||||
) {
|
|
||||||
Text("Reset level")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (suggestionsExpanded && level.commandSuggestions.isNotEmpty()) {
|
|
||||||
Text("Suggestions", color = TextPrimary, fontWeight = FontWeight.Bold)
|
|
||||||
level.commandSuggestions.forEach { suggestion ->
|
|
||||||
Text(suggestion, color = TextSecondary, fontFamily = FontFamily.Monospace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (visibleHint != null) {
|
|
||||||
Text("Hint", color = TextPrimary, fontWeight = FontWeight.Bold)
|
|
||||||
Text(visibleHint, color = TextSecondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun InfoPaneCard(
|
|
||||||
title: String,
|
|
||||||
content: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
modifier = modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(containerColor = PanelPrimary),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(8.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
Text(title, color = TextPrimary, fontWeight = FontWeight.Bold)
|
|
||||||
Text(
|
|
||||||
content,
|
|
||||||
color = TextSecondary,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun TerminalPane(
|
|
||||||
output: List<String>,
|
|
||||||
inputFieldVersion: Int,
|
|
||||||
commandInput: TextFieldValue,
|
|
||||||
onValueChange: (TextFieldValue) -> Unit,
|
|
||||||
onRun: () -> Unit,
|
|
||||||
onTab: () -> Unit,
|
|
||||||
onHelp: () -> Unit,
|
|
||||||
onCursorLeft: () -> Unit,
|
|
||||||
onCursorRight: () -> Unit,
|
|
||||||
onHistoryUp: () -> Unit,
|
|
||||||
onHistoryDown: () -> Unit,
|
|
||||||
) {
|
|
||||||
val configuration = LocalConfiguration.current
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
val horizontalScroll = rememberScrollState()
|
|
||||||
val outputVerticalScroll = rememberScrollState()
|
|
||||||
val terminalMinWidth = 80.dp * 7.2f
|
|
||||||
val terminalPaneMaxHeight = configuration.screenHeightDp.dp * 0.7f
|
|
||||||
val reservedControlsHeight = 86.dp
|
|
||||||
val maxOutputHeight = (terminalPaneMaxHeight - reservedControlsHeight).coerceAtLeast(120.dp)
|
|
||||||
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()
|
|
||||||
onRun()
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(TerminalBackground, RoundedCornerShape(12.dp))
|
|
||||||
.padding(8.dp)
|
|
||||||
.horizontalScroll(horizontalScroll),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.widthIn(min = terminalMinWidth),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.heightIn(min = 120.dp, max = maxOutputHeight)
|
|
||||||
.height(desiredOutputHeight)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(PanelPrimary, RoundedCornerShape(10.dp))
|
|
||||||
.padding(8.dp)
|
|
||||||
.verticalScroll(outputVerticalScroll),
|
|
||||||
) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
output.forEach { line ->
|
|
||||||
Text(
|
|
||||||
text = line,
|
|
||||||
color = if (line.startsWith("✔") || line.startsWith("🏁")) Success else TextSecondary,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SpecialKeyBar(
|
|
||||||
onTab = onTab,
|
|
||||||
onHelp = onHelp,
|
|
||||||
onCursorLeft = onCursorLeft,
|
|
||||||
onCursorRight = onCursorRight,
|
|
||||||
onHistoryUp = onHistoryUp,
|
|
||||||
onHistoryDown = onHistoryDown,
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.bringIntoViewRequester(bringIntoViewRequester)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.widthIn(min = terminalMinWidth)
|
|
||||||
.background(PanelPrimary, RoundedCornerShape(8.dp))
|
|
||||||
.padding(horizontal = 8.dp, vertical = 3.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "$",
|
|
||||||
color = Accent,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 15.sp,
|
|
||||||
)
|
|
||||||
TerminalInputField(
|
|
||||||
inputFieldVersion = inputFieldVersion,
|
|
||||||
commandInput = commandInput,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
bringIntoViewRequester = bringIntoViewRequester,
|
|
||||||
keyboardController = keyboardController,
|
|
||||||
onSubmit = { submitCommand() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SpecialKeyBar(
|
|
||||||
onTab: () -> Unit,
|
|
||||||
onHelp: () -> Unit,
|
|
||||||
onCursorLeft: () -> Unit,
|
|
||||||
onCursorRight: () -> Unit,
|
|
||||||
onHistoryUp: () -> Unit,
|
|
||||||
onHistoryDown: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
) {
|
|
||||||
TerminalKeyButton(label = "↹", onClick = onTab, modifier = Modifier.width(56.dp), fontFamily = FontFamily.Default)
|
|
||||||
TerminalKeyButton(label = "?", onClick = onHelp, modifier = Modifier.width(40.dp), fontFamily = FontFamily.Default)
|
|
||||||
TerminalKeyButton(label = "←", onClick = onCursorLeft, modifier = Modifier.width(40.dp))
|
|
||||||
TerminalKeyButton(label = "→", onClick = onCursorRight, modifier = Modifier.width(40.dp))
|
|
||||||
TerminalKeyButton(label = "↑", onClick = onHistoryUp, modifier = Modifier.width(40.dp))
|
|
||||||
TerminalKeyButton(label = "↓", onClick = onHistoryDown, modifier = Modifier.width(40.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun TerminalKeyButton(
|
|
||||||
label: String,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
fontFamily: FontFamily = FontFamily.Monospace,
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier.height(28.dp),
|
|
||||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = PanelSecondary,
|
|
||||||
contentColor = TextPrimary,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
fontFamily = fontFamily,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 11.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
41
app/src/main/java/com/kawomi/githugandroid/Header.kt
Normal file
41
app/src/main/java/com/kawomi/githugandroid/Header.kt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package com.kawomi.githugandroid
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FixedHeader() {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PanelPrimary),
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.githug_android_logo),
|
||||||
|
contentDescription = "GitHug Android logo",
|
||||||
|
modifier = Modifier.size(28.dp),
|
||||||
|
)
|
||||||
|
Text("GitHug Android", style = MaterialTheme.typography.titleLarge, color = TextPrimary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/src/main/java/com/kawomi/githugandroid/Levels.kt
Normal file
52
app/src/main/java/com/kawomi/githugandroid/Levels.kt
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package com.kawomi.githugandroid
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LevelsPane(
|
||||||
|
levels: List<Level>,
|
||||||
|
currentLevelIndex: Int,
|
||||||
|
completedLevels: Set<String>,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
levels.forEachIndexed { index, level ->
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (index == currentLevelIndex) PanelTertiary else PanelPrimary,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = { onSelect(index) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = if (index == currentLevelIndex) Accent else TextSecondary,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = buildString {
|
||||||
|
append(if (level.id in completedLevels) "✓ " else "• ")
|
||||||
|
append(level.title)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
252
app/src/main/java/com/kawomi/githugandroid/Panes.kt
Normal file
252
app/src/main/java/com/kawomi/githugandroid/Panes.kt
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
package com.kawomi.githugandroid
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
|
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.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PaneWorkspace(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
paneLayout: PaneLayout,
|
||||||
|
onMovePane: (PaneId, Int) -> Unit,
|
||||||
|
onTogglePane: (PaneId) -> Unit,
|
||||||
|
onResize: (PaneId, PaneId, Float) -> Unit,
|
||||||
|
onResizeFinished: () -> Unit,
|
||||||
|
levelsContent: @Composable () -> Unit,
|
||||||
|
visualContent: @Composable () -> Unit,
|
||||||
|
exerciseContent: @Composable () -> Unit,
|
||||||
|
terminalContent: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
BoxWithConstraints(modifier = modifier) {
|
||||||
|
val heightBasis = maxHeight.value.takeIf { it > 0f } ?: 1f
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
|
) {
|
||||||
|
paneLayout.order.forEachIndexed { index, paneId ->
|
||||||
|
val isCollapsed = paneId in paneLayout.collapsed
|
||||||
|
if (isCollapsed) {
|
||||||
|
CollapsedPaneCard(
|
||||||
|
paneId = paneId,
|
||||||
|
canMoveUp = index > 0,
|
||||||
|
canMoveDown = index < paneLayout.order.lastIndex,
|
||||||
|
onMoveUp = { onMovePane(paneId, -1) },
|
||||||
|
onMoveDown = { onMovePane(paneId, 1) },
|
||||||
|
onToggleCollapse = { onTogglePane(paneId) },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
PaneCard(
|
||||||
|
paneId = paneId,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight((paneLayout.weights[paneId] ?: 1f).coerceAtLeast(0.6f)),
|
||||||
|
canMoveUp = index > 0,
|
||||||
|
canMoveDown = index < paneLayout.order.lastIndex,
|
||||||
|
onMoveUp = { onMovePane(paneId, -1) },
|
||||||
|
onMoveDown = { onMovePane(paneId, 1) },
|
||||||
|
onToggleCollapse = { onTogglePane(paneId) },
|
||||||
|
) {
|
||||||
|
when (paneId) {
|
||||||
|
PaneId.LEVELS -> levelsContent()
|
||||||
|
PaneId.VISUAL -> visualContent()
|
||||||
|
PaneId.EXERCISE -> exerciseContent()
|
||||||
|
PaneId.TERMINAL -> terminalContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < paneLayout.order.lastIndex) {
|
||||||
|
val upper = paneId
|
||||||
|
val lower = paneLayout.order[index + 1]
|
||||||
|
PaneDivider(
|
||||||
|
enabled = upper !in paneLayout.collapsed && lower !in paneLayout.collapsed,
|
||||||
|
onDrag = { deltaPx ->
|
||||||
|
val fraction = deltaPx / heightBasis
|
||||||
|
onResize(upper, lower, fraction)
|
||||||
|
},
|
||||||
|
onDragFinished = onResizeFinished,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaneCard(
|
||||||
|
paneId: PaneId,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
canMoveUp: Boolean,
|
||||||
|
canMoveDown: Boolean,
|
||||||
|
onMoveUp: () -> Unit,
|
||||||
|
onMoveDown: () -> Unit,
|
||||||
|
onToggleCollapse: () -> Unit,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier,
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PanelSecondary),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
PaneHeader(
|
||||||
|
paneId = paneId,
|
||||||
|
collapsed = false,
|
||||||
|
canMoveUp = canMoveUp,
|
||||||
|
canMoveDown = canMoveDown,
|
||||||
|
onMoveUp = onMoveUp,
|
||||||
|
onMoveDown = onMoveDown,
|
||||||
|
onToggleCollapse = onToggleCollapse,
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CollapsedPaneCard(
|
||||||
|
paneId: PaneId,
|
||||||
|
canMoveUp: Boolean,
|
||||||
|
canMoveDown: Boolean,
|
||||||
|
onMoveUp: () -> Unit,
|
||||||
|
onMoveDown: () -> Unit,
|
||||||
|
onToggleCollapse: () -> Unit,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PanelSecondary),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
) {
|
||||||
|
PaneHeader(
|
||||||
|
paneId = paneId,
|
||||||
|
collapsed = true,
|
||||||
|
canMoveUp = canMoveUp,
|
||||||
|
canMoveDown = canMoveDown,
|
||||||
|
onMoveUp = onMoveUp,
|
||||||
|
onMoveDown = onMoveDown,
|
||||||
|
onToggleCollapse = onToggleCollapse,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaneHeader(
|
||||||
|
paneId: PaneId,
|
||||||
|
collapsed: Boolean,
|
||||||
|
canMoveUp: Boolean,
|
||||||
|
canMoveDown: Boolean,
|
||||||
|
onMoveUp: () -> Unit,
|
||||||
|
onMoveDown: () -> Unit,
|
||||||
|
onToggleCollapse: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(if (collapsed) PanelTertiary else PanelPrimary)
|
||||||
|
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(paneId.title, color = TextPrimary, fontWeight = FontWeight.Bold)
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
HeaderActionButton(label = if (collapsed) "Expand" else "Collapse", onClick = onToggleCollapse)
|
||||||
|
HeaderActionButton(label = "↑", enabled = canMoveUp, onClick = onMoveUp)
|
||||||
|
HeaderActionButton(label = "↓", enabled = canMoveDown, onClick = onMoveDown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun HeaderActionButton(
|
||||||
|
label: String,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
contentPadding = PaddingValues(horizontal = 6.dp, vertical = 0.dp),
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = Accent,
|
||||||
|
disabledContentColor = TextMuted,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(label, fontSize = 11.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaneDivider(
|
||||||
|
enabled: Boolean,
|
||||||
|
onDrag: (Float) -> Unit,
|
||||||
|
onDragFinished: () -> Unit,
|
||||||
|
) {
|
||||||
|
val dragState = rememberDraggableState { delta ->
|
||||||
|
if (enabled) onDrag(delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(18.dp)
|
||||||
|
.draggable(
|
||||||
|
state = dragState,
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
enabled = enabled,
|
||||||
|
onDragStopped = { onDragFinished() },
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(if (enabled) Accent.copy(alpha = 0.45f) else PanelTertiary),
|
||||||
|
)
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (enabled) PanelPrimary else PanelTertiary,
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(999.dp),
|
||||||
|
modifier = Modifier.padding(start = 28.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "↕",
|
||||||
|
color = if (enabled) Accent else TextMuted,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 1.dp),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
291
app/src/main/java/com/kawomi/githugandroid/Terminal.kt
Normal file
291
app/src/main/java/com/kawomi/githugandroid/Terminal.kt
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
package com.kawomi.githugandroid
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
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.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TerminalPane(
|
||||||
|
output: List<String>,
|
||||||
|
inputFieldVersion: Int,
|
||||||
|
commandInput: TextFieldValue,
|
||||||
|
onValueChange: (TextFieldValue) -> Unit,
|
||||||
|
onRun: () -> Unit,
|
||||||
|
onTab: () -> Unit,
|
||||||
|
onHelp: () -> Unit,
|
||||||
|
onCursorLeft: () -> Unit,
|
||||||
|
onCursorRight: () -> Unit,
|
||||||
|
onHistoryUp: () -> Unit,
|
||||||
|
onHistoryDown: () -> Unit,
|
||||||
|
) {
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val horizontalScroll = rememberScrollState()
|
||||||
|
val outputVerticalScroll = rememberScrollState()
|
||||||
|
val terminalMinWidth = 80.dp * 7.2f
|
||||||
|
val terminalPaneMaxHeight = configuration.screenHeightDp.dp * 0.7f
|
||||||
|
val reservedControlsHeight = 86.dp
|
||||||
|
val maxOutputHeight = (terminalPaneMaxHeight - reservedControlsHeight).coerceAtLeast(120.dp)
|
||||||
|
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()
|
||||||
|
onRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(TerminalBackground, RoundedCornerShape(12.dp))
|
||||||
|
.padding(8.dp)
|
||||||
|
.horizontalScroll(horizontalScroll),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.widthIn(min = terminalMinWidth),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.heightIn(min = 120.dp, max = maxOutputHeight)
|
||||||
|
.height(desiredOutputHeight)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(PanelPrimary, RoundedCornerShape(10.dp))
|
||||||
|
.padding(8.dp)
|
||||||
|
.verticalScroll(outputVerticalScroll),
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
output.forEach { line ->
|
||||||
|
Text(
|
||||||
|
text = line,
|
||||||
|
color = if (line.startsWith("✔") || line.startsWith("🏁")) Success else TextSecondary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SpecialKeyBar(
|
||||||
|
onTab = onTab,
|
||||||
|
onHelp = onHelp,
|
||||||
|
onCursorLeft = onCursorLeft,
|
||||||
|
onCursorRight = onCursorRight,
|
||||||
|
onHistoryUp = onHistoryUp,
|
||||||
|
onHistoryDown = onHistoryDown,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.bringIntoViewRequester(bringIntoViewRequester)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.widthIn(min = terminalMinWidth)
|
||||||
|
.background(PanelPrimary, RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 3.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$",
|
||||||
|
color = Accent,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
)
|
||||||
|
TerminalInputField(
|
||||||
|
inputFieldVersion = inputFieldVersion,
|
||||||
|
commandInput = commandInput,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
bringIntoViewRequester = bringIntoViewRequester,
|
||||||
|
keyboardController = keyboardController,
|
||||||
|
onSubmit = { submitCommand() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.TerminalInputField(
|
||||||
|
inputFieldVersion: Int,
|
||||||
|
commandInput: TextFieldValue,
|
||||||
|
onValueChange: (TextFieldValue) -> Unit,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
bringIntoViewRequester: BringIntoViewRequester,
|
||||||
|
keyboardController: androidx.compose.ui.platform.SoftwareKeyboardController?,
|
||||||
|
onSubmit: () -> Unit,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
key(inputFieldVersion) {
|
||||||
|
BasicTextField(
|
||||||
|
value = commandInput,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.bringIntoViewRequester(bringIntoViewRequester)
|
||||||
|
.onFocusChanged { state ->
|
||||||
|
if (state.isFocused) {
|
||||||
|
keyboardController?.show()
|
||||||
|
scope.launch {
|
||||||
|
delay(150)
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = TextPrimary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(Accent),
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(onDone = { onSubmit() }),
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
) {
|
||||||
|
if (commandInput.text.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Enter git command",
|
||||||
|
color = TextMuted.copy(alpha = 0.7f),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
innerTextField()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SpecialKeyBar(
|
||||||
|
onTab: () -> Unit,
|
||||||
|
onHelp: () -> Unit,
|
||||||
|
onCursorLeft: () -> Unit,
|
||||||
|
onCursorRight: () -> Unit,
|
||||||
|
onHistoryUp: () -> Unit,
|
||||||
|
onHistoryDown: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
TerminalKeyButton(label = "↹", onClick = onTab, modifier = Modifier.width(56.dp), fontFamily = FontFamily.Default)
|
||||||
|
TerminalKeyButton(label = "?", onClick = onHelp, modifier = Modifier.width(40.dp), fontFamily = FontFamily.Default)
|
||||||
|
TerminalKeyButton(label = "←", onClick = onCursorLeft, modifier = Modifier.width(40.dp))
|
||||||
|
TerminalKeyButton(label = "→", onClick = onCursorRight, modifier = Modifier.width(40.dp))
|
||||||
|
TerminalKeyButton(label = "↑", onClick = onHistoryUp, modifier = Modifier.width(40.dp))
|
||||||
|
TerminalKeyButton(label = "↓", onClick = onHistoryDown, modifier = Modifier.width(40.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TerminalKeyButton(
|
||||||
|
label: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
fontFamily: FontFamily = FontFamily.Monospace,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier.height(28.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = PanelSecondary,
|
||||||
|
contentColor = TextPrimary,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontFamily = fontFamily,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
103
app/src/main/java/com/kawomi/githugandroid/Visual.kt
Normal file
103
app/src/main/java/com/kawomi/githugandroid/Visual.kt
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package com.kawomi.githugandroid
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VisualPane(repo: RepoState) {
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val isWide = configuration.screenWidthDp >= 840
|
||||||
|
|
||||||
|
if (isWide) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
InfoPaneCard(
|
||||||
|
title = "Workspace",
|
||||||
|
content = repo.files.joinToString("\n") {
|
||||||
|
"${if (it.staged) "[staged]" else "[file] "} ${it.name}"
|
||||||
|
}.ifBlank { "(empty)" },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
InfoPaneCard(
|
||||||
|
title = "Branches",
|
||||||
|
content = buildString {
|
||||||
|
appendLine("HEAD -> ${repo.headBranch}")
|
||||||
|
repo.branches.forEach { (name, _) -> appendLine(name) }
|
||||||
|
}.trim(),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
InfoPaneCard(
|
||||||
|
title = "Commits",
|
||||||
|
content = repo.commits.reversed().joinToString("\n") { "${it.id} ${it.message}" }.ifBlank { "No commits yet" },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
InfoPaneCard(
|
||||||
|
title = "Workspace",
|
||||||
|
content = repo.files.joinToString("\n") {
|
||||||
|
"${if (it.staged) "[staged]" else "[file] "} ${it.name}"
|
||||||
|
}.ifBlank { "(empty)" },
|
||||||
|
)
|
||||||
|
InfoPaneCard(
|
||||||
|
title = "Branches",
|
||||||
|
content = buildString {
|
||||||
|
appendLine("HEAD -> ${repo.headBranch}")
|
||||||
|
repo.branches.forEach { (name, _) -> appendLine(name) }
|
||||||
|
}.trim(),
|
||||||
|
)
|
||||||
|
InfoPaneCard(
|
||||||
|
title = "Commits",
|
||||||
|
content = repo.commits.reversed().joinToString("\n") { "${it.id} ${it.message}" }.ifBlank { "No commits yet" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InfoPaneCard(
|
||||||
|
title: String,
|
||||||
|
content: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = PanelPrimary),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(title, color = TextPrimary, fontWeight = FontWeight.Bold)
|
||||||
|
Text(
|
||||||
|
content,
|
||||||
|
color = TextSecondary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user