mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: add adaptive two-pane layout to ChannelScreen
This commit is contained in:
parent
df6b0e1949
commit
a65cc7699e
4 changed files with 167 additions and 101 deletions
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue