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 079b8d6b6..0387e0fae 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -11,19 +11,14 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row 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.foundation.shape.RoundedCornerShape import androidx.compose.material.ContentAlpha import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -39,8 +34,6 @@ 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 @@ -54,7 +47,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter @@ -73,12 +65,10 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.PreviewScreenSizes 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 -import com.geeksville.mesh.AppOnlyProtos +import com.geeksville.mesh.AppOnlyProtos.ChannelSet import com.geeksville.mesh.analytics.DataPair import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.Logging @@ -102,6 +92,7 @@ import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.ui.components.AdaptiveTwoPane import com.geeksville.mesh.ui.components.DropDownPreference import com.geeksville.mesh.ui.components.PreferenceFooter +import com.geeksville.mesh.ui.components.ScannedQrCodeDialog import com.geeksville.mesh.ui.components.config.ChannelCard import com.geeksville.mesh.ui.components.config.ChannelSelection import com.geeksville.mesh.ui.components.config.EditChannelDialog @@ -179,7 +170,7 @@ fun ChannelScreen( val channelUrl = channelSet.getChannelUrl() val modemPresetName = Channel(loraConfig = channelSet.loraConfig).name - var scannedChannelSet by remember { mutableStateOf(null) } + var scannedChannelSet by remember { mutableStateOf(null) } val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result -> if (result.contents != null) { try { @@ -234,7 +225,7 @@ fun ChannelScreen( /// Send new channel settings to the device fun installSettings( - newChannelSet: AppOnlyProtos.ChannelSet + newChannelSet: ChannelSet ) { // Try to change the radio, if it fails, tell the user why and throw away their edits try { @@ -491,7 +482,7 @@ fun ChannelScreen( @Composable private fun QrCodeImage( enabled: Boolean, - channelSet: AppOnlyProtos.ChannelSet, + channelSet: ChannelSet, modifier: Modifier = Modifier, ) = Image( painter = channelSet.qrCode @@ -507,7 +498,7 @@ private fun QrCodeImage( @Composable private fun ChannelListView( enabled: Boolean, - channelSet: AppOnlyProtos.ChannelSet, + channelSet: ChannelSet, modemPresetName: String, channelSelections: SnapshotStateList, onClick: () -> Unit = {}, @@ -551,158 +542,6 @@ private fun ChannelListView( ) } -/** - * 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) - ) - } - } - } - } - } - } -} - @PreviewScreenSizes @Composable private fun ChannelScreenPreview() { diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/ScannedQrCodeDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/ScannedQrCodeDialog.kt new file mode 100644 index 000000000..94dd4b49a --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/ScannedQrCodeDialog.kt @@ -0,0 +1,205 @@ +package com.geeksville.mesh.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.geeksville.mesh.AppOnlyProtos.ChannelSet +import com.geeksville.mesh.R +import com.geeksville.mesh.channelSet +import com.geeksville.mesh.copy +import com.geeksville.mesh.model.Channel +import com.geeksville.mesh.ui.components.config.ChannelCard +import com.geeksville.mesh.ui.components.config.ChannelSelection + +/** + * Enables the user to select which channels to accept after scanning a QR code. + */ +@Suppress("LongMethod") +@Composable +fun ScannedQrCodeDialog( + channels: ChannelSet, + incoming: ChannelSet, + onDismiss: () -> Unit, + onConfirm: (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) + ) + } + } + } + } + } + } +} + +@PreviewScreenSizes +@Composable +private fun ScannedQrCodeDialogPreview() { + ScannedQrCodeDialog( + channels = channelSet { + settings.add(Channel.default.settings) + loraConfig = Channel.default.loraConfig + }, + incoming = channelSet { + settings.add(Channel.default.settings) + loraConfig = Channel.default.loraConfig + }, + onDismiss = {}, + onConfirm = {}, + ) +}