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