Add low-output tooling mode
This commit is contained in:
@@ -37,11 +37,66 @@ ANDROID_CMAKE_DIR="$SDK_DIR/cmake/3.22.1"
|
|||||||
ANDROID_TOOLBIN="$ANDROID_NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
ANDROID_TOOLBIN="$ANDROID_NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||||
STATE_TTL_SECONDS=86400
|
STATE_TTL_SECONDS=86400
|
||||||
HIDE_EMULATOR_WINDOW="false"
|
HIDE_EMULATOR_WINDOW="false"
|
||||||
|
QUIET_LOG_DIR="$PROJECT_DIR/build/reports/android-project-tooling"
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
printf '\n[%s] %s\n' "tooling" "$1"
|
printf '\n[%s] %s\n' "tooling" "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quiet_log_name() {
|
||||||
|
local mode="setup"
|
||||||
|
local arg
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--build|--build-release-aab|--test|--clean-test|--test-emulator|--compile-git)
|
||||||
|
mode="${arg#--}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
printf '%s-%s.log\n' "$(date +%Y%m%d-%H%M%S)" "$mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_quiet_if_requested() {
|
||||||
|
if [ "${ANDROID_PROJECT_TOOLING_QUIET_CHILD:-}" = "1" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local quiet="false"
|
||||||
|
local filtered_args=()
|
||||||
|
local arg
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$arg" = "--quiet" ]; then
|
||||||
|
quiet="true"
|
||||||
|
else
|
||||||
|
filtered_args+=("$arg")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$quiet" != "true" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$QUIET_LOG_DIR"
|
||||||
|
local log_file="$QUIET_LOG_DIR/$(quiet_log_name "${filtered_args[@]}")"
|
||||||
|
printf '[tooling] Running quietly; full log: %s\n' "${log_file#$PROJECT_DIR/}"
|
||||||
|
|
||||||
|
set +e
|
||||||
|
ANDROID_PROJECT_TOOLING_QUIET_CHILD=1 bash "$0" "${filtered_args[@]}" >"$log_file" 2>&1 &
|
||||||
|
local child_pid=$!
|
||||||
|
wait "$child_pid"
|
||||||
|
local status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$status" -eq 0 ]; then
|
||||||
|
printf '[tooling] Completed successfully; full log: %s\n' "${log_file#$PROJECT_DIR/}"
|
||||||
|
else
|
||||||
|
printf '[tooling] Failed with exit code %s; full log: %s\n' "$status" "${log_file#$PROJECT_DIR/}" >&2
|
||||||
|
printf '[tooling] Last 120 log lines:\n' >&2
|
||||||
|
tail -n 120 "$log_file" >&2 || true
|
||||||
|
fi
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
|
||||||
require_tool() {
|
require_tool() {
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
echo "Missing required tool: $1" >&2
|
echo "Missing required tool: $1" >&2
|
||||||
@@ -795,7 +850,7 @@ maybe_run_operation() {
|
|||||||
|
|
||||||
print_usage() {
|
print_usage() {
|
||||||
cat <<EOF_USAGE
|
cat <<EOF_USAGE
|
||||||
Usage: bash ./AndroidProjectTooling.sh [--build | --build-release-aab | --test | --clean-test | --test-emulator [--hide-emulator-window] | --compile-git]
|
Usage: bash ./AndroidProjectTooling.sh [--quiet] [--build | --build-release-aab | --test | --clean-test | --test-emulator [--hide-emulator-window] | --compile-git]
|
||||||
|
|
||||||
--build Set up the environment and build the debug APK
|
--build Set up the environment and build the debug APK
|
||||||
--build-release-aab Set up the environment and build a release Android App Bundle (AAB)
|
--build-release-aab Set up the environment and build a release Android App Bundle (AAB)
|
||||||
@@ -804,11 +859,16 @@ Usage: bash ./AndroidProjectTooling.sh [--build | --build-release-aab | --test |
|
|||||||
--test-emulator Set up a visible Android emulator and run debug instrumentation tests on it
|
--test-emulator Set up a visible Android emulator and run debug instrumentation tests on it
|
||||||
--hide-emulator-window Run --test-emulator with the emulator window hidden
|
--hide-emulator-window Run --test-emulator with the emulator window hidden
|
||||||
--compile-git Compile Git for the development host and all Android target ABIs
|
--compile-git Compile Git for the development host and all Android target ABIs
|
||||||
|
--quiet Write full output to build/reports/android-project-tooling/ and print only a concise result
|
||||||
EOF_USAGE
|
EOF_USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_args() {
|
validate_args() {
|
||||||
local mode="${1:-}"
|
local mode="${1:-}"
|
||||||
|
if [ "$mode" = "--quiet" ]; then
|
||||||
|
mode="${2:-}"
|
||||||
|
shift || true
|
||||||
|
fi
|
||||||
shift || true
|
shift || true
|
||||||
|
|
||||||
case "$mode" in
|
case "$mode" in
|
||||||
@@ -823,6 +883,8 @@ validate_args() {
|
|||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
--quiet)
|
||||||
|
;;
|
||||||
--hide-emulator-window)
|
--hide-emulator-window)
|
||||||
if [ "$mode" != "--test-emulator" ]; then
|
if [ "$mode" != "--test-emulator" ]; then
|
||||||
echo "--hide-emulator-window can only be used with --test-emulator" >&2
|
echo "--hide-emulator-window can only be used with --test-emulator" >&2
|
||||||
@@ -842,6 +904,7 @@ validate_args() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
|
run_quiet_if_requested "$@"
|
||||||
validate_args "$@"
|
validate_args "$@"
|
||||||
|
|
||||||
require_tool curl
|
require_tool curl
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ Available commands:
|
|||||||
| `bash ./AndroidProjectTooling.sh --build-release-aab` | Build the release Android App Bundle. | `app/build/outputs/bundle/release/githug-android-release-v<versionCode>.aab` |
|
| `bash ./AndroidProjectTooling.sh --build-release-aab` | Build the release Android App Bundle. | `app/build/outputs/bundle/release/githug-android-release-v<versionCode>.aab` |
|
||||||
| `bash ./AndroidProjectTooling.sh --compile-git` | Compile Git for the development host and all Android target ABIs. | Host and Android `libgit.so` binaries |
|
| `bash ./AndroidProjectTooling.sh --compile-git` | Compile Git for the development host and all Android target ABIs. | Host and Android `libgit.so` binaries |
|
||||||
|
|
||||||
|
Add `--quiet` before any tooling command when running through an AI or other log-sensitive automation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ./AndroidProjectTooling.sh --quiet --test
|
||||||
|
```
|
||||||
|
|
||||||
|
Quiet mode runs the same command in a child process, writes the full output to `build/reports/android-project-tooling/`, waits for completion, and prints only a concise success/failure result. On failure it prints the last 120 log lines so the immediate error is visible without repeatedly streaming the full Gradle or emulator log.
|
||||||
|
|
||||||
To run the JVM unit test suite after ensuring the local toolchain is ready:
|
To run the JVM unit test suite after ensuring the local toolchain is ready:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ android {
|
|||||||
applicationId = "solutions.tretter.githugandroid"
|
applicationId = "solutions.tretter.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 180
|
versionCode = 181
|
||||||
versionName = "0.1.179"
|
versionName = "0.1.180"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -117,6 +117,15 @@ class GitRepositoryRuntimeInstrumentedTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun visualFileEditorStartsWithActionsVisible() {
|
||||||
|
launchFreshApp().use {
|
||||||
|
submitTerminalCommand("edit notes.txt")
|
||||||
|
|
||||||
|
waitForTextEditorActions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun selectingLevelFromLevelsPaneUpdatesExerciseImmediately() {
|
fun selectingLevelFromLevelsPaneUpdatesExerciseImmediately() {
|
||||||
launchFreshApp().use {
|
launchFreshApp().use {
|
||||||
@@ -200,6 +209,7 @@ class GitRepositoryRuntimeInstrumentedTest {
|
|||||||
composeRule.onAllNodes(hasText(path, substring = true)).fetchSemanticsNodes().isNotEmpty()
|
composeRule.onAllNodes(hasText(path, substring = true)).fetchSemanticsNodes().isNotEmpty()
|
||||||
}
|
}
|
||||||
composeRule.onNodeWithTag("git-message-editor-path").assertIsDisplayed()
|
composeRule.onNodeWithTag("git-message-editor-path").assertIsDisplayed()
|
||||||
|
waitForGitEditorActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun waitForGitMessageEditor() {
|
private fun waitForGitMessageEditor() {
|
||||||
@@ -207,6 +217,20 @@ class GitRepositoryRuntimeInstrumentedTest {
|
|||||||
composeRule.onAllNodesWithTag("git-message-editor-content").fetchSemanticsNodes().isNotEmpty()
|
composeRule.onAllNodesWithTag("git-message-editor-content").fetchSemanticsNodes().isNotEmpty()
|
||||||
}
|
}
|
||||||
composeRule.onNodeWithTag("git-message-editor-content").assertIsDisplayed()
|
composeRule.onNodeWithTag("git-message-editor-content").assertIsDisplayed()
|
||||||
|
waitForGitEditorActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun waitForGitEditorActions() {
|
||||||
|
composeRule.onNodeWithTag("git-message-editor-save").assertIsDisplayed()
|
||||||
|
composeRule.onNodeWithTag("git-message-editor-dismiss").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun waitForTextEditorActions() {
|
||||||
|
composeRule.waitUntil(timeoutMillis = 30_000) {
|
||||||
|
composeRule.onAllNodesWithTag("text-editor-content").fetchSemanticsNodes().isNotEmpty()
|
||||||
|
}
|
||||||
|
composeRule.onNodeWithTag("text-editor-save").assertIsDisplayed()
|
||||||
|
composeRule.onNodeWithTag("text-editor-dismiss").assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun currentGitEditorContent(): String {
|
private fun currentGitEditorContent(): String {
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
package solutions.tretter.githugandroid
|
|
||||||
|
|
||||||
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.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
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.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.platform.testTag
|
|
||||||
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.KeyboardType
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
|
|
||||||
data class GitMessageEditorState(
|
|
||||||
val invocation: GitEditorInvocation,
|
|
||||||
val content: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GitMessageEditorDialog(
|
|
||||||
state: GitMessageEditorState,
|
|
||||||
onContentChange: (String) -> Unit,
|
|
||||||
onClose: () -> Unit,
|
|
||||||
onSave: () -> Unit,
|
|
||||||
) {
|
|
||||||
val horizontalScroll = rememberScrollState()
|
|
||||||
val verticalScroll = rememberScrollState()
|
|
||||||
val dialogScroll = rememberScrollState()
|
|
||||||
val contentFocusRequester = remember { FocusRequester() }
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
|
|
||||||
LaunchedEffect(state.invocation.command) {
|
|
||||||
runCatching { contentFocusRequester.requestFocus() }
|
|
||||||
keyboardController?.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = onClose,
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.imePadding()
|
|
||||||
.verticalScroll(dialogScroll)
|
|
||||||
.padding(12.dp),
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.heightIn(min = 360.dp, max = 720.dp),
|
|
||||||
color = PanelPrimary,
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(12.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = state.invocation.title,
|
|
||||||
modifier = Modifier.testTag("git-message-editor-title"),
|
|
||||||
color = TextPrimary,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
EditorButton(
|
|
||||||
label = "Save",
|
|
||||||
enabled = state.content.isNotBlank(),
|
|
||||||
modifier = Modifier.testTag("git-message-editor-save"),
|
|
||||||
onClick = onSave,
|
|
||||||
)
|
|
||||||
EditorButton(
|
|
||||||
label = "Dismiss",
|
|
||||||
modifier = Modifier.testTag("git-message-editor-dismiss"),
|
|
||||||
onClick = onClose,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = state.invocation.displayPath,
|
|
||||||
modifier = Modifier.testTag("git-message-editor-path"),
|
|
||||||
color = TextSecondary,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 13.sp,
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f)
|
|
||||||
.heightIn(min = 260.dp)
|
|
||||||
.background(TerminalBackground, RoundedCornerShape(6.dp))
|
|
||||||
.padding(10.dp)
|
|
||||||
.horizontalScroll(horizontalScroll)
|
|
||||||
.verticalScroll(verticalScroll),
|
|
||||||
) {
|
|
||||||
BasicTextField(
|
|
||||||
value = state.content,
|
|
||||||
onValueChange = onContentChange,
|
|
||||||
modifier = Modifier
|
|
||||||
.sizeIn(minWidth = 1200.dp, minHeight = 1200.dp)
|
|
||||||
.focusRequester(contentFocusRequester)
|
|
||||||
.testTag("git-message-editor-content"),
|
|
||||||
textStyle = TextStyle(
|
|
||||||
color = TextPrimary,
|
|
||||||
fontFamily = FontFamily.Monospace,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
),
|
|
||||||
cursorBrush = SolidColor(Accent),
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
autoCorrect = false,
|
|
||||||
keyboardType = KeyboardType.Ascii,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun EditorButton(
|
|
||||||
label: String,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = onClick,
|
|
||||||
enabled = enabled,
|
|
||||||
modifier = modifier,
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = Accent,
|
|
||||||
contentColor = AppBackground,
|
|
||||||
disabledContainerColor = PanelTertiary,
|
|
||||||
disabledContentColor = TextMuted,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Text(label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,13 +7,11 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
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.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
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.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
@@ -26,17 +24,16 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -50,6 +47,11 @@ data class TextEditorState(
|
|||||||
val saveAsPath: String,
|
val saveAsPath: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class GitMessageEditorState(
|
||||||
|
val invocation: GitEditorInvocation,
|
||||||
|
val content: String,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TextEditorDialog(
|
fun TextEditorDialog(
|
||||||
state: TextEditorState,
|
state: TextEditorState,
|
||||||
@@ -57,16 +59,75 @@ fun TextEditorDialog(
|
|||||||
onSaveAsPathChange: (String) -> Unit,
|
onSaveAsPathChange: (String) -> Unit,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
|
) {
|
||||||
|
ParameterizedTextEditorDialog(
|
||||||
|
title = "Edit File",
|
||||||
|
titleTag = null,
|
||||||
|
metadata = "${state.editor} ${state.path.ifBlank { "<new file>" }}",
|
||||||
|
metadataTag = null,
|
||||||
|
metadataStyle = EditorMetadataStyle.Prominent,
|
||||||
|
content = state.content,
|
||||||
|
saveAsPath = state.saveAsPath,
|
||||||
|
saveEnabled = state.saveAsPath.isNotBlank(),
|
||||||
|
testTagPrefix = "text-editor",
|
||||||
|
resetKey = "${state.originalCommand}\n${state.path}",
|
||||||
|
onContentChange = onContentChange,
|
||||||
|
onSaveAsPathChange = onSaveAsPathChange,
|
||||||
|
onClose = onClose,
|
||||||
|
onSave = onSave,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GitMessageEditorDialog(
|
||||||
|
state: GitMessageEditorState,
|
||||||
|
onContentChange: (String) -> Unit,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
) {
|
||||||
|
ParameterizedTextEditorDialog(
|
||||||
|
title = state.invocation.title,
|
||||||
|
titleTag = "git-message-editor-title",
|
||||||
|
metadata = state.invocation.displayPath,
|
||||||
|
metadataTag = "git-message-editor-path",
|
||||||
|
metadataStyle = EditorMetadataStyle.SecondaryMonospace,
|
||||||
|
content = state.content,
|
||||||
|
saveAsPath = null,
|
||||||
|
saveEnabled = state.content.isNotBlank(),
|
||||||
|
testTagPrefix = "git-message-editor",
|
||||||
|
resetKey = "${state.invocation.command}\n${state.invocation.displayPath}",
|
||||||
|
onContentChange = onContentChange,
|
||||||
|
onSaveAsPathChange = {},
|
||||||
|
onClose = onClose,
|
||||||
|
onSave = onSave,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ParameterizedTextEditorDialog(
|
||||||
|
title: String,
|
||||||
|
titleTag: String?,
|
||||||
|
metadata: String,
|
||||||
|
metadataTag: String?,
|
||||||
|
metadataStyle: EditorMetadataStyle,
|
||||||
|
content: String,
|
||||||
|
saveAsPath: String?,
|
||||||
|
saveEnabled: Boolean,
|
||||||
|
testTagPrefix: String,
|
||||||
|
resetKey: String,
|
||||||
|
onContentChange: (String) -> Unit,
|
||||||
|
onSaveAsPathChange: (String) -> Unit,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val editorHorizontalScroll = rememberScrollState()
|
val editorHorizontalScroll = rememberScrollState()
|
||||||
val editorVerticalScroll = rememberScrollState()
|
val editorVerticalScroll = rememberScrollState()
|
||||||
val dialogScroll = rememberScrollState()
|
val dialogScroll = rememberScrollState()
|
||||||
val contentFocusRequester = remember { FocusRequester() }
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
|
||||||
|
|
||||||
LaunchedEffect(state.originalCommand, state.path) {
|
LaunchedEffect(resetKey) {
|
||||||
runCatching { contentFocusRequester.requestFocus() }
|
dialogScroll.scrollTo(0)
|
||||||
keyboardController?.show()
|
editorVerticalScroll.scrollTo(0)
|
||||||
|
editorHorizontalScroll.scrollTo(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(
|
Dialog(
|
||||||
@@ -92,7 +153,8 @@ fun TextEditorDialog(
|
|||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Edit File",
|
text = title,
|
||||||
|
modifier = titleTag?.let { Modifier.testTag(it) } ?: Modifier,
|
||||||
color = TextPrimary,
|
color = TextPrimary,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
@@ -101,16 +163,29 @@ fun TextEditorDialog(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
EditorButton(label = "Save", enabled = state.saveAsPath.isNotBlank(), onClick = onSave)
|
EditorButton(
|
||||||
EditorButton(label = "Dismiss", onClick = onClose)
|
label = "Save",
|
||||||
|
enabled = saveEnabled,
|
||||||
|
modifier = Modifier.testTag("$testTagPrefix-save"),
|
||||||
|
onClick = onSave,
|
||||||
|
)
|
||||||
|
EditorButton(
|
||||||
|
label = "Dismiss",
|
||||||
|
modifier = Modifier.testTag("$testTagPrefix-dismiss"),
|
||||||
|
onClick = onClose,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "${state.editor} ${state.path.ifBlank { "<new file>" }}",
|
text = metadata,
|
||||||
color = TextPrimary,
|
modifier = metadataTag?.let { Modifier.testTag(it) } ?: Modifier,
|
||||||
fontWeight = FontWeight.Bold,
|
color = metadataStyle.color,
|
||||||
|
fontFamily = metadataStyle.fontFamily,
|
||||||
|
fontWeight = metadataStyle.fontWeight,
|
||||||
|
fontSize = metadataStyle.fontSize,
|
||||||
)
|
)
|
||||||
|
saveAsPath?.let { path ->
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.saveAsPath,
|
value = path,
|
||||||
onValueChange = onSaveAsPathChange,
|
onValueChange = onSaveAsPathChange,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
@@ -121,6 +196,7 @@ fun TextEditorDialog(
|
|||||||
),
|
),
|
||||||
label = { Text("File name") },
|
label = { Text("File name") },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -132,11 +208,11 @@ fun TextEditorDialog(
|
|||||||
.verticalScroll(editorVerticalScroll),
|
.verticalScroll(editorVerticalScroll),
|
||||||
) {
|
) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = state.content,
|
value = content,
|
||||||
onValueChange = onContentChange,
|
onValueChange = onContentChange,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.sizeIn(minWidth = 1200.dp, minHeight = 1200.dp)
|
.sizeIn(minWidth = 1200.dp, minHeight = 1200.dp)
|
||||||
.focusRequester(contentFocusRequester),
|
.testTag("$testTagPrefix-content"),
|
||||||
textStyle = TextStyle(
|
textStyle = TextStyle(
|
||||||
color = TextPrimary,
|
color = TextPrimary,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
@@ -155,15 +231,34 @@ fun TextEditorDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum class EditorMetadataStyle(
|
||||||
|
val color: Color,
|
||||||
|
val fontFamily: FontFamily? = null,
|
||||||
|
val fontWeight: FontWeight? = null,
|
||||||
|
val fontSize: TextUnit = 14.sp,
|
||||||
|
) {
|
||||||
|
Prominent(
|
||||||
|
color = TextPrimary,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
),
|
||||||
|
SecondaryMonospace(
|
||||||
|
color = TextSecondary,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EditorButton(
|
private fun EditorButton(
|
||||||
label: String,
|
label: String,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
modifier = modifier,
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = Accent,
|
containerColor = Accent,
|
||||||
contentColor = AppBackground,
|
contentColor = AppBackground,
|
||||||
|
|||||||
Reference in New Issue
Block a user