feat: add adaptive two-pane layout to ChannelScreen

This commit is contained in:
andrekir 2024-07-28 08:13:21 -03:00 committed by Andre K
parent df6b0e1949
commit a65cc7699e
4 changed files with 167 additions and 101 deletions

View file

@ -24,11 +24,7 @@ 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.Card
import androidx.compose.material.Checkbox
import androidx.compose.material.Chip
import androidx.compose.material.ContentAlpha
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalContentAlpha
@ -56,10 +52,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.listSaver
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.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
@ -75,7 +71,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
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.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@ -103,9 +99,11 @@ import com.geeksville.mesh.model.getChannelUrl
import com.geeksville.mesh.model.qrCode
import com.geeksville.mesh.model.toChannelSet
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.config.ChannelCard
import com.geeksville.mesh.ui.components.config.ChannelSelection
import com.geeksville.mesh.ui.components.config.EditChannelDialog
import com.geeksville.mesh.ui.components.dragContainer
import com.geeksville.mesh.ui.components.dragDropItemsIndexed
@ -178,12 +176,6 @@ fun ChannelScreen(
)
) { mutableStateListOf(elements = Array(size = 8, init = { true })) }
val selectedChannelSet = channelSet.copy {
val result = settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }
settings.clear()
settings.addAll(result)
}
val channelUrl = channelSet.getChannelUrl()
val modemPresetName = Channel(loraConfig = channelSet.loraConfig).name
@ -346,25 +338,14 @@ fun ChannelScreen(
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
) {
if (!showChannelEditor) {
itemsIndexed(channelSet.settingsList) { index, channel ->
ChannelSelection(
index = index,
title = channel.name.ifEmpty { modemPresetName },
enabled = enabled,
isSelected = channelSelections[index],
onSelected = {
if (it || selectedChannelSet.settingsCount > 1) {
channelSelections[index] = it
}
}
)
}
item {
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = { showChannelEditor = true },
ChannelListView(
enabled = enabled,
) { Text(text = stringResource(R.string.edit)) }
channelSet = channelSet,
modemPresetName = modemPresetName,
channelSelections = channelSelections,
onClick = { showChannelEditor = true }
)
}
} else {
dragDropItemsIndexed(
@ -398,16 +379,6 @@ fun ChannelScreen(
}
}
if (!isEditing) item {
QRCodeImage(
enabled = enabled,
channelSet = selectedChannelSet,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
)
}
item {
var valueState by remember(channelUrl) { mutableStateOf(channelUrl) }
val isError = valueState != channelUrl
@ -513,7 +484,7 @@ fun ChannelScreen(
}
@Composable
private fun QRCodeImage(
private fun QrCodeImage(
enabled: Boolean,
channelSet: AppOnlyProtos.ChannelSet,
modifier: Modifier = Modifier,
@ -528,42 +499,51 @@ private fun QRCodeImage(
// colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
)
/**
* Enables the user to select what channels are used for QR generation.
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun ChannelSelection(
index: Int,
title: String,
private fun ChannelListView(
enabled: Boolean,
isSelected: Boolean,
onSelected: (Boolean) -> Unit
channelSet: AppOnlyProtos.ChannelSet,
modemPresetName: String,
channelSelections: SnapshotStateList<Boolean>,
onClick: () -> Unit = {},
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
elevation = 4.dp
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp)
) {
Chip(onClick = { }, enabled = enabled) { Text("$index") }
Text(
text = title,
style = MaterialTheme.typography.body1,
color = if (!enabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) else Color.Unspecified,
modifier = Modifier.weight(1f)
)
Checkbox(
enabled = enabled,
checked = isSelected,
onCheckedChange = onSelected,
)
}
val selectedChannelSet = channelSet.copy {
val result = settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }
settings.clear()
settings.addAll(result)
}
AdaptiveTwoPane(
first = {
channelSet.settingsList.forEachIndexed { index, channel ->
ChannelSelection(
index = index,
title = channel.name.ifEmpty { modemPresetName },
enabled = enabled,
isSelected = channelSelections[index],
onSelected = {
if (it || selectedChannelSet.settingsCount > 1) {
channelSelections[index] = it
}
},
)
}
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = onClick,
enabled = enabled,
) { Text(text = stringResource(R.string.edit)) }
},
second = {
QrCodeImage(
enabled = enabled,
channelSet = selectedChannelSet,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
)
},
)
}
/**
@ -718,8 +698,16 @@ fun ScannedQrCodeDialog(
}
}
@Preview(showBackground = true)
@PreviewScreenSizes
@Composable
private fun ChannelScreenPreview() {
// ChannelScreen()
ChannelListView(
enabled = true,
channelSet = channelSet {
settings.add(Channel.default.settings)
loraConfig = Channel.default.loraConfig
},
modemPresetName = Channel.default.name,
channelSelections = listOf(true).toMutableStateList(),
)
}

View file

@ -0,0 +1,32 @@
package com.geeksville.mesh.ui.components
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun AdaptiveTwoPane(
first: @Composable ColumnScope.() -> Unit,
second: @Composable ColumnScope.() -> Unit,
) = BoxWithConstraints {
val compactWidth = maxWidth < 600.dp
Row {
Column(modifier = Modifier.weight(1f)) {
first()
if (compactWidth) {
second()
}
}
if (!compactWidth) {
Column(modifier = Modifier.weight(1f)) {
second()
}
}
}
}

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -17,7 +18,9 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Card
import androidx.compose.material.Checkbox
import androidx.compose.material.Chip
import androidx.compose.material.ContentAlpha
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
@ -35,6 +38,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
@ -52,6 +56,41 @@ import com.geeksville.mesh.ui.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.components.rememberDragDropState
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun ChannelItem(
index: Int,
title: String,
enabled: Boolean,
onClick: () -> Unit = {},
elevation: Dp = 4.dp,
content: @Composable RowScope.() -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp)
.clickable(enabled = enabled) { onClick() },
elevation = elevation,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp)
) {
val textColor = if (enabled) Color.Unspecified
else MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
Chip(onClick = onClick) { Text("$index") }
Text(
text = title,
modifier = Modifier.weight(1f),
color = textColor,
style = MaterialTheme.typography.body1,
)
content()
}
}
}
@Composable
fun ChannelCard(
index: Int,
@ -60,35 +99,42 @@ fun ChannelCard(
onEditClick: () -> Unit,
onDeleteClick: () -> Unit,
elevation: Dp = 4.dp,
) = ChannelItem(
index = index,
title = title,
enabled = enabled,
onClick = onEditClick,
elevation = elevation,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp)
.clickable(enabled = enabled) { onEditClick() },
elevation = elevation,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 4.dp)
) {
Chip(onClick = onEditClick) { Text("$index") }
Text(
text = title,
style = MaterialTheme.typography.body1,
modifier = Modifier.weight(1f)
)
IconButton(onClick = { onDeleteClick() }) {
Icon(
Icons.TwoTone.Close,
stringResource(R.string.delete),
modifier = Modifier.wrapContentSize(),
)
}
}
IconButton(onClick = { onDeleteClick() }) {
Icon(
imageVector = Icons.TwoTone.Close,
contentDescription = stringResource(R.string.delete),
modifier = Modifier.wrapContentSize(),
)
}
}
@Composable
fun ChannelSelection(
index: Int,
title: String,
enabled: Boolean,
isSelected: Boolean,
onSelected: (Boolean) -> Unit
) = ChannelItem(
index = index,
title = title,
enabled = enabled,
onClick = {},
) {
Checkbox(
enabled = enabled,
checked = isSelected,
onCheckedChange = onSelected,
)
}
@Composable
fun ChannelSettingsItemList(
settingsList: List<ChannelSettings>,

View file

@ -175,7 +175,7 @@ complexity:
ignoreDeprecated: false
ignorePrivate: false
ignoreOverridden: false
ignoreAnnotated: [ 'Preview', 'PreviewLightDark' ]
ignoreAnnotated: [ 'Preview', 'PreviewLightDark', 'PreviewScreenSizes' ]
coroutines:
active: true
@ -739,7 +739,7 @@ style:
UnusedPrivateMember:
active: true
allowedNames: ''
ignoreAnnotated: [ 'Preview', 'PreviewLightDark' ]
ignoreAnnotated: [ 'Preview', 'PreviewLightDark', 'PreviewScreenSizes' ]
UnusedPrivateProperty:
active: true
allowedNames: '_|ignored|expected|serialVersionUID'