Fix runtime diagnostics, rotation stability, and manpage search positioning
- Expand the missing-native-Git message with the selected ABI, 32/64-bit ABI lists, Android version, device model/platform details, expected binary path, and a request to report the information so device support can be added. - Use application-scoped DataStore delegates for game progress and pane layout preferences to avoid duplicate DataStore instances during activity recreation, such as screen orientation changes. - Center manpage search matches in the visible scroll viewport when possible and apply IME padding so keyboard-covered space is accounted for. - Support mobile-friendly handling for interactive add/stage commands (`git add -i`, `git stage -i`, and `git add --interactive`) by routing them through GitHug's staging semantics instead of rejecting them or launching Git's terminal UI. - Apply IME padding to editor dialogs so controls remain reachable while the keyboard is displayed.
This commit is contained in:
@@ -1,173 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_DIR="$SCRIPT_DIR"
|
|
||||||
SDK_DIR="$PROJECT_DIR/android-sdk"
|
|
||||||
NDK_DIR="$SDK_DIR/ndk/27.2.12479018"
|
|
||||||
TOOLBIN="$NDK_DIR/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
|
||||||
TMP_DIR="$PROJECT_DIR/.tmp-android-build"
|
|
||||||
GIT_SRC_DIR="$TMP_DIR/git-src"
|
|
||||||
HOST_GIT_DIR="$PROJECT_DIR/build/host-git"
|
|
||||||
HOST_GIT_BINARY="$HOST_GIT_DIR/libgit.so"
|
|
||||||
ANDROID_JNI_DIR="$PROJECT_DIR/app/src/main/jniLibs"
|
|
||||||
ANDROID_API=24
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '\n[%s] %s\n' "compile-git" "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
require_path() {
|
|
||||||
if [ ! -e "$1" ]; then
|
|
||||||
echo "Required path not found: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require_tool() {
|
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
|
||||||
echo "Missing required tool: $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_git_source() {
|
|
||||||
mkdir -p "$TMP_DIR"
|
|
||||||
if [ ! -d "$GIT_SRC_DIR/.git" ]; then
|
|
||||||
log "Cloning Git source"
|
|
||||||
git clone --depth 1 https://github.com/git/git.git "$GIT_SRC_DIR"
|
|
||||||
else
|
|
||||||
log "Using existing Git source checkout"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
common_git_make_args() {
|
|
||||||
printf '%s\n' \
|
|
||||||
NO_OPENSSL=YesPlease \
|
|
||||||
NO_CURL=YesPlease \
|
|
||||||
NO_EXPAT=YesPlease \
|
|
||||||
NO_GETTEXT=YesPlease \
|
|
||||||
NO_TCLTK=YesPlease \
|
|
||||||
NO_PERL=YesPlease \
|
|
||||||
NO_PYTHON=YesPlease \
|
|
||||||
NO_INSTALL_HARDLINKS=YesPlease \
|
|
||||||
NO_ICONV=YesPlease \
|
|
||||||
NO_REGEX=NeedsStartEnd \
|
|
||||||
HAVE_ALLOCA_H=YesPlease \
|
|
||||||
HAVE_PATHS_H=YesPlease \
|
|
||||||
HAVE_CLOCK_GETTIME=YesPlease \
|
|
||||||
HAVE_CLOCK_MONOTONIC=YesPlease \
|
|
||||||
HAVE_GETDELIM=YesPlease \
|
|
||||||
FREAD_READS_DIRECTORIES=UnfortunatelyYes \
|
|
||||||
CSPRNG_METHOD=
|
|
||||||
}
|
|
||||||
|
|
||||||
build_host_git() {
|
|
||||||
log "Building Git for development host"
|
|
||||||
cd "$GIT_SRC_DIR"
|
|
||||||
make clean >/dev/null 2>&1 || true
|
|
||||||
mapfile -t make_args < <(common_git_make_args)
|
|
||||||
make -j"$(nproc 2>/dev/null || printf 4)" "${make_args[@]}" git
|
|
||||||
|
|
||||||
mkdir -p "$HOST_GIT_DIR"
|
|
||||||
cp git "$HOST_GIT_BINARY"
|
|
||||||
chmod 755 "$HOST_GIT_BINARY"
|
|
||||||
|
|
||||||
log "Host test Git binary: $HOST_GIT_BINARY"
|
|
||||||
"$HOST_GIT_BINARY" --version
|
|
||||||
}
|
|
||||||
|
|
||||||
build_android_git_for_abi() {
|
|
||||||
local abi="$1"
|
|
||||||
local cc="$2"
|
|
||||||
local output_dir="$ANDROID_JNI_DIR/$abi"
|
|
||||||
|
|
||||||
require_path "$TOOLBIN/${cc}"
|
|
||||||
|
|
||||||
log "Building Git for Android $abi"
|
|
||||||
cd "$GIT_SRC_DIR"
|
|
||||||
make clean >/dev/null 2>&1 || true
|
|
||||||
mapfile -t make_args < <(common_git_make_args)
|
|
||||||
make -j"$(nproc 2>/dev/null || printf 4)" \
|
|
||||||
uname_S=Android \
|
|
||||||
uname_O=Android \
|
|
||||||
NO_PTHREADS=YesPlease \
|
|
||||||
NO_LIBGEN_H=YesPlease \
|
|
||||||
HAVE_DEV_TTY=YesPlease \
|
|
||||||
CC="$cc" \
|
|
||||||
AR=llvm-ar \
|
|
||||||
RANLIB=llvm-ranlib \
|
|
||||||
STRIP=llvm-strip \
|
|
||||||
"${make_args[@]}" \
|
|
||||||
git
|
|
||||||
|
|
||||||
log "Validating Android $abi binary"
|
|
||||||
file git
|
|
||||||
readelf -h git | sed -n '1,12p'
|
|
||||||
|
|
||||||
mkdir -p "$output_dir"
|
|
||||||
llvm-strip -s git -o "$output_dir/libgit.so"
|
|
||||||
chmod 755 "$output_dir/libgit.so"
|
|
||||||
ls -lh "$output_dir/libgit.so"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_android_git() {
|
|
||||||
require_path "$NDK_DIR"
|
|
||||||
export PATH="$TOOLBIN:$PATH"
|
|
||||||
|
|
||||||
build_android_git_for_abi "arm64-v8a" "aarch64-linux-android${ANDROID_API}-clang"
|
|
||||||
build_android_git_for_abi "armeabi-v7a" "armv7a-linux-androideabi${ANDROID_API}-clang"
|
|
||||||
build_android_git_for_abi "x86" "i686-linux-android${ANDROID_API}-clang"
|
|
||||||
build_android_git_for_abi "x86_64" "x86_64-linux-android${ANDROID_API}-clang"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_usage() {
|
|
||||||
cat <<EOF_USAGE
|
|
||||||
Usage: bash ./CompileGitForAllTargetPlatforms.sh [--host | --android | --all]
|
|
||||||
|
|
||||||
--host Compile Git for the development host and copy it to build/host-git/libgit.so
|
|
||||||
--android Cross-compile Git for Android ABIs served through Google Play
|
|
||||||
--all Compile both host and Android targets (default)
|
|
||||||
EOF_USAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
case "${1:---all}" in
|
|
||||||
--host|--android|--all)
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
print_usage
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown argument: $1" >&2
|
|
||||||
print_usage >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
require_tool git
|
|
||||||
require_tool make
|
|
||||||
require_tool perl
|
|
||||||
require_tool clang
|
|
||||||
require_tool pkg-config
|
|
||||||
require_tool readelf
|
|
||||||
|
|
||||||
ensure_git_source
|
|
||||||
|
|
||||||
case "${1:---all}" in
|
|
||||||
--host)
|
|
||||||
build_host_git
|
|
||||||
;;
|
|
||||||
--android)
|
|
||||||
build_android_git
|
|
||||||
;;
|
|
||||||
--all)
|
|
||||||
build_host_git
|
|
||||||
build_android_git
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -19,8 +19,8 @@ android {
|
|||||||
applicationId = "solutions.tretter.githugandroid"
|
applicationId = "solutions.tretter.githugandroid"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 130
|
versionCode = 131
|
||||||
versionName = "0.1.129"
|
versionName = "0.1.130"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
package solutions.tretter.githugandroid
|
package solutions.tretter.githugandroid
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.emptyPreferences
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
private val Context.gameProgressDataStore by preferencesDataStore(name = "game_progress_preferences")
|
||||||
|
|
||||||
class GameProgressStore(private val context: Context) {
|
class GameProgressStore(private val context: Context) {
|
||||||
init {
|
init {
|
||||||
AppLog.d("ProgressStore", "GameProgressStore initialized")
|
AppLog.d("ProgressStore", "GameProgressStore initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val dataStore = PreferenceDataStoreFactory.create(
|
private val dataStore = context.applicationContext.gameProgressDataStore
|
||||||
produceFile = { context.preferencesDataStoreFile("game_progress_preferences") }
|
|
||||||
)
|
|
||||||
|
|
||||||
private val completedLevelsKey = stringPreferencesKey("completed_levels")
|
private val completedLevelsKey = stringPreferencesKey("completed_levels")
|
||||||
private val activeLevelIdKey = stringPreferencesKey("active_level_id")
|
private val activeLevelIdKey = stringPreferencesKey("active_level_id")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -50,7 +51,8 @@ fun GitMessageEditorDialog(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(0.72f),
|
.fillMaxHeight(0.72f)
|
||||||
|
.imePadding(),
|
||||||
color = PanelPrimary,
|
color = PanelPrimary,
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -58,12 +58,23 @@ class GitRepositoryRuntime private constructor(
|
|||||||
fun isNativeGitAvailable(): Boolean = nativeGitBinary() != null
|
fun isNativeGitAvailable(): Boolean = nativeGitBinary() != null
|
||||||
|
|
||||||
fun unavailableMessage(): String {
|
fun unavailableMessage(): String {
|
||||||
|
val selectedAbi = Build.SUPPORTED_ABIS.firstOrNull() ?: "unknown"
|
||||||
|
val nativeLibraryDir = context?.applicationInfo?.nativeLibraryDir ?: "unknown"
|
||||||
return buildString {
|
return buildString {
|
||||||
append("GitHug Android cannot start because this build does not include a native Git binary for this device ABI.")
|
append("GitHug Android cannot start because this build does not include a native Git binary for this device ABI.")
|
||||||
append("\n\nSupported device ABIs: ")
|
append("\n\nCurrent device ABI: ")
|
||||||
|
append(selectedAbi)
|
||||||
|
append("\nSupported device ABIs: ")
|
||||||
append(Build.SUPPORTED_ABIS.joinToString(", ").ifBlank { "unknown" })
|
append(Build.SUPPORTED_ABIS.joinToString(", ").ifBlank { "unknown" })
|
||||||
append("\nExpected binary: nativeLibraryDir/libgit.so")
|
append("\nSupported 64-bit ABIs: ")
|
||||||
append("\n\nInstall a build that bundles the cross-compiled Git binary for this device.")
|
append(Build.SUPPORTED_64_BIT_ABIS.joinToString(", ").ifBlank { "none" })
|
||||||
|
append("\nSupported 32-bit ABIs: ")
|
||||||
|
append(Build.SUPPORTED_32_BIT_ABIS.joinToString(", ").ifBlank { "none" })
|
||||||
|
append("\nPlatform: Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})")
|
||||||
|
append("\nDevice: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE}; ${Build.HARDWARE})")
|
||||||
|
append("\nExpected binary: $nativeLibraryDir/libgit.so")
|
||||||
|
append("\n\nPlease report this information to the developer so support can be added for this device/platform.")
|
||||||
|
append("\nInstall a build that bundles the cross-compiled Git binary for this device.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +135,6 @@ class GitRepositoryRuntime private constructor(
|
|||||||
expandShellPathspecs(currentRepo, shellTokens)
|
expandShellPathspecs(currentRepo, shellTokens)
|
||||||
}.normalizeGitStageAlias()
|
}.normalizeGitStageAlias()
|
||||||
|
|
||||||
rejectUnsupportedInteractiveGitCommand(currentRepo, expandedTokens)?.let { return it }
|
|
||||||
|
|
||||||
executeSyntheticGitCommand(currentRepo, command, expandedTokens)?.let { return it }
|
executeSyntheticGitCommand(currentRepo, command, expandedTokens)?.let { return it }
|
||||||
|
|
||||||
val result = when (expandedTokens.first()) {
|
val result = when (expandedTokens.first()) {
|
||||||
@@ -538,7 +547,7 @@ class GitRepositoryRuntime private constructor(
|
|||||||
) to listOf("Cloned ${tokens[2]} into $target")
|
) to listOf("Cloned ${tokens[2]} into $target")
|
||||||
}
|
}
|
||||||
val shouldUseSandboxSemantics = when (gitCommand) {
|
val shouldUseSandboxSemantics = when (gitCommand) {
|
||||||
"add" -> tokens.any { it == "-p" || it == "--patch" }
|
"add" -> tokens.any { it == "-p" || it == "--patch" || it == "-i" || it == "--interactive" }
|
||||||
"rebase" -> "-i" in tokens || "--onto" in tokens
|
"rebase" -> "-i" in tokens || "--onto" in tokens
|
||||||
"merge" -> "--squash" in tokens || tokens.lastOrNull() == "mybranch" || tokens.lastOrNull() == "feature"
|
"merge" -> "--squash" in tokens || tokens.lastOrNull() == "mybranch" || tokens.lastOrNull() == "feature"
|
||||||
"revert", "stash" -> true
|
"revert", "stash" -> true
|
||||||
@@ -561,19 +570,6 @@ class GitRepositoryRuntime private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun rejectUnsupportedInteractiveGitCommand(
|
|
||||||
currentRepo: RepoState,
|
|
||||||
tokens: List<String>,
|
|
||||||
): Pair<RepoState, List<String>>? {
|
|
||||||
if (tokens.firstOrNull() != "git") return null
|
|
||||||
val subcommand = tokens.getOrNull(1) ?: return null
|
|
||||||
val interactive = tokens.drop(2).any { it == "-i" || it == "--interactive" }
|
|
||||||
if (subcommand == "add" && interactive) {
|
|
||||||
return currentRepo to listOf("Interactive staging is not supported in the mobile terminal. Use git add <path> or git add -p <path>.")
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun executeEcho(workingDir: File, currentRepo: RepoState, tokens: List<String>): Pair<RepoState, List<String>> {
|
private fun executeEcho(workingDir: File, currentRepo: RepoState, tokens: List<String>): Pair<RepoState, List<String>> {
|
||||||
val redirectIndex = tokens.indexOfFirst { it == ">" || it == ">>" }
|
val redirectIndex = tokens.indexOfFirst { it == ">" || it == ">>" }
|
||||||
if (redirectIndex == -1 || redirectIndex == tokens.lastIndex) {
|
if (redirectIndex == -1 || redirectIndex == tokens.lastIndex) {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ object GitSandboxEngine {
|
|||||||
}
|
}
|
||||||
parts.size >= 2 && (parts[1] == "stage" || parts[1] == "add") -> {
|
parts.size >= 2 && (parts[1] == "stage" || parts[1] == "add") -> {
|
||||||
if (parts.drop(2).any { it == "-i" || it == "--interactive" }) {
|
if (parts.drop(2).any { it == "-i" || it == "--interactive" }) {
|
||||||
return repo to listOf("Interactive staging is not supported in the mobile terminal. Use git add <path> or git add -p <path>.")
|
return interactiveAdd(repo, parts.drop(2))
|
||||||
}
|
}
|
||||||
val target = parts.drop(2).lastOrNull { !it.startsWith("-") }
|
val target = parts.drop(2).lastOrNull { !it.startsWith("-") }
|
||||||
?: return repo to listOf("usage: git add <path>")
|
?: return repo to listOf("usage: git add <path>")
|
||||||
@@ -162,6 +162,28 @@ object GitSandboxEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun interactiveAdd(repo: RepoState, arguments: List<String>): Pair<RepoState, List<String>> {
|
||||||
|
val targets = arguments.filterNot { it == "-i" || it == "--interactive" || it.startsWith("--") }
|
||||||
|
val target = targets.lastOrNull()
|
||||||
|
val updated = repo.files.map { file ->
|
||||||
|
if (file.deleted) {
|
||||||
|
file
|
||||||
|
} else if (target == null || target == "." || file.name == target) {
|
||||||
|
file.copy(staged = true)
|
||||||
|
} else {
|
||||||
|
file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val stagedCount = updated.count { updatedFile ->
|
||||||
|
val before = repo.files.firstOrNull { it.name == updatedFile.name }
|
||||||
|
updatedFile.staged && before?.staged != true
|
||||||
|
}
|
||||||
|
return repo.copy(files = updated) to listOf(
|
||||||
|
"Interactive add is handled by GitHug Android.",
|
||||||
|
"Staged ${if (target == null || target == ".") "$stagedCount file(s)" else target}.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun tokenizeCommand(command: String): List<String> {
|
fun tokenizeCommand(command: String): List<String> {
|
||||||
return tokenizeShellCommand(command).map { it.value }
|
return tokenizeShellCommand(command).map { it.value }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -28,10 +29,13 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
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.unit.IntSize
|
||||||
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
|
||||||
@@ -55,6 +59,8 @@ fun ManPageDialog(
|
|||||||
) {
|
) {
|
||||||
val horizontalScrollState = rememberScrollState()
|
val horizontalScrollState = rememberScrollState()
|
||||||
val verticalScrollState = rememberScrollState()
|
val verticalScrollState = rememberScrollState()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var contentViewportSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
var searchQuery by remember(state.content) { mutableStateOf("") }
|
var searchQuery by remember(state.content) { mutableStateOf("") }
|
||||||
var selectedMatch by remember(state.content) { mutableStateOf(0) }
|
var selectedMatch by remember(state.content) { mutableStateOf(0) }
|
||||||
val contentLines = remember(state.content) { state.content.lines() }
|
val contentLines = remember(state.content) { state.content.lines() }
|
||||||
@@ -106,15 +112,28 @@ fun ManPageDialog(
|
|||||||
|
|
||||||
LaunchedEffect(selectedMatch, matches) {
|
LaunchedEffect(selectedMatch, matches) {
|
||||||
val match = matches.getOrNull(selectedMatch) ?: return@LaunchedEffect
|
val match = matches.getOrNull(selectedMatch) ?: return@LaunchedEffect
|
||||||
verticalScrollState.animateScrollTo((match.lineIndex * 18).coerceAtMost(verticalScrollState.maxValue))
|
val lineHeightPx = with(density) { 18.sp.toPx() }
|
||||||
horizontalScrollState.animateScrollTo((match.columnIndex * 8).coerceAtMost(horizontalScrollState.maxValue))
|
val charWidthPx = with(density) { 8.sp.toPx() }
|
||||||
|
val viewportHeight = contentViewportSize.height.takeIf { it > 0 } ?: 0
|
||||||
|
val viewportWidth = contentViewportSize.width.takeIf { it > 0 } ?: 0
|
||||||
|
val verticalTarget = (
|
||||||
|
match.lineIndex * lineHeightPx - viewportHeight / 2f + lineHeightPx / 2f
|
||||||
|
).toInt()
|
||||||
|
.coerceIn(0, verticalScrollState.maxValue)
|
||||||
|
val horizontalTarget = (
|
||||||
|
match.columnIndex * charWidthPx - viewportWidth / 2f + (match.end - match.start) * charWidthPx / 2f
|
||||||
|
).toInt()
|
||||||
|
.coerceIn(0, horizontalScrollState.maxValue)
|
||||||
|
verticalScrollState.animateScrollTo(verticalTarget)
|
||||||
|
horizontalScrollState.animateScrollTo(horizontalTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(onDismissRequest = onClose) {
|
Dialog(onDismissRequest = onClose) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(0.86f),
|
.fillMaxHeight(0.86f)
|
||||||
|
.imePadding(),
|
||||||
color = PanelPrimary,
|
color = PanelPrimary,
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
) {
|
) {
|
||||||
@@ -204,6 +223,7 @@ fun ManPageDialog(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
.onSizeChanged { contentViewportSize = it }
|
||||||
.background(TerminalBackground, RoundedCornerShape(6.dp))
|
.background(TerminalBackground, RoundedCornerShape(6.dp))
|
||||||
.padding(10.dp)
|
.padding(10.dp)
|
||||||
.horizontalScroll(horizontalScrollState)
|
.horizontalScroll(horizontalScrollState)
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
package solutions.tretter.githugandroid
|
package solutions.tretter.githugandroid
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.emptyPreferences
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
private val Context.paneLayoutDataStore by preferencesDataStore(name = "pane_layout_preferences")
|
||||||
|
|
||||||
class PaneLayoutStore(private val context: Context) {
|
class PaneLayoutStore(private val context: Context) {
|
||||||
private val dataStore = PreferenceDataStoreFactory.create(
|
private val dataStore = context.applicationContext.paneLayoutDataStore
|
||||||
produceFile = { context.preferencesDataStoreFile("pane_layout_preferences") }
|
|
||||||
)
|
|
||||||
|
|
||||||
private val orderKey = stringPreferencesKey("pane_order")
|
private val orderKey = stringPreferencesKey("pane_order")
|
||||||
private val collapsedKey = stringPreferencesKey("pane_collapsed")
|
private val collapsedKey = stringPreferencesKey("pane_collapsed")
|
||||||
@@ -68,4 +67,4 @@ class PaneLayoutStore(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
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.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.layout.widthIn
|
||||||
@@ -57,7 +58,8 @@ fun TextEditorDialog(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(0.86f),
|
.fillMaxHeight(0.86f)
|
||||||
|
.imePadding(),
|
||||||
color = PanelPrimary,
|
color = PanelPrimary,
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -282,8 +282,8 @@ class GitSandboxEngineTest {
|
|||||||
|
|
||||||
val (updatedRepo, output) = GitSandboxEngine.execute(repo, "git stage -i")
|
val (updatedRepo, output) = GitSandboxEngine.execute(repo, "git stage -i")
|
||||||
|
|
||||||
assertEquals(repo, updatedRepo)
|
assertTrue(updatedRepo.files.single { it.name == "README" }.staged)
|
||||||
assertTrue(output.any { it.contains("Interactive staging is not supported") })
|
assertTrue(output.any { it.contains("Interactive add is handled by GitHug Android") })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testGitBinary(): File {
|
private fun testGitBinary(): File {
|
||||||
|
|||||||
Reference in New Issue
Block a user