Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
60315098e2
|
|||
|
4b112d47f4
|
@@ -94,35 +94,6 @@ build/outputs/apk/debug/a3270-debug.apk
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ How to Tag & Publish (Beta) Releases
|
||||
|
||||
Releases can be published automatically to Gitea via Git tags:
|
||||
|
||||
```bash
|
||||
# 1. Create a version tag (stable or beta)
|
||||
git tag -a v0.1.0-beta.1 -m "Release v0.1.0-beta.1"
|
||||
|
||||
# Or a stable release:
|
||||
git tag -a v0.1.0 -m "Release v0.1.0"
|
||||
|
||||
# 2. Push the tag to Gitea
|
||||
git push origin v0.1.0-beta.1
|
||||
```
|
||||
|
||||
Pushing any `v*` tag triggers the automated release workflow (`.gitea/workflows/release.yaml`), which:
|
||||
1. Automatically detects pre-releases (tags containing `beta`, `alpha`, `rc`, `pre`, `preview`, or `dev`) and marks them as **Pre-release** on Gitea.
|
||||
2. Builds the APK and packages it.
|
||||
3. Publishes a new Gitea Release with `a3270.apk` attached automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Topics & Discoverability
|
||||
|
||||
Suggested keywords/topics for repository indexing:
|
||||
`tn3270` · `tn3270e` · `ibm-3270` · `mainframe` · `terminal-emulator` · `android` · `android-app` · `jetpack-compose` · `kotlin` · `x3270` · `j3270` · `ind-file` · `file-transfer` · `ebcdic` · `z-vm` · `z-os` · `cms` · `tso` · `cics`
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
|
||||
|
||||
@@ -6,11 +6,11 @@ cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=== Building a3270 (Android APK) ==="
|
||||
|
||||
# Set up JAVA_HOME if not already set
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -d "/Users/rudi/.sdkman/candidates/java/21.0.6-sem" ]; then
|
||||
export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/21.0.6-sem"
|
||||
elif [ -d "/Users/rudi/.sdkman/candidates/java/current" ]; then
|
||||
# Set up JAVA_HOME (prefer JDK 21 if available)
|
||||
if [ -d "/Users/rudi/.sdkman/candidates/java/21.0.6-sem" ]; then
|
||||
export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/21.0.6-sem"
|
||||
elif [ -z "$JAVA_HOME" ]; then
|
||||
if [ -d "/Users/rudi/.sdkman/candidates/java/current" ]; then
|
||||
export JAVA_HOME="/Users/rudi/.sdkman/candidates/java/current"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.pubvm.a3270.service.TerminalService
|
||||
import org.pubvm.a3270.storage.HostStorage
|
||||
import org.pubvm.a3270.ui.ConnectDialog
|
||||
import org.pubvm.a3270.ui.FileTransferDialog
|
||||
import org.pubvm.a3270.ui.SettingsDialog
|
||||
import org.pubvm.a3270.ui.TwoRowKeyBar
|
||||
import org.pubvm.a3270.ui.OiaStatusBar
|
||||
import org.pubvm.a3270.ui.TerminalView
|
||||
@@ -156,9 +157,12 @@ fun MainScreen(
|
||||
val oiaText by viewModel.oiaText.collectAsState()
|
||||
val isKeyboardLocked by viewModel.isKeyboardLocked.collectAsState()
|
||||
val screenVersion by viewModel.screenVersion.collectAsState()
|
||||
val maskHiddenInput by viewModel.maskHiddenInput.collectAsState()
|
||||
val cursorBlink by viewModel.cursorBlink.collectAsState()
|
||||
|
||||
var showConnectDialog by remember { mutableStateOf(false) }
|
||||
var showFtDialog by remember { mutableStateOf(false) }
|
||||
var showSettingsDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
@@ -213,6 +217,8 @@ fun MainScreen(
|
||||
cols = cols,
|
||||
cursorAddr = cursorAddr,
|
||||
screenVersion = screenVersion,
|
||||
maskHiddenFields = maskHiddenInput,
|
||||
blinkCursor = cursorBlink,
|
||||
onTapAddress = { addr ->
|
||||
viewModel.setCursor(addr)
|
||||
focusRequester.requestFocus()
|
||||
@@ -279,6 +285,7 @@ fun MainScreen(
|
||||
onConnectClick = { showConnectDialog = true },
|
||||
onDisconnectClick = { viewModel.disconnect() },
|
||||
onFtClick = { showFtDialog = true },
|
||||
onSettingsClick = { showSettingsDialog = true },
|
||||
onSendAid = { aid ->
|
||||
viewModel.sendAid(aid)
|
||||
focusRequester.requestFocus()
|
||||
@@ -345,4 +352,17 @@ fun MainScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showSettingsDialog) {
|
||||
SettingsDialog(
|
||||
initialMaskHiddenInput = maskHiddenInput,
|
||||
initialCursorBlink = cursorBlink,
|
||||
onDismiss = { showSettingsDialog = false },
|
||||
onSave = { mask, blink ->
|
||||
showSettingsDialog = false
|
||||
viewModel.updateSettings(mask, blink)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.lib3270j.listener.ScreenUpdateListener
|
||||
import org.lib3270j.protocol.DS3270Constants.faIsProtected
|
||||
import org.lib3270j.screen.ScreenBuffer
|
||||
import org.pubvm.a3270.service.TerminalService
|
||||
import org.pubvm.a3270.storage.AppSettings
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.logging.Logger
|
||||
|
||||
@@ -55,6 +56,19 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
private val _isKeyboardLocked = MutableStateFlow(false)
|
||||
val isKeyboardLocked: StateFlow<Boolean> = _isKeyboardLocked.asStateFlow()
|
||||
|
||||
private val _maskHiddenInput = MutableStateFlow(AppSettings.isMaskHiddenInputEnabled(application))
|
||||
val maskHiddenInput: StateFlow<Boolean> = _maskHiddenInput.asStateFlow()
|
||||
|
||||
private val _cursorBlink = MutableStateFlow(AppSettings.isCursorBlinkEnabled(application))
|
||||
val cursorBlink: StateFlow<Boolean> = _cursorBlink.asStateFlow()
|
||||
|
||||
fun updateSettings(maskHidden: Boolean, blink: Boolean) {
|
||||
AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden)
|
||||
AppSettings.setCursorBlinkEnabled(getApplication(), blink)
|
||||
_maskHiddenInput.value = maskHidden
|
||||
_cursorBlink.value = blink
|
||||
}
|
||||
|
||||
private var client: Telnet3270Client? = null
|
||||
var currentHost: String = ""
|
||||
private set
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.pubvm.a3270.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
object AppSettings {
|
||||
private const val PREFS_NAME = "a3270_settings"
|
||||
private const val KEY_MASK_HIDDEN_INPUT = "mask_hidden_input"
|
||||
private const val KEY_CURSOR_BLINK = "cursor_blink"
|
||||
|
||||
private fun getPrefs(context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
fun isMaskHiddenInputEnabled(context: Context): Boolean {
|
||||
return getPrefs(context).getBoolean(KEY_MASK_HIDDEN_INPUT, true)
|
||||
}
|
||||
|
||||
fun setMaskHiddenInputEnabled(context: Context, enabled: Boolean) {
|
||||
getPrefs(context).edit().putBoolean(KEY_MASK_HIDDEN_INPUT, enabled).apply()
|
||||
}
|
||||
|
||||
fun isCursorBlinkEnabled(context: Context): Boolean {
|
||||
return getPrefs(context).getBoolean(KEY_CURSOR_BLINK, true)
|
||||
}
|
||||
|
||||
fun setCursorBlinkEnabled(context: Context, enabled: Boolean) {
|
||||
getPrefs(context).edit().putBoolean(KEY_CURSOR_BLINK, enabled).apply()
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ fun TwoRowKeyBar(
|
||||
onConnectClick: () -> Unit,
|
||||
onDisconnectClick: () -> Unit,
|
||||
onFtClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit = {},
|
||||
onSendAid: (Int) -> Unit,
|
||||
onReset: () -> Unit,
|
||||
onTab: () -> Unit,
|
||||
@@ -51,7 +52,7 @@ fun TwoRowKeyBar(
|
||||
.padding(horizontal = 1.dp, vertical = 1.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// 1. Menu Dropdown Button (Connect/Disconnect/FT)
|
||||
// 1. Menu Dropdown Button (Connect/Disconnect/FT/Settings)
|
||||
Box(modifier = Modifier.weight(1f)) {
|
||||
KeyButton(
|
||||
label = "⋮",
|
||||
@@ -89,6 +90,13 @@ fun TwoRowKeyBar(
|
||||
}
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text("Settings", color = Color.White, fontWeight = FontWeight.SemiBold) },
|
||||
onClick = {
|
||||
menuExpanded = false
|
||||
onSettingsClick()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +186,7 @@ fun TwoRowKeyBar(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KeyButton(
|
||||
label: String,
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package org.pubvm.a3270.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
|
||||
@Composable
|
||||
fun SettingsDialog(
|
||||
initialMaskHiddenInput: Boolean,
|
||||
initialCursorBlink: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean) -> Unit
|
||||
) {
|
||||
var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) }
|
||||
var cursorBlink by remember { mutableStateOf(initialCursorBlink) }
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color(0xFF1E1E1E)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Terminal Settings",
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Setting 1: Mask Protected/Hidden Input with '*'
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text(
|
||||
text = "Show '*' for Hidden Fields",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "Display asterisks for typed characters in non-display / password fields rather than blank spaces.",
|
||||
fontSize = 12.sp,
|
||||
color = Color.LightGray
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = maskHiddenInput,
|
||||
onCheckedChange = { maskHiddenInput = it },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
checkedTrackColor = Color(0xFF2B8A3E)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Setting 2: Blinking Cursor
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text(
|
||||
text = "Blinking Cursor",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "Blink the terminal cursor to clearly indicate current screen position.",
|
||||
fontSize = 12.sp,
|
||||
color = Color.LightGray
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = cursorBlink,
|
||||
onCheckedChange = { cursorBlink = it },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
checkedTrackColor = Color(0xFF2B8A3E)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Action Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("Cancel", color = Color.LightGray)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
onSave(maskHiddenInput, cursorBlink)
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||
) {
|
||||
Text("Save", color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import org.lib3270j.protocol.DS3270Constants.*
|
||||
import org.lib3270j.screen.ExtendedAttribute
|
||||
import org.lib3270j.screen.ScreenBuffer
|
||||
@@ -57,6 +58,8 @@ fun TerminalView(
|
||||
cols: Int,
|
||||
cursorAddr: Int,
|
||||
screenVersion: Long,
|
||||
maskHiddenFields: Boolean = true,
|
||||
blinkCursor: Boolean = true,
|
||||
onTapAddress: (Int) -> Unit,
|
||||
onPasteText: (String) -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
@@ -70,6 +73,20 @@ fun TerminalView(
|
||||
var showContextMenu by remember { mutableStateOf(false) }
|
||||
var contextMenuOffset by remember { mutableStateOf(Offset.Zero) }
|
||||
|
||||
// Blinking cursor state (~530ms interval matching j3270)
|
||||
var cursorVisible by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(cursorAddr, blinkCursor) {
|
||||
if (!blinkCursor) {
|
||||
cursorVisible = true
|
||||
return@LaunchedEffect
|
||||
}
|
||||
cursorVisible = true
|
||||
while (true) {
|
||||
delay(530L)
|
||||
cursorVisible = !cursorVisible
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
@@ -213,9 +230,19 @@ fun TerminalView(
|
||||
isBold = true
|
||||
}
|
||||
|
||||
// Invisible fields (passwords / hidden)
|
||||
// Invisible fields (passwords / hidden inputs)
|
||||
if (faIsZero(currentFA.toInt() and 0xFF)) {
|
||||
continue
|
||||
if (maskHiddenFields && ea.ucs4 > ' ' && ea.ucs4.code != 0xFFFF) {
|
||||
charVal = '*'
|
||||
} else {
|
||||
// Blanks out character
|
||||
charVal = ' '
|
||||
}
|
||||
} else {
|
||||
// Normal visible character
|
||||
if (ea.ucs4 > ' ' && ea.ucs4.code != 0xFFFF) {
|
||||
charVal = ea.ucs4
|
||||
}
|
||||
}
|
||||
|
||||
// Extended Graphic Rendition
|
||||
@@ -226,10 +253,6 @@ fun TerminalView(
|
||||
if ((grVal and GR_UNDERLINE) != 0) isUnderline = true
|
||||
if ((grVal and GR_REVERSE) != 0) isReverse = true
|
||||
}
|
||||
|
||||
if (ea.ucs4 > ' ' && ea.ucs4.code != 0xFFFF) {
|
||||
charVal = ea.ucs4
|
||||
}
|
||||
} else {
|
||||
fgColor = HOST_COLORS[HOST_COLOR_GREEN]
|
||||
}
|
||||
@@ -258,7 +281,8 @@ fun TerminalView(
|
||||
)
|
||||
}
|
||||
|
||||
if (addr == cursorAddr && !isSelected) {
|
||||
// Cursor indicator (respects blinking toggle & timer)
|
||||
if (addr == cursorAddr && !isSelected && cursorVisible) {
|
||||
drawRect(
|
||||
color = HOST_COLORS[HOST_COLOR_TURQUOISE].copy(alpha = 0.5f),
|
||||
topLeft = Offset(left, top),
|
||||
|
||||
Reference in New Issue
Block a user