diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
index d898b506b..e8308bf55 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt
@@ -16,12 +16,14 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.ButtonDefaults
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Checkbox
import androidx.compose.material.Chip
@@ -41,6 +43,9 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.twotone.Check
import androidx.compose.material.icons.twotone.Close
+import androidx.compose.material.Button
+import androidx.compose.material.Surface
+import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -72,6 +77,8 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -180,9 +187,12 @@ fun ChannelScreen(
val channelUrl = channelSet.getChannelUrl()
val modemPresetName = Channel(loraConfig = channelSet.loraConfig).name
+ val userScannedQrCode = remember { mutableStateOf(false) }
+ val scannedQR = remember { mutableStateOf("") }
val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
if (result.contents != null) {
- viewModel.setRequestChannelUrl(Uri.parse(result.contents))
+ scannedQR.value = result.contents
+ userScannedQrCode.value = true
}
}
@@ -293,6 +303,15 @@ fun ChannelScreen(
.show()
}
+ if (userScannedQrCode.value)
+ /* Prompt the user to modify channels after scanning a QR code. */
+ ScannedQrCodeDialog(
+ channels = channels,
+ incoming = Uri.parse(scannedQR.value).toChannelSet(),
+ onDismiss = { userScannedQrCode.value = false },
+ onConfirm = { newChannelSet -> installSettings(newChannelSet) }
+ )
+
var showEditChannelDialog: Int? by remember { mutableStateOf(null) }
if (showEditChannelDialog != null) {
@@ -547,6 +566,158 @@ private fun ChannelSelection(
}
}
+/**
+ * Enables the user to select which channels to accept after scanning a QR code.
+ */
+@Suppress("LongMethod")
+@Composable
+fun ScannedQrCodeDialog(
+ channels: AppOnlyProtos.ChannelSet,
+ incoming: AppOnlyProtos.ChannelSet,
+ onDismiss: () -> Unit,
+ onConfirm: (AppOnlyProtos.ChannelSet) -> Unit
+) {
+ var currentChannelSet by remember(channels) { mutableStateOf(channels) }
+ val modemPresetName = Channel(loraConfig = currentChannelSet.loraConfig).name
+
+ /* Holds selections made by the user */
+ val channelSelections = remember { mutableStateListOf(elements = Array(size = 8, init = { true })) }
+
+ /* The save button is enabled based on this count */
+ var totalCount = currentChannelSet.settingsList.size
+ for ((index, isSelected) in channelSelections.withIndex()) {
+ if (index >= incoming.settingsList.size)
+ break
+ if (isSelected)
+ totalCount++
+ }
+
+ Dialog(
+ onDismissRequest = { onDismiss() },
+ properties = DialogProperties(usePlatformDefaultWidth = false, dismissOnBackPress = true)
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ shape = RoundedCornerShape(16.dp),
+ color = MaterialTheme.colors.background
+ ) {
+ LazyColumn(
+ contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ /* Incoming ChannelSet */
+ item {
+ Text(
+ style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.scanned_channels)
+ )
+ }
+ itemsIndexed(incoming.settingsList) { index, channel ->
+ ChannelSelection(
+ index = index,
+ title = channel.name.ifEmpty { modemPresetName },
+ enabled = true,
+ isSelected = channelSelections[index],
+ onSelected = { channelSelections[index] = it }
+ )
+ }
+
+ /* Current ChannelSet */
+ item {
+ Text(
+ style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.current_channels)
+ )
+ }
+ itemsIndexed(currentChannelSet.settingsList) { index, channel ->
+ ChannelCard(
+ index = index,
+ title = channel.name.ifEmpty { modemPresetName },
+ enabled = true,
+ onEditClick = { /* Currently we don't enable editing from this dialog. */ },
+ onDeleteClick = {
+ val list = currentChannelSet.settingsList.toMutableList()
+ list.removeAt(index)
+ currentChannelSet = currentChannelSet.copy {
+ settings.clear()
+ settings.addAll(list)
+ }
+ }
+ )
+ }
+
+ /* User Actions via buttons */
+ item {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ /* Cancel */
+ Button(
+ onClick = onDismiss,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp)
+ .weight(1f)
+ .padding(3.dp)
+ ) {
+ Text(
+ style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.cancel)
+ )
+ }
+
+ /* Add - Appends incoming selected channels to the current set */
+ Button(
+ enabled = totalCount <= 8,
+ onClick = {
+ val appended = incoming.copy {
+ val result = settings.filterIndexed { i, _ ->
+ channelSelections.getOrNull(i) == true
+ }
+ settings.clear()
+ settings.addAll(currentChannelSet.settingsList)
+ settings.addAll(result)
+ }
+ onDismiss.invoke()
+ onConfirm.invoke(appended)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp)
+ .weight(1f)
+ .padding(3.dp)
+ ) {
+ Text(
+ style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.add)
+ )
+ }
+
+ /* Replace - Replaces the previous set with the scanned channel set */
+ Button(
+ onClick = {
+ onDismiss.invoke()
+ onConfirm.invoke(incoming)
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(48.dp)
+ .weight(1f)
+ .padding(3.dp)
+ ) {
+ Text(
+ style = MaterialTheme.typography.body1,
+ text = stringResource(id = R.string.replace)
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
@Preview(showBackground = true)
@Composable
private fun ChannelScreenPreview() {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b5855ff72..06b6c9401 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -206,4 +206,7 @@
8 hours
1 week
Always
+ Scanned Channels
+ Current Channels
+ Replace