mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: add configs import/export (#628)
This commit is contained in:
parent
9dc1a45fe6
commit
9e78e516da
3 changed files with 293 additions and 0 deletions
|
|
@ -1,9 +1,14 @@
|
|||
package com.geeksville.mesh.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
|
@ -57,6 +62,7 @@ import com.geeksville.mesh.android.Logging
|
|||
import com.geeksville.mesh.channel
|
||||
import com.geeksville.mesh.channelSettings
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.deviceProfile
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.moduleConfig
|
||||
import com.geeksville.mesh.service.MeshService
|
||||
|
|
@ -68,6 +74,7 @@ import com.geeksville.mesh.ui.components.config.CannedMessageConfigItemList
|
|||
import com.geeksville.mesh.ui.components.config.ChannelSettingsItemList
|
||||
import com.geeksville.mesh.ui.components.config.DeviceConfigItemList
|
||||
import com.geeksville.mesh.ui.components.config.DisplayConfigItemList
|
||||
import com.geeksville.mesh.ui.components.config.EditDeviceProfileDialog
|
||||
import com.geeksville.mesh.ui.components.config.ExternalNotificationConfigItemList
|
||||
import com.geeksville.mesh.ui.components.config.LoRaConfigItemList
|
||||
import com.geeksville.mesh.ui.components.config.MQTTConfigItemList
|
||||
|
|
@ -148,11 +155,76 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) {
|
|||
var cannedMessageMessages by remember { mutableStateOf("") }
|
||||
|
||||
val configResponse by viewModel.packetResponse.collectAsStateWithLifecycle()
|
||||
val deviceProfile by viewModel.deviceProfile.collectAsStateWithLifecycle()
|
||||
var isWaiting by remember { mutableStateOf(false) }
|
||||
|
||||
val importConfigLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { file_uri -> viewModel.importProfile(file_uri) }
|
||||
}
|
||||
}
|
||||
|
||||
val exportConfigLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { file_uri -> viewModel.exportProfile(file_uri) }
|
||||
}
|
||||
}
|
||||
|
||||
var showEditDeviceProfileDialog by remember { mutableStateOf(false) }
|
||||
if (showEditDeviceProfileDialog) EditDeviceProfileDialog(
|
||||
title = "Export configuration",
|
||||
deviceProfile = with(viewModel) {
|
||||
deviceProfile {
|
||||
ourNodeInfo.value?.user?.let {
|
||||
longName = it.longName
|
||||
shortName = it.shortName
|
||||
}
|
||||
channelUrl = channels.value.getChannelUrl().toString()
|
||||
config = localConfig.value
|
||||
this.moduleConfig = module
|
||||
}
|
||||
},
|
||||
onAddClick = {
|
||||
isWaiting = false
|
||||
viewModel.setDeviceProfile(it)
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/*"
|
||||
putExtra(Intent.EXTRA_TITLE, "${destNum.toUInt()}.cfg")
|
||||
}
|
||||
exportConfigLauncher.launch(intent)
|
||||
showEditDeviceProfileDialog = false
|
||||
},
|
||||
onDismissRequest = {
|
||||
isWaiting = false
|
||||
showEditDeviceProfileDialog = false
|
||||
viewModel.setDeviceProfile(null)
|
||||
}
|
||||
)
|
||||
|
||||
if (isWaiting && deviceProfile != null) {
|
||||
EditDeviceProfileDialog(
|
||||
title = "Import configuration",
|
||||
deviceProfile = deviceProfile ?: return,
|
||||
onAddClick = {
|
||||
isWaiting = false
|
||||
viewModel.installProfile(it)
|
||||
},
|
||||
onDismissRequest = {
|
||||
isWaiting = false
|
||||
viewModel.setDeviceProfile(null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (isWaiting) LaunchedEffect(configResponse) {
|
||||
val data = configResponse?.meshPacket?.decoded
|
||||
if (data?.portnumValue == Portnums.PortNum.ADMIN_APP_VALUE) {
|
||||
viewModel.clearPacketResponse()
|
||||
val parsed = AdminProtos.AdminMessage.parseFrom(data.payload)
|
||||
when (parsed.payloadVariantCase) {
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> {
|
||||
|
|
@ -216,6 +288,7 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) {
|
|||
composable("home") {
|
||||
RadioSettingsScreen(
|
||||
enabled = connected && !isWaiting,
|
||||
isLocal = destNum == viewModel.myNodeNum,
|
||||
headerText = node.user?.longName ?: stringResource(R.string.unknown_username),
|
||||
onRouteClick = { configType ->
|
||||
isWaiting = true
|
||||
|
|
@ -226,6 +299,15 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) {
|
|||
channelList.clear()
|
||||
viewModel.getChannel(destNum, 0)
|
||||
}
|
||||
"IMPORT" -> {
|
||||
viewModel.setDeviceProfile(null)
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "application/*"
|
||||
}
|
||||
importConfigLauncher.launch(intent)
|
||||
}
|
||||
"EXPORT" -> { showEditDeviceProfileDialog = true }
|
||||
is ConfigType -> {
|
||||
viewModel.getConfig(destNum, configType.number)
|
||||
}
|
||||
|
|
@ -544,10 +626,16 @@ fun NavCard(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavCard(@StringRes title: Int, enabled: Boolean, onClick: () -> Unit) {
|
||||
NavCard(title = stringResource(title), enabled = enabled, onClick = onClick)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun RadioSettingsScreen(
|
||||
enabled: Boolean = true,
|
||||
isLocal: Boolean = true,
|
||||
headerText: String = "longName",
|
||||
onRouteClick: (Any) -> Unit = {},
|
||||
) {
|
||||
|
|
@ -567,6 +655,12 @@ fun RadioSettingsScreen(
|
|||
items(ModuleDest.values()) { modules ->
|
||||
NavCard(modules.title, enabled = enabled) { onRouteClick(modules.config) }
|
||||
}
|
||||
|
||||
if (isLocal) {
|
||||
item { PreferenceCategory("Import / Export") }
|
||||
item { NavCard("Import configuration", enabled = enabled) { onRouteClick("IMPORT") } }
|
||||
item { NavCard("Export configuration", enabled = enabled) { onRouteClick("EXPORT") } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
package com.geeksville.mesh.ui.components.config
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ClientOnlyProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.deviceProfile
|
||||
import com.geeksville.mesh.ui.components.SwitchPreference
|
||||
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
|
||||
|
||||
@Composable
|
||||
fun EditDeviceProfileDialog(
|
||||
title: String,
|
||||
deviceProfile: ClientOnlyProtos.DeviceProfile,
|
||||
onAddClick: (ClientOnlyProtos.DeviceProfile) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var longNameInput by remember { mutableStateOf(deviceProfile.hasLongName()) }
|
||||
var shortNameInput by remember { mutableStateOf(deviceProfile.hasShortName()) }
|
||||
var channelUrlInput by remember { mutableStateOf(deviceProfile.hasChannelUrl()) }
|
||||
var configInput by remember { mutableStateOf(deviceProfile.hasConfig()) }
|
||||
var moduleConfigInput by remember { mutableStateOf(deviceProfile.hasModuleConfig()) }
|
||||
|
||||
AlertDialog(
|
||||
title = { Text(title) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
text = {
|
||||
AppCompatTheme {
|
||||
Column(modifier.fillMaxWidth()) {
|
||||
SwitchPreference(title = "longName",
|
||||
checked = longNameInput,
|
||||
enabled = deviceProfile.hasLongName(),
|
||||
onCheckedChange = { longNameInput = it }
|
||||
)
|
||||
SwitchPreference(title = "shortName",
|
||||
checked = shortNameInput,
|
||||
enabled = deviceProfile.hasShortName(),
|
||||
onCheckedChange = { shortNameInput = it }
|
||||
)
|
||||
SwitchPreference(title = "channelUrl",
|
||||
checked = channelUrlInput,
|
||||
enabled = deviceProfile.hasChannelUrl(),
|
||||
onCheckedChange = { channelUrlInput = it }
|
||||
)
|
||||
SwitchPreference(title = "config",
|
||||
checked = configInput,
|
||||
enabled = deviceProfile.hasConfig(),
|
||||
onCheckedChange = { configInput = it }
|
||||
)
|
||||
SwitchPreference(title = "moduleConfig",
|
||||
checked = moduleConfigInput,
|
||||
enabled = deviceProfile.hasModuleConfig(),
|
||||
onCheckedChange = { moduleConfigInput = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
buttons = {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp)
|
||||
.weight(1f),
|
||||
onClick = onDismissRequest
|
||||
) { Text(stringResource(R.string.cancel)) }
|
||||
Button(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 24.dp)
|
||||
.weight(1f),
|
||||
onClick = {
|
||||
onAddClick(deviceProfile {
|
||||
if (longNameInput) longName = deviceProfile.longName
|
||||
if (shortNameInput) shortName = deviceProfile.shortName
|
||||
if (channelUrlInput) channelUrl = deviceProfile.channelUrl
|
||||
if (configInput) config = deviceProfile.config
|
||||
if (moduleConfigInput) moduleConfig = deviceProfile.moduleConfig
|
||||
})
|
||||
},
|
||||
) { Text(stringResource(R.string.save)) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun EditDeviceProfileDialogPreview() {
|
||||
EditDeviceProfileDialog(
|
||||
title = "Export configuration",
|
||||
deviceProfile = deviceProfile { },
|
||||
onAddClick = { },
|
||||
onDismissRequest = { },
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue