This commit is contained in:
@@ -147,6 +147,7 @@ fun MainScreen(
|
||||
val screenVersion by viewModel.screenVersion.collectAsState()
|
||||
val maskHiddenInput by viewModel.maskHiddenInput.collectAsState()
|
||||
val cursorBlink by viewModel.cursorBlink.collectAsState()
|
||||
val hapticFeedback by viewModel.hapticFeedback.collectAsState()
|
||||
val ftState by viewModel.ftState.collectAsState()
|
||||
|
||||
var showConnectDialog by remember { mutableStateOf(false) }
|
||||
@@ -249,6 +250,7 @@ fun MainScreen(
|
||||
TwoRowKeyBar(
|
||||
connectionState = connectionState,
|
||||
isShiftPressed = isShiftPressed,
|
||||
hapticFeedbackEnabled = hapticFeedback,
|
||||
onClearShift = onClearShift,
|
||||
onConnectClick = { showConnectDialog = true },
|
||||
onDisconnectClick = { viewModel.disconnect() },
|
||||
@@ -318,9 +320,10 @@ fun MainScreen(
|
||||
ftProgressState = ftState,
|
||||
onDismiss = { showFtDialog = false },
|
||||
onStartTransfer = { config ->
|
||||
val error = viewModel.startFileTransfer(config)
|
||||
if (error != null) {
|
||||
Toast.makeText(context, error, Toast.LENGTH_LONG).show()
|
||||
viewModel.startFileTransfer(config) { error ->
|
||||
if (error != null) {
|
||||
Toast.makeText(context, error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancelTransfer = {
|
||||
@@ -333,10 +336,11 @@ fun MainScreen(
|
||||
SettingsDialog(
|
||||
initialMaskHiddenInput = maskHiddenInput,
|
||||
initialCursorBlink = cursorBlink,
|
||||
initialHapticFeedback = hapticFeedback,
|
||||
onDismiss = { showSettingsDialog = false },
|
||||
onSave = { mask, blink ->
|
||||
onSave = { mask, blink, haptic ->
|
||||
showSettingsDialog = false
|
||||
viewModel.updateSettings(mask, blink)
|
||||
viewModel.updateSettings(mask, blink, haptic)
|
||||
terminalInputViewRef?.showSoftKeyboard()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.lib3270j.ConnectionConfig
|
||||
import org.lib3270j.ConnectionState
|
||||
import org.lib3270j.Telnet3270Client
|
||||
@@ -92,6 +93,9 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
private val _cursorBlink = MutableStateFlow(AppSettings.isCursorBlinkEnabled(application))
|
||||
val cursorBlink: StateFlow<Boolean> = _cursorBlink.asStateFlow()
|
||||
|
||||
private val _hapticFeedback = MutableStateFlow(AppSettings.isHapticFeedbackEnabled(application))
|
||||
val hapticFeedback: StateFlow<Boolean> = _hapticFeedback.asStateFlow()
|
||||
|
||||
// File Transfer State
|
||||
private var fileTransferCoordinator: FileTransfer? = null
|
||||
private val _ftState = MutableStateFlow(FTProgressState())
|
||||
@@ -191,11 +195,13 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSettings(maskHidden: Boolean, blink: Boolean) {
|
||||
fun updateSettings(maskHidden: Boolean, blink: Boolean, haptic: Boolean) {
|
||||
AppSettings.setMaskHiddenInputEnabled(getApplication(), maskHidden)
|
||||
AppSettings.setCursorBlinkEnabled(getApplication(), blink)
|
||||
AppSettings.setHapticFeedbackEnabled(getApplication(), haptic)
|
||||
_maskHiddenInput.value = maskHidden
|
||||
_cursorBlink.value = blink
|
||||
_hapticFeedback.value = haptic
|
||||
}
|
||||
|
||||
fun connect(host: String, port: Int = 23, modelNum: Int = 2, luName: String = "", hostType: String = "TSO") {
|
||||
@@ -316,77 +322,105 @@ class TerminalViewModel(application: Application) : AndroidViewModel(application
|
||||
}
|
||||
}
|
||||
|
||||
fun startFileTransfer(config: FTConfig): String? {
|
||||
val c = client ?: return "Terminal is not connected."
|
||||
|
||||
// Resolve local path if relative (default to app files or download directory)
|
||||
val rawPath = config.localFilename
|
||||
if (!rawPath.startsWith("/")) {
|
||||
val appFilesDir = getApplication<Application>().getExternalFilesDir(null) ?: getApplication<Application>().filesDir
|
||||
val resolvedFile = File(appFilesDir, rawPath)
|
||||
config.localFilename = resolvedFile.absolutePath
|
||||
fun startFileTransfer(config: FTConfig, onResult: (String?) -> Unit = {}) {
|
||||
val c = client
|
||||
if (c == null || !_connectionState.value.isConnected()) {
|
||||
val err = "Terminal is not connected."
|
||||
_ftState.value = FTProgressState(isActive = false, statusMessage = err, isError = true)
|
||||
onResult(err)
|
||||
return
|
||||
}
|
||||
|
||||
if (fileTransferCoordinator == null) {
|
||||
fileTransferCoordinator = FileTransfer(c, object : FileTransfer.FileTransferCallback {
|
||||
override fun onTransferStarted() {
|
||||
_ftState.value = FTProgressState(
|
||||
isActive = true,
|
||||
isRunning = false,
|
||||
bytesTransferred = 0L,
|
||||
statusMessage = "Starting IND\$FILE transfer..."
|
||||
)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Resolve local path if relative (default to app files or download directory)
|
||||
val rawPath = config.localFilename
|
||||
if (!rawPath.startsWith("/")) {
|
||||
val appFilesDir = getApplication<Application>().getExternalFilesDir(null) ?: getApplication<Application>().filesDir
|
||||
val resolvedFile = File(appFilesDir, rawPath)
|
||||
config.localFilename = resolvedFile.absolutePath
|
||||
}
|
||||
|
||||
override fun onTransferRunning() {
|
||||
_ftState.value = _ftState.value.copy(
|
||||
isRunning = true,
|
||||
statusMessage = "Transfer in progress..."
|
||||
)
|
||||
if (fileTransferCoordinator == null) {
|
||||
fileTransferCoordinator = FileTransfer(c, object : FileTransfer.FileTransferCallback {
|
||||
override fun onTransferStarted() {
|
||||
_ftState.value = FTProgressState(
|
||||
isActive = true,
|
||||
isRunning = false,
|
||||
bytesTransferred = 0L,
|
||||
statusMessage = "Starting IND\$FILE transfer..."
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTransferRunning() {
|
||||
_ftState.value = _ftState.value.copy(
|
||||
isRunning = true,
|
||||
statusMessage = "Transfer in progress..."
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBytesTransferred(bytes: Long) {
|
||||
_ftState.value = _ftState.value.copy(
|
||||
bytesTransferred = bytes,
|
||||
statusMessage = "Transferring: $bytes bytes"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTransferComplete(message: String) {
|
||||
_ftState.value = FTProgressState(
|
||||
isActive = false,
|
||||
isRunning = false,
|
||||
bytesTransferred = _ftState.value.bytesTransferred,
|
||||
statusMessage = message,
|
||||
isError = false
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTransferAborted(error: String) {
|
||||
_ftState.value = FTProgressState(
|
||||
isActive = false,
|
||||
isRunning = false,
|
||||
bytesTransferred = _ftState.value.bytesTransferred,
|
||||
statusMessage = "Transfer failed: $error",
|
||||
isError = true
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onBytesTransferred(bytes: Long) {
|
||||
_ftState.value = _ftState.value.copy(
|
||||
bytesTransferred = bytes,
|
||||
statusMessage = "Transferring: $bytes bytes"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTransferComplete(message: String) {
|
||||
val err = fileTransferCoordinator?.startTransfer(config)
|
||||
if (err != null) {
|
||||
_ftState.value = FTProgressState(
|
||||
isActive = false,
|
||||
isRunning = false,
|
||||
bytesTransferred = _ftState.value.bytesTransferred,
|
||||
statusMessage = message,
|
||||
isError = false
|
||||
)
|
||||
}
|
||||
|
||||
override fun onTransferAborted(error: String) {
|
||||
_ftState.value = FTProgressState(
|
||||
isActive = false,
|
||||
isRunning = false,
|
||||
bytesTransferred = _ftState.value.bytesTransferred,
|
||||
statusMessage = "Transfer failed: $error",
|
||||
statusMessage = "Error: $err",
|
||||
isError = true
|
||||
)
|
||||
}
|
||||
})
|
||||
withContext(Dispatchers.Main) {
|
||||
onResult(err)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.warning("Error in startFileTransfer: ${e.message}")
|
||||
_ftState.value = FTProgressState(
|
||||
isActive = false,
|
||||
statusMessage = "Transfer error: ${e.message}",
|
||||
isError = true
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
onResult(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val err = fileTransferCoordinator?.startTransfer(config)
|
||||
if (err != null) {
|
||||
_ftState.value = FTProgressState(
|
||||
isActive = false,
|
||||
statusMessage = "Error: $err",
|
||||
isError = true
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
fun cancelFileTransfer() {
|
||||
fileTransferCoordinator?.cancel()
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
fileTransferCoordinator?.cancel()
|
||||
} catch (e: Exception) {
|
||||
log.warning("Error cancelling transfer: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun typeChar(ch: Char) {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 const val KEY_HAPTIC_FEEDBACK = "haptic_feedback"
|
||||
|
||||
private fun getPrefs(context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
@@ -27,4 +28,12 @@ object AppSettings {
|
||||
fun setCursorBlinkEnabled(context: Context, enabled: Boolean) {
|
||||
getPrefs(context).edit().putBoolean(KEY_CURSOR_BLINK, enabled).apply()
|
||||
}
|
||||
|
||||
fun isHapticFeedbackEnabled(context: Context): Boolean {
|
||||
return getPrefs(context).getBoolean(KEY_HAPTIC_FEEDBACK, true)
|
||||
}
|
||||
|
||||
fun setHapticFeedbackEnabled(context: Context, enabled: Boolean) {
|
||||
getPrefs(context).edit().putBoolean(KEY_HAPTIC_FEEDBACK, enabled).apply()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package org.pubvm.a3270.ui
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
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.focus.focusProperties
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import org.lib3270j.ConnectionState
|
||||
import org.lib3270j.protocol.DS3270Constants.*
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.lib3270j.protocol.DS3270Constants.*
|
||||
fun TwoRowKeyBar(
|
||||
connectionState: ConnectionState,
|
||||
isShiftPressed: Boolean = false,
|
||||
hapticFeedbackEnabled: Boolean = true,
|
||||
onClearShift: () -> Unit = {},
|
||||
onConnectClick: () -> Unit,
|
||||
onDisconnectClick: () -> Unit,
|
||||
@@ -58,6 +59,7 @@ fun TwoRowKeyBar(
|
||||
label = "⋮",
|
||||
color = Color(0xFF343A40),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
hapticFeedbackEnabled = hapticFeedbackEnabled,
|
||||
innerPaddingHorizontal = 0.dp,
|
||||
onClick = { menuExpanded = true }
|
||||
)
|
||||
@@ -106,6 +108,7 @@ fun TwoRowKeyBar(
|
||||
label = tabLabel,
|
||||
color = Color(0xFF1C7ED6),
|
||||
modifier = Modifier.weight(1f),
|
||||
hapticFeedbackEnabled = hapticFeedbackEnabled,
|
||||
innerPaddingHorizontal = 0.dp,
|
||||
onClick = {
|
||||
if (isShiftPressed) {
|
||||
@@ -118,34 +121,34 @@ fun TwoRowKeyBar(
|
||||
)
|
||||
|
||||
// 3. RESET
|
||||
KeyButton("RESET", Color(0xFFE67700), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onReset)
|
||||
KeyButton("RESET", Color(0xFFE67700), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onReset)
|
||||
|
||||
// 4. ENTER
|
||||
KeyButton("ENTER", Color(0xFF2B8A3E), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_ENTER) })
|
||||
KeyButton("ENTER", Color(0xFF2B8A3E), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_ENTER) })
|
||||
|
||||
// 5. CLEAR
|
||||
KeyButton("CLEAR", Color(0xFFC92A2A), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_CLEAR) })
|
||||
KeyButton("CLEAR", Color(0xFFC92A2A), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_CLEAR) })
|
||||
|
||||
// 6. PA1
|
||||
KeyButton("PA1", Color(0xFF495057), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA1) })
|
||||
KeyButton("PA1", Color(0xFF495057), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA1) })
|
||||
|
||||
// 7. PA2
|
||||
KeyButton("PA2", Color(0xFF495057), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA2) })
|
||||
KeyButton("PA2", Color(0xFF495057), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA2) })
|
||||
|
||||
// 8. PA3
|
||||
KeyButton("PA3", Color(0xFF495057), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA3) })
|
||||
KeyButton("PA3", Color(0xFF495057), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = { onSendAid(AID_PA3) })
|
||||
|
||||
// 9. Left Navigation
|
||||
KeyButton("←", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorLeft)
|
||||
KeyButton("←", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorLeft)
|
||||
|
||||
// 10. Up Navigation
|
||||
KeyButton("↑", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorUp)
|
||||
KeyButton("↑", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorUp)
|
||||
|
||||
// 11. Down Navigation
|
||||
KeyButton("↓", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorDown)
|
||||
KeyButton("↓", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorDown)
|
||||
|
||||
// 12. Right Navigation
|
||||
KeyButton("→", Color(0xFF343A40), modifier = Modifier.weight(1f), innerPaddingHorizontal = 0.dp, onClick = onCursorRight)
|
||||
KeyButton("→", Color(0xFF343A40), modifier = Modifier.weight(1f), hapticFeedbackEnabled = hapticFeedbackEnabled, innerPaddingHorizontal = 0.dp, onClick = onCursorRight)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(1.dp).fillMaxWidth().background(Color(0xFF2C2D30)))
|
||||
@@ -174,6 +177,7 @@ fun TwoRowKeyBar(
|
||||
label = "F$i",
|
||||
color = Color(0xFF364FC7),
|
||||
modifier = Modifier.weight(1f),
|
||||
hapticFeedbackEnabled = hapticFeedbackEnabled,
|
||||
innerPaddingHorizontal = 0.dp,
|
||||
onClick = {
|
||||
onSendAid(aid)
|
||||
@@ -193,10 +197,17 @@ private fun KeyButton(
|
||||
color: Color,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
hapticFeedbackEnabled: Boolean = true,
|
||||
innerPaddingHorizontal: androidx.compose.ui.unit.Dp = 0.dp
|
||||
) {
|
||||
val view = LocalView.current
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
onClick = {
|
||||
if (hapticFeedbackEnabled) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
}
|
||||
onClick()
|
||||
},
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = color,
|
||||
shadowElevation = 1.dp,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.pubvm.a3270.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -16,11 +18,13 @@ import androidx.compose.ui.window.Dialog
|
||||
fun SettingsDialog(
|
||||
initialMaskHiddenInput: Boolean,
|
||||
initialCursorBlink: Boolean,
|
||||
initialHapticFeedback: Boolean = true,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean) -> Unit
|
||||
onSave: (maskHiddenInput: Boolean, cursorBlink: Boolean, hapticFeedback: Boolean) -> Unit
|
||||
) {
|
||||
var maskHiddenInput by remember { mutableStateOf(initialMaskHiddenInput) }
|
||||
var cursorBlink by remember { mutableStateOf(initialCursorBlink) }
|
||||
var hapticFeedback by remember { mutableStateOf(initialHapticFeedback) }
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
@@ -33,6 +37,7 @@ fun SettingsDialog(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Text(
|
||||
@@ -106,6 +111,39 @@ fun SettingsDialog(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
HorizontalDivider(color = Color(0xFF2C2D30))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Setting 3: Button Haptic Feedback
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Text(
|
||||
text = "Button Haptic Feedback",
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White
|
||||
)
|
||||
Text(
|
||||
text = "Vibrate softly when tapping function and navigation buttons to match keyboard tactile feedback.",
|
||||
fontSize = 12.sp,
|
||||
color = Color.LightGray
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = hapticFeedback,
|
||||
onCheckedChange = { hapticFeedback = it },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = Color.White,
|
||||
checkedTrackColor = Color(0xFF2B8A3E)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Action Buttons
|
||||
@@ -119,7 +157,7 @@ fun SettingsDialog(
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
onSave(maskHiddenInput, cursorBlink)
|
||||
onSave(maskHiddenInput, cursorBlink, hapticFeedback)
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2B8A3E))
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user