diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/CopyIconButton.kt b/app/src/main/java/com/geeksville/mesh/ui/components/CopyIconButton.kt new file mode 100644 index 000000000..05a9b0126 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/CopyIconButton.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.ui.components + +import android.content.ClipData +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.ContentCopy +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.stringResource +import com.geeksville.mesh.R + +@Composable +fun CopyIconButton( + valueToCopy: String, + modifier: Modifier = Modifier, + label: String = stringResource(id = R.string.copy), +) { + val clipboardManager = LocalClipboardManager.current + IconButton( + modifier = modifier, + onClick = { + val clipData = ClipData.newPlainText(label, valueToCopy) + val clipEntry = ClipEntry(clipData) + clipboardManager.setClip(clipEntry) + } + ) { + Icon( + imageVector = Icons.TwoTone.ContentCopy, + contentDescription = label + ) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/EditBase64Preference.kt b/app/src/main/java/com/geeksville/mesh/ui/components/EditBase64Preference.kt index b5878ac7b..18daf11db 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/EditBase64Preference.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/EditBase64Preference.kt @@ -17,7 +17,6 @@ package com.geeksville.mesh.ui.components -import android.util.Base64 import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions @@ -47,22 +46,23 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.geeksville.mesh.R import com.geeksville.mesh.model.Channel +import com.geeksville.mesh.util.encodeToString +import com.geeksville.mesh.util.toByteString import com.google.protobuf.ByteString -import com.google.protobuf.kotlin.toByteString +@Suppress("LongMethod") @Composable fun EditBase64Preference( + modifier: Modifier = Modifier, title: String, value: ByteString, enabled: Boolean, + readOnly: Boolean = false, keyboardActions: KeyboardActions, onValueChange: (ByteString) -> Unit, - modifier: Modifier = Modifier, onGenerateKey: (() -> Unit)? = null, trailingIcon: (@Composable () -> Unit)? = null, ) { - fun ByteString.encodeToString() = Base64.encodeToString(this.toByteArray(), Base64.NO_WRAP) - fun String.toByteString() = Base64.decode(this, Base64.NO_WRAP).toByteString() var valueState by remember { mutableStateOf(value.encodeToString()) } val isError = value.encodeToString() != valueState @@ -91,6 +91,7 @@ fun EditBase64Preference( .fillMaxWidth() .onFocusChanged { focusState -> isFocused = focusState.isFocused }, enabled = enabled, + readOnly = readOnly, label = { Text(text = title) }, isError = isError, keyboardOptions = KeyboardOptions.Default.copy( @@ -98,9 +99,7 @@ fun EditBase64Preference( ), keyboardActions = keyboardActions, trailingIcon = { - if (trailingIcon != null) { - trailingIcon() - } else if (icon != null) { + if (icon != null) { IconButton( onClick = { if (isError) { @@ -122,6 +121,8 @@ fun EditBase64Preference( } ) } + } else if (trailingIcon != null) { + trailingIcon() } }, ) diff --git a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/SecurityConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/SecurityConfigItemList.kt index 53d36c9e5..35b84d5be 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/SecurityConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/radioconfig/components/SecurityConfigItemList.kt @@ -34,12 +34,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig import com.geeksville.mesh.config import com.geeksville.mesh.copy +import com.geeksville.mesh.ui.components.CopyIconButton import com.geeksville.mesh.ui.components.EditBase64Preference import com.geeksville.mesh.ui.components.EditListPreference import com.geeksville.mesh.ui.components.PreferenceCategory import com.geeksville.mesh.ui.components.PreferenceFooter import com.geeksville.mesh.ui.components.SwitchPreference import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel +import com.geeksville.mesh.util.encodeToString @Composable fun SecurityConfigScreen( @@ -83,13 +85,19 @@ fun SecurityConfigItemList( EditBase64Preference( title = "Public Key", value = securityInput.publicKey, - enabled = false, + enabled = enabled, + readOnly = true, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChange = { if (it.size() == 32) { securityInput = securityInput.copy { publicKey = it } } }, + trailingIcon = { + CopyIconButton( + valueToCopy = securityInput.privateKey.encodeToString(), + ) + } ) } @@ -104,6 +112,11 @@ fun SecurityConfigItemList( securityInput = securityInput.copy { privateKey = it } } }, + trailingIcon = { + CopyIconButton( + valueToCopy = securityInput.privateKey.encodeToString(), + ) + } ) } diff --git a/app/src/main/java/com/geeksville/mesh/util/ByteStringExtensions.kt b/app/src/main/java/com/geeksville/mesh/util/ByteStringExtensions.kt new file mode 100644 index 000000000..a3de63f42 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/util/ByteStringExtensions.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.geeksville.mesh.util + +import android.util.Base64 +import com.google.protobuf.ByteString +import com.google.protobuf.kotlin.toByteString + +fun ByteString.encodeToString() = Base64.encodeToString(this.toByteArray(), Base64.NO_WRAP) +fun String.toByteString() = Base64.decode(this, Base64.NO_WRAP).toByteString()