kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor: channel qr clean (#1983)
rodzic
7dc2147169
commit
5edc2a8d57
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Meshtastic LLC
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.geeksville.mesh.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.navigation
|
||||||
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
|
import com.geeksville.mesh.ui.sharing.ChannelScreen
|
||||||
|
import com.geeksville.mesh.ui.radioconfig.components.ChannelConfigScreen
|
||||||
|
import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation graph for for the top level ChannelScreen - [Route.Channels].
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.channelsGraph(navController: NavHostController, uiViewModel: UIViewModel) {
|
||||||
|
navigation<Graph.ChannelsGraph>(
|
||||||
|
startDestination = Route.Channels,
|
||||||
|
) {
|
||||||
|
composable<Route.Channels> { backStackEntry ->
|
||||||
|
val parentEntry = remember(backStackEntry) {
|
||||||
|
navController.getBackStackEntry<Graph.ChannelsGraph>()
|
||||||
|
}
|
||||||
|
ChannelScreen(
|
||||||
|
viewModel = uiViewModel,
|
||||||
|
radioConfigViewModel = hiltViewModel(parentEntry),
|
||||||
|
onNavigate = { route -> navController.navigate(route) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
configRoutes(navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NavGraphBuilder.configRoutes(
|
||||||
|
navController: NavHostController,
|
||||||
|
) {
|
||||||
|
ConfigRoute.entries.forEach { configRoute ->
|
||||||
|
composable(configRoute.route::class) { backStackEntry ->
|
||||||
|
val parentEntry = remember(backStackEntry) {
|
||||||
|
navController.getBackStackEntry<Graph.ChannelsGraph>()
|
||||||
|
}
|
||||||
|
when (configRoute) {
|
||||||
|
ConfigRoute.CHANNELS -> ChannelConfigScreen(hiltViewModel(parentEntry))
|
||||||
|
ConfigRoute.LORA -> LoRaConfigScreen(hiltViewModel(parentEntry))
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,7 +39,6 @@ import com.geeksville.mesh.ui.map.MapView
|
||||||
import com.geeksville.mesh.ui.message.MessageScreen
|
import com.geeksville.mesh.ui.message.MessageScreen
|
||||||
import com.geeksville.mesh.ui.message.QuickChatScreen
|
import com.geeksville.mesh.ui.message.QuickChatScreen
|
||||||
import com.geeksville.mesh.ui.node.NodeScreen
|
import com.geeksville.mesh.ui.node.NodeScreen
|
||||||
import com.geeksville.mesh.ui.sharing.ChannelScreen
|
|
||||||
import com.geeksville.mesh.ui.sharing.ShareScreen
|
import com.geeksville.mesh.ui.sharing.ShareScreen
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@ -54,6 +53,9 @@ const val DEEP_LINK_BASE_URI = "meshtastic://meshtastic"
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface Graph : Route {
|
sealed interface Graph : Route {
|
||||||
|
@Serializable
|
||||||
|
data class ChannelsGraph(val destNum: Int?)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NodeDetailGraph(val destNum: Int) : Graph
|
data class NodeDetailGraph(val destNum: Int) : Graph
|
||||||
|
|
||||||
|
@ -241,9 +243,9 @@ fun NavGraph(
|
||||||
composable<Route.Map> {
|
composable<Route.Map> {
|
||||||
MapView(uIViewModel)
|
MapView(uIViewModel)
|
||||||
}
|
}
|
||||||
composable<Route.Channels> {
|
|
||||||
ChannelScreen(uIViewModel)
|
channelsGraph(navController, uIViewModel)
|
||||||
}
|
|
||||||
composable<Route.Connections>(
|
composable<Route.Connections>(
|
||||||
deepLinks = listOf(
|
deepLinks = listOf(
|
||||||
navDeepLink {
|
navDeepLink {
|
||||||
|
|
|
@ -77,6 +77,11 @@ import com.geeksville.mesh.ui.radioconfig.components.StoreForwardConfigScreen
|
||||||
import com.geeksville.mesh.ui.radioconfig.components.TelemetryConfigScreen
|
import com.geeksville.mesh.ui.radioconfig.components.TelemetryConfigScreen
|
||||||
import com.geeksville.mesh.ui.radioconfig.components.UserConfigScreen
|
import com.geeksville.mesh.ui.radioconfig.components.UserConfigScreen
|
||||||
|
|
||||||
|
fun getNavRouteFrom(routeName: String): Route? {
|
||||||
|
return ConfigRoute.entries.find { it.name == routeName }?.route
|
||||||
|
?: ModuleRoute.entries.find { it.name == routeName }?.route
|
||||||
|
}
|
||||||
|
|
||||||
fun NavGraphBuilder.radioConfigGraph(navController: NavHostController, uiViewModel: UIViewModel) {
|
fun NavGraphBuilder.radioConfigGraph(navController: NavHostController, uiViewModel: UIViewModel) {
|
||||||
navigation<Graph.RadioConfigGraph>(
|
navigation<Graph.RadioConfigGraph>(
|
||||||
startDestination = Route.RadioConfig(),
|
startDestination = Route.RadioConfig(),
|
||||||
|
|
|
@ -68,16 +68,12 @@ import com.geeksville.mesh.navigation.AdminRoute
|
||||||
import com.geeksville.mesh.navigation.ConfigRoute
|
import com.geeksville.mesh.navigation.ConfigRoute
|
||||||
import com.geeksville.mesh.navigation.ModuleRoute
|
import com.geeksville.mesh.navigation.ModuleRoute
|
||||||
import com.geeksville.mesh.navigation.Route
|
import com.geeksville.mesh.navigation.Route
|
||||||
|
import com.geeksville.mesh.navigation.getNavRouteFrom
|
||||||
import com.geeksville.mesh.ui.common.components.PreferenceCategory
|
import com.geeksville.mesh.ui.common.components.PreferenceCategory
|
||||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||||
import com.geeksville.mesh.ui.radioconfig.components.EditDeviceProfileDialog
|
import com.geeksville.mesh.ui.radioconfig.components.EditDeviceProfileDialog
|
||||||
import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog
|
import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog
|
||||||
|
|
||||||
private fun getNavRouteFrom(routeName: String): Route? {
|
|
||||||
return ConfigRoute.entries.find { it.name == routeName }?.route
|
|
||||||
?: ModuleRoute.entries.find { it.name == routeName }?.route
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
@Composable
|
@Composable
|
||||||
fun RadioConfigScreen(
|
fun RadioConfigScreen(
|
||||||
|
|
|
@ -23,14 +23,22 @@ import android.os.RemoteException
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
import androidx.compose.material.icons.twotone.Check
|
import androidx.compose.material.icons.twotone.Check
|
||||||
import androidx.compose.material.icons.twotone.Close
|
import androidx.compose.material.icons.twotone.Close
|
||||||
import androidx.compose.material.icons.twotone.ContentCopy
|
import androidx.compose.material.icons.twotone.ContentCopy
|
||||||
|
@ -56,6 +64,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
|
@ -64,13 +73,14 @@ import androidx.compose.ui.platform.ClipEntry
|
||||||
import androidx.compose.ui.platform.LocalClipboard
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
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.tooling.preview.PreviewScreenSizes
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
@ -85,24 +95,21 @@ import com.geeksville.mesh.android.GeeksvilleApplication
|
||||||
import com.geeksville.mesh.android.getCameraPermissions
|
import com.geeksville.mesh.android.getCameraPermissions
|
||||||
import com.geeksville.mesh.android.hasCameraPermission
|
import com.geeksville.mesh.android.hasCameraPermission
|
||||||
import com.geeksville.mesh.channelSet
|
import com.geeksville.mesh.channelSet
|
||||||
import com.geeksville.mesh.channelSettings
|
|
||||||
import com.geeksville.mesh.copy
|
import com.geeksville.mesh.copy
|
||||||
import com.geeksville.mesh.model.Channel
|
import com.geeksville.mesh.model.Channel
|
||||||
import com.geeksville.mesh.model.ChannelOption
|
|
||||||
import com.geeksville.mesh.model.UIViewModel
|
import com.geeksville.mesh.model.UIViewModel
|
||||||
import com.geeksville.mesh.model.getChannelUrl
|
import com.geeksville.mesh.model.getChannelUrl
|
||||||
import com.geeksville.mesh.model.qrCode
|
import com.geeksville.mesh.model.qrCode
|
||||||
import com.geeksville.mesh.model.toChannelSet
|
import com.geeksville.mesh.model.toChannelSet
|
||||||
|
import com.geeksville.mesh.navigation.ConfigRoute
|
||||||
|
import com.geeksville.mesh.navigation.Route
|
||||||
|
import com.geeksville.mesh.navigation.getNavRouteFrom
|
||||||
import com.geeksville.mesh.service.MeshService
|
import com.geeksville.mesh.service.MeshService
|
||||||
|
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
|
||||||
import com.geeksville.mesh.ui.common.components.AdaptiveTwoPane
|
import com.geeksville.mesh.ui.common.components.AdaptiveTwoPane
|
||||||
import com.geeksville.mesh.ui.common.components.DropDownPreference
|
|
||||||
import com.geeksville.mesh.ui.common.components.PreferenceFooter
|
import com.geeksville.mesh.ui.common.components.PreferenceFooter
|
||||||
import com.geeksville.mesh.ui.common.components.dragContainer
|
|
||||||
import com.geeksville.mesh.ui.common.components.dragDropItemsIndexed
|
|
||||||
import com.geeksville.mesh.ui.common.components.rememberDragDropState
|
|
||||||
import com.geeksville.mesh.ui.radioconfig.components.ChannelCard
|
|
||||||
import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection
|
import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection
|
||||||
import com.geeksville.mesh.ui.radioconfig.components.EditChannelDialog
|
import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -111,20 +118,44 @@ import kotlinx.coroutines.launch
|
||||||
@Composable
|
@Composable
|
||||||
fun ChannelScreen(
|
fun ChannelScreen(
|
||||||
viewModel: UIViewModel = hiltViewModel(),
|
viewModel: UIViewModel = hiltViewModel(),
|
||||||
|
radioConfigViewModel: RadioConfigViewModel = hiltViewModel(),
|
||||||
|
onNavigate: (Route) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||||
|
val radioConfigState by radioConfigViewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val enabled = connectionState == MeshService.ConnectionState.CONNECTED && !viewModel.isManaged
|
val enabled = connectionState == MeshService.ConnectionState.CONNECTED && !viewModel.isManaged
|
||||||
|
|
||||||
val channels by viewModel.channels.collectAsStateWithLifecycle()
|
val channels by viewModel.channels.collectAsStateWithLifecycle()
|
||||||
var channelSet by remember(channels) { mutableStateOf(channels) }
|
var channelSet by remember(channels) { mutableStateOf(channels) }
|
||||||
var showChannelEditor by rememberSaveable { mutableStateOf(false) }
|
val modemPresetName by remember(channels) {
|
||||||
var showSendDialog by remember { mutableStateOf(false) }
|
mutableStateOf(Channel(loraConfig = channels.loraConfig).name)
|
||||||
|
}
|
||||||
|
|
||||||
var showResetDialog by remember { mutableStateOf(false) }
|
var showResetDialog by remember { mutableStateOf(false) }
|
||||||
var showScanDialog by remember { mutableStateOf(false) }
|
var showScanDialog by remember { mutableStateOf(false) }
|
||||||
val isEditing = channelSet != channels || showChannelEditor
|
|
||||||
|
/* Animate waiting for the channel configurations */
|
||||||
|
var isWaiting by remember { mutableStateOf(false) }
|
||||||
|
if (isWaiting) {
|
||||||
|
PacketResponseStateDialog(
|
||||||
|
state = radioConfigState.responseState,
|
||||||
|
onDismiss = {
|
||||||
|
isWaiting = false
|
||||||
|
radioConfigViewModel.clearPacketResponse()
|
||||||
|
},
|
||||||
|
onComplete = {
|
||||||
|
getNavRouteFrom(radioConfigState.route)?.let { route ->
|
||||||
|
isWaiting = false
|
||||||
|
radioConfigViewModel.clearPacketResponse()
|
||||||
|
onNavigate(route)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/* Holds selections made by the user for QR generation. */
|
/* Holds selections made by the user for QR generation. */
|
||||||
val channelSelections = rememberSaveable(
|
val channelSelections = rememberSaveable(
|
||||||
|
@ -139,7 +170,6 @@ fun ChannelScreen(
|
||||||
settings.clear()
|
settings.clear()
|
||||||
settings.addAll(result)
|
settings.addAll(result)
|
||||||
}
|
}
|
||||||
val modemPresetName = Channel(loraConfig = channelSet.loraConfig).name
|
|
||||||
|
|
||||||
val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
|
val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||||
if (result.contents != null) {
|
if (result.contents != null) {
|
||||||
|
@ -147,19 +177,6 @@ fun ChannelScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSettingsList(update: MutableList<ChannelProtos.ChannelSettings>.() -> Unit) {
|
|
||||||
try {
|
|
||||||
val list = channelSet.settingsList.toMutableList()
|
|
||||||
list.update()
|
|
||||||
channelSet = channelSet.copy {
|
|
||||||
settings.clear()
|
|
||||||
settings.addAll(list)
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
errormsg("Error updating ChannelSettings list:", ex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun zxingScan() {
|
fun zxingScan() {
|
||||||
debug("Starting zxing QR code scanner")
|
debug("Starting zxing QR code scanner")
|
||||||
val zxingScan = ScanOptions()
|
val zxingScan = ScanOptions()
|
||||||
|
@ -209,8 +226,6 @@ fun ChannelScreen(
|
||||||
|
|
||||||
// Tell the user to try again
|
// Tell the user to try again
|
||||||
viewModel.showSnackbar(R.string.cant_change_no_radio)
|
viewModel.showSnackbar(R.string.cant_change_no_radio)
|
||||||
} finally {
|
|
||||||
showChannelEditor = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,146 +270,51 @@ fun ChannelScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showSendDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = {
|
|
||||||
showSendDialog = false
|
|
||||||
showChannelEditor = false
|
|
||||||
channelSet = channels
|
|
||||||
},
|
|
||||||
title = { Text(text = stringResource(id = R.string.change_channel)) },
|
|
||||||
text = { Text(text = stringResource(id = R.string.are_you_sure_channel)) },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = {
|
|
||||||
installSettings(channelSet)
|
|
||||||
showSendDialog = false
|
|
||||||
}) { Text(text = stringResource(id = R.string.accept)) }
|
|
||||||
installSettings(channelSet)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showEditChannelDialog: Int? by remember { mutableStateOf(null) }
|
|
||||||
|
|
||||||
if (showEditChannelDialog != null) {
|
|
||||||
val index = showEditChannelDialog ?: return
|
|
||||||
EditChannelDialog(
|
|
||||||
channelSettings = with(channelSet) {
|
|
||||||
if (settingsCount > index) getSettings(index) else channelSettings { }
|
|
||||||
},
|
|
||||||
modemPresetName = modemPresetName,
|
|
||||||
onAddClick = {
|
|
||||||
with(channelSet) {
|
|
||||||
if (settingsCount > index) {
|
|
||||||
channelSet = copy { settings[index] = it }
|
|
||||||
} else {
|
|
||||||
channelSet = copy { settings.add(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showEditChannelDialog = null
|
|
||||||
},
|
|
||||||
onDismissRequest = { showEditChannelDialog = null }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
|
|
||||||
updateSettingsList { add(toIndex, removeAt(fromIndex)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.dragContainer(
|
|
||||||
dragDropState = dragDropState,
|
|
||||||
haptics = LocalHapticFeedback.current,
|
|
||||||
),
|
|
||||||
state = listState,
|
state = listState,
|
||||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
|
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
|
||||||
) {
|
) {
|
||||||
if (!showChannelEditor) {
|
|
||||||
item {
|
|
||||||
ChannelListView(
|
|
||||||
enabled = enabled,
|
|
||||||
channelSet = channelSet,
|
|
||||||
modemPresetName = modemPresetName,
|
|
||||||
channelSelections = channelSelections,
|
|
||||||
onClick = { showChannelEditor = true }
|
|
||||||
)
|
|
||||||
EditChannelUrl(
|
|
||||||
enabled = enabled,
|
|
||||||
channelUrl = selectedChannelSet.getChannelUrl(),
|
|
||||||
onConfirm = viewModel::requestChannelUrl
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dragDropItemsIndexed(
|
|
||||||
items = channelSet.settingsList,
|
|
||||||
dragDropState = dragDropState,
|
|
||||||
) { index, channel, isDragging ->
|
|
||||||
ChannelCard(
|
|
||||||
index = index,
|
|
||||||
title = channel.name.ifEmpty { modemPresetName },
|
|
||||||
enabled = enabled,
|
|
||||||
onEditClick = { showEditChannelDialog = index },
|
|
||||||
onDeleteClick = { updateSettingsList { removeAt(index) } }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
OutlinedButton(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
onClick = {
|
|
||||||
channelSet = channelSet.copy {
|
|
||||||
settings.add(channelSettings { psk = Channel.default.settings.psk })
|
|
||||||
}
|
|
||||||
showEditChannelDialog = channelSet.settingsList.lastIndex
|
|
||||||
},
|
|
||||||
enabled = enabled && viewModel.maxChannels > channelSet.settingsCount,
|
|
||||||
) { Text(text = stringResource(R.string.add)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
item {
|
||||||
DropDownPreference(
|
ChannelListView(
|
||||||
title = stringResource(id = R.string.channel_options),
|
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
items = ChannelOption.entries
|
channelSet = channelSet,
|
||||||
.map { it.modemPreset to stringResource(it.configRes) },
|
modemPresetName = modemPresetName,
|
||||||
selectedItem = channelSet.loraConfig.modemPreset,
|
channelSelections = channelSelections,
|
||||||
onItemSelected = {
|
onClick = {
|
||||||
val lora = channelSet.loraConfig.copy { modemPreset = it }
|
isWaiting = true
|
||||||
channelSet = channelSet.copy { loraConfig = lora }
|
radioConfigViewModel.setResponseStateLoading(ConfigRoute.CHANNELS)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
EditChannelUrl(
|
||||||
|
enabled = enabled,
|
||||||
|
channelUrl = selectedChannelSet.getChannelUrl(),
|
||||||
|
onConfirm = viewModel::requestChannelUrl
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
if (isEditing) {
|
ModemPresetInfo(
|
||||||
PreferenceFooter(
|
modemPresetName = modemPresetName,
|
||||||
enabled = enabled,
|
onClick = {
|
||||||
onCancelClicked = {
|
isWaiting = true
|
||||||
focusManager.clearFocus()
|
radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA)
|
||||||
showChannelEditor = false
|
}
|
||||||
channelSet = channels
|
)
|
||||||
},
|
}
|
||||||
onSaveClicked = {
|
item {
|
||||||
focusManager.clearFocus()
|
PreferenceFooter(
|
||||||
showSendDialog = true
|
enabled = enabled,
|
||||||
}
|
negativeText = R.string.reset,
|
||||||
)
|
onNegativeClicked = {
|
||||||
} else {
|
focusManager.clearFocus()
|
||||||
PreferenceFooter(
|
showResetDialog = true
|
||||||
enabled = enabled,
|
},
|
||||||
negativeText = R.string.reset,
|
positiveText = R.string.scan,
|
||||||
onNegativeClicked = {
|
onPositiveClicked = {
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
showResetDialog = true
|
if (context.hasCameraPermission()) zxingScan() else showScanDialog = true
|
||||||
},
|
}
|
||||||
positiveText = R.string.scan,
|
)
|
||||||
onPositiveClicked = {
|
|
||||||
focusManager.clearFocus()
|
|
||||||
if (context.hasCameraPermission()) zxingScan() else showScanDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -433,6 +353,7 @@ private fun EditChannelUrl(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
label = { Text(stringResource(R.string.url)) },
|
label = { Text(stringResource(R.string.url)) },
|
||||||
isError = isError,
|
isError = isError,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
val label = stringResource(R.string.url)
|
val label = stringResource(R.string.url)
|
||||||
val isUrlEqual = valueState == channelUrl
|
val isUrlEqual = valueState == channelUrl
|
||||||
|
@ -560,6 +481,55 @@ private fun ChannelListView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ModemPresetInfo(
|
||||||
|
modemPresetName: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 12.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.onBackground,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.modem_preset),
|
||||||
|
fontSize = 16.sp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = modemPresetName,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ChevronRight,
|
||||||
|
contentDescription = stringResource(R.string.navigate_into_label),
|
||||||
|
modifier = Modifier.padding(end = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun ModemPresetInfoPreview() {
|
||||||
|
ModemPresetInfo(
|
||||||
|
modemPresetName = "Long Fast",
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewScreenSizes
|
@PreviewScreenSizes
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChannelScreenPreview() {
|
private fun ChannelScreenPreview() {
|
||||||
|
|
|
@ -481,7 +481,7 @@
|
||||||
<string name="use_i2s_as_buzzer">Use I2S as buzzer</string>
|
<string name="use_i2s_as_buzzer">Use I2S as buzzer</string>
|
||||||
<string name="lora_config">LoRa Config</string>
|
<string name="lora_config">LoRa Config</string>
|
||||||
<string name="use_modem_preset">Use modem preset</string>
|
<string name="use_modem_preset">Use modem preset</string>
|
||||||
<string name="modem_preset">Modem preset</string>
|
<string name="modem_preset">Modem Preset</string>
|
||||||
<string name="bandwidth">Bandwidth</string>
|
<string name="bandwidth">Bandwidth</string>
|
||||||
<string name="spread_factor">Spread factor</string>
|
<string name="spread_factor">Spread factor</string>
|
||||||
<string name="coding_rate">Coding rate</string>
|
<string name="coding_rate">Coding rate</string>
|
||||||
|
@ -655,6 +655,7 @@
|
||||||
<string name="disk_free">Disk Free</string>
|
<string name="disk_free">Disk Free</string>
|
||||||
<string name="load">Load</string>
|
<string name="load">Load</string>
|
||||||
<string name="user_string">User String</string>
|
<string name="user_string">User String</string>
|
||||||
|
<string name="navigate_into_label">Navigate Into</string>
|
||||||
<string name="connections">Connections</string>
|
<string name="connections">Connections</string>
|
||||||
<string name="map">Map</string>
|
<string name="map">Map</string>
|
||||||
<string name="contacts">Contacts</string>
|
<string name="contacts">Contacts</string>
|
||||||
|
|
Ładowanie…
Reference in New Issue