Refactor large Kotlin source files
- Split core game models from sandbox engine, native level setup, and repo state saving. - Move app overlays and completion helpers out of GitHugApp. - Extract the terminal special key bar into its own component.
This commit is contained in:
@@ -1,35 +1,18 @@
|
||||
package solutions.tretter.githugandroid
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.imePadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CheckboxDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -39,25 +22,16 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun GitHugApp() {
|
||||
@@ -653,266 +627,3 @@ fun GitHugApp() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun fileCompletionCandidates(repo: RepoState): List<String> {
|
||||
val prefix = repo.currentDirPrefix()
|
||||
return repo.files
|
||||
.filterNot { it.deleted }
|
||||
.map { it.name }
|
||||
.filter { it.startsWith(prefix) }
|
||||
.map { it.removePrefix(prefix) }
|
||||
.filter { it.isNotBlank() }
|
||||
}
|
||||
|
||||
internal fun directoryCompletionCandidates(repo: RepoState): List<String> {
|
||||
val prefix = repo.currentDirPrefix()
|
||||
return repo.files
|
||||
.filterNot { it.deleted }
|
||||
.map { it.name }
|
||||
.filter { it.startsWith(prefix) }
|
||||
.map { it.removePrefix(prefix) }
|
||||
.flatMap { file ->
|
||||
val parts = file.split('/').dropLast(1)
|
||||
parts.indices.map { index -> parts.take(index + 1).joinToString("/") + "/" }
|
||||
}
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private fun RepoState.currentDirPrefix(): String {
|
||||
return if (currentDir == ".") "" else currentDir.trimEnd('/') + "/"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HelpCalloutOverlay(
|
||||
showHelpOnStart: Boolean,
|
||||
onShowHelpOnStartChange: (Boolean) -> Unit,
|
||||
onOk: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
workspaceBounds: Rect?,
|
||||
exerciseBounds: Rect?,
|
||||
promptBounds: Rect?,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val workspaceTop = workspaceBounds?.top ?: 0f
|
||||
val insetPx = with(density) { 14.dp.roundToPx() }
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
HelpBubble(
|
||||
text = "Read the exercise description, then solve it by entering commands below.",
|
||||
modifier = exerciseBounds?.let { bounds ->
|
||||
Modifier.offset {
|
||||
IntOffset(
|
||||
x = insetPx,
|
||||
y = (bounds.top - workspaceTop - insetPx).roundToInt().coerceAtLeast(0),
|
||||
)
|
||||
}
|
||||
} ?: Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
)
|
||||
HelpBubbleWithControls(
|
||||
modifier = promptBounds?.let { bounds ->
|
||||
val bubbleHeight = with(density) { 190.dp.roundToPx() }
|
||||
Modifier.offset {
|
||||
IntOffset(
|
||||
x = insetPx,
|
||||
y = (bounds.top - workspaceTop - bubbleHeight).roundToInt().coerceAtLeast(0),
|
||||
)
|
||||
}
|
||||
} ?: Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(start = 14.dp, bottom = 96.dp),
|
||||
text = "Tap the prompt to enter a command or answer.",
|
||||
showHelpOnStart = showHelpOnStart,
|
||||
onShowHelpOnStartChange = onShowHelpOnStartChange,
|
||||
onOk = onOk,
|
||||
onClose = onClose,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HelpBubbleWithControls(
|
||||
text: String,
|
||||
showHelpOnStart: Boolean,
|
||||
onShowHelpOnStartChange: (Boolean) -> Unit,
|
||||
onOk: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val bubbleColor = Color(0xFFFFF1A8)
|
||||
Column(modifier = modifier.fillMaxWidth(0.9f)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(bubbleColor, RoundedCornerShape(18.dp))
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color(0xFF161000),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Checkbox(
|
||||
checked = showHelpOnStart,
|
||||
onCheckedChange = onShowHelpOnStartChange,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = Color(0xFF161000),
|
||||
uncheckedColor = Color(0xFF6E5A00),
|
||||
checkmarkColor = bubbleColor,
|
||||
),
|
||||
)
|
||||
Text("Show help on start", color = Color(0xFF161000))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = onOk,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF161000),
|
||||
contentColor = bubbleColor,
|
||||
),
|
||||
) {
|
||||
Text("OK")
|
||||
}
|
||||
TextButton(onClick = onClose) {
|
||||
Text("Close", color = Color(0xFF161000))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.padding(start = 28.dp)
|
||||
.size(width = 26.dp, height = 13.dp),
|
||||
) {
|
||||
drawPath(
|
||||
path = Path().apply {
|
||||
moveTo(0f, 0f)
|
||||
lineTo(size.width, 0f)
|
||||
lineTo(size.width * 0.25f, size.height)
|
||||
close()
|
||||
},
|
||||
color = bubbleColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HelpBubble(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val bubbleColor = Color(0xFFFFF1A8)
|
||||
Column(modifier = modifier.fillMaxWidth(0.86f)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(bubbleColor, RoundedCornerShape(18.dp))
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
color = Color(0xFF161000),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.padding(start = 28.dp)
|
||||
.size(width = 26.dp, height = 13.dp),
|
||||
) {
|
||||
drawPath(
|
||||
path = Path().apply {
|
||||
moveTo(0f, 0f)
|
||||
lineTo(size.width, 0f)
|
||||
lineTo(size.width * 0.25f, size.height)
|
||||
close()
|
||||
},
|
||||
color = bubbleColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MissingNativeGitScreen(message: String) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(AppBackground)
|
||||
.padding(24.dp),
|
||||
color = AppBackground,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
text = "Native Git unavailable",
|
||||
color = TextPrimary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
)
|
||||
Text(
|
||||
text = message,
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
color = TextSecondary,
|
||||
fontSize = 15.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SolvedCelebrationOverlay(title: String) {
|
||||
val transition = rememberInfiniteTransition(label = "solved-celebration")
|
||||
val progress by transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1_200, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart,
|
||||
),
|
||||
label = "confetti-progress",
|
||||
)
|
||||
val colors = listOf(Accent, Success, Color(0xFFFFD166), Color(0xFFFF6B6B), Color(0xFF9BF6FF))
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
repeat(42) { index ->
|
||||
val x = ((index * 73) % 100) / 100f * size.width
|
||||
val baseY = ((index * 37) % 100) / 100f * size.height
|
||||
val y = (baseY + progress * size.height * 0.6f) % size.height
|
||||
val pieceSize = 6.dp.toPx() + (index % 4) * 2.dp.toPx()
|
||||
drawCircle(
|
||||
color = colors[index % colors.size],
|
||||
radius = pieceSize / 2f,
|
||||
center = Offset(x, y),
|
||||
alpha = 0.85f,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(PanelPrimary.copy(alpha = 0.94f), RoundedCornerShape(24.dp))
|
||||
.padding(horizontal = 26.dp, vertical = 22.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(text = "👍", fontSize = 56.sp)
|
||||
Text(text = "Level solved", color = Success, fontSize = 22.sp)
|
||||
Text(text = title, color = TextSecondary, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user