mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Spruce up LoRaConfigScreen (#3224)
This commit is contained in:
parent
8c16052229
commit
3951ebb375
12 changed files with 332 additions and 239 deletions
|
|
@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.protobuf.ProtocolMessageEnum
|
||||
|
|
@ -90,18 +91,29 @@ fun <T> DropDownPreference(
|
|||
expanded = !expanded
|
||||
}
|
||||
},
|
||||
modifier = modifier.padding(vertical = 8.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
|
||||
readOnly = true,
|
||||
value = items.firstOrNull { it.first == selectedItem }?.second ?: "",
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
label = { Text(title) },
|
||||
prefix = { Text(title) },
|
||||
suffix = { Text(items.firstOrNull { it.first == selectedItem }?.second ?: "") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors(),
|
||||
colors =
|
||||
ExposedDropdownMenuDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent,
|
||||
disabledBorderColor = Color.Transparent,
|
||||
errorBorderColor = Color.Transparent,
|
||||
),
|
||||
enabled = enabled,
|
||||
supportingText = { if (summary != null) Text(text = summary) },
|
||||
supportingText =
|
||||
if (summary != null) {
|
||||
{ Text(text = summary, modifier = Modifier.padding(bottom = 8.dp)) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
items
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.ui.common.components
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun PreferenceDivider() {
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
}
|
||||
|
|
@ -17,31 +17,36 @@
|
|||
|
||||
package com.geeksville.mesh.ui.settings.radio.components
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
|
||||
import com.geeksville.mesh.config
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.ui.common.components.DropDownPreference
|
||||
import com.geeksville.mesh.ui.common.components.PreferenceDivider
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.ChannelOption
|
||||
import org.meshtastic.core.model.RegionInfo
|
||||
import org.meshtastic.core.model.numChannels
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.EditTextPreference
|
||||
import org.meshtastic.core.ui.component.PreferenceCategory
|
||||
import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference
|
||||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
|
||||
@Composable
|
||||
fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
|
||||
|
|
@ -65,184 +70,182 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
|
|||
viewModel.setConfig(config)
|
||||
},
|
||||
) {
|
||||
item { PreferenceCategory(text = stringResource(R.string.options)) }
|
||||
item {
|
||||
DropDownPreference(
|
||||
title = stringResource(R.string.region_frequency_plan),
|
||||
summary = stringResource(id = R.string.config_lora_region_summary),
|
||||
enabled = state.connected,
|
||||
items = RegionInfo.entries.map { it.regionCode to it.description },
|
||||
selectedItem = formState.value.region,
|
||||
onItemSelected = { formState.value = formState.value.copy { region = it } },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.use_modem_preset),
|
||||
checked = formState.value.usePreset,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { usePreset = it } },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
|
||||
if (formState.value.usePreset) {
|
||||
item {
|
||||
TitledCard(title = stringResource(R.string.options)) {
|
||||
DropDownPreference(
|
||||
title = stringResource(R.string.modem_preset),
|
||||
summary = stringResource(id = R.string.config_lora_modem_preset_summary),
|
||||
enabled = state.connected && formState.value.usePreset,
|
||||
items =
|
||||
LoRaConfig.ModemPreset.entries
|
||||
.filter { it != LoRaConfig.ModemPreset.UNRECOGNIZED }
|
||||
.map { it to it.name },
|
||||
selectedItem = formState.value.modemPreset,
|
||||
onItemSelected = { formState.value = formState.value.copy { modemPreset = it } },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
} else {
|
||||
item {
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.bandwidth),
|
||||
value = formState.value.bandwidth,
|
||||
enabled = state.connected && !formState.value.usePreset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { bandwidth = it } },
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.spread_factor),
|
||||
value = formState.value.spreadFactor,
|
||||
enabled = state.connected && !formState.value.usePreset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } },
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.coding_rate),
|
||||
value = formState.value.codingRate,
|
||||
enabled = state.connected && !formState.value.usePreset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { codingRate = it } },
|
||||
)
|
||||
}
|
||||
}
|
||||
item { PreferenceCategory(text = stringResource(R.string.advanced)) }
|
||||
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.ignore_mqtt),
|
||||
checked = formState.value.ignoreMqtt,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { ignoreMqtt = it } },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.ok_to_mqtt),
|
||||
checked = formState.value.configOkToMqtt,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { configOkToMqtt = it } },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.tx_enabled),
|
||||
checked = formState.value.txEnabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { txEnabled = it } },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
item {
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.hop_limit),
|
||||
summary = stringResource(id = R.string.config_lora_hop_limit_summary),
|
||||
value = formState.value.hopLimit,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { hopLimit = it } },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
|
||||
item {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.frequency_slot),
|
||||
summary = stringResource(id = R.string.config_lora_frequency_slot_summary),
|
||||
value =
|
||||
if (isFocused || formState.value.channelNum != 0) {
|
||||
formState.value.channelNum
|
||||
} else {
|
||||
primaryChannel.channelNum
|
||||
},
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onFocusChanged = { isFocused = it.isFocused },
|
||||
onValueChanged = {
|
||||
if (it <= formState.value.numChannels) { // total num of LoRa channels
|
||||
formState.value = formState.value.copy { channelNum = it }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.sx126x_rx_boosted_gain),
|
||||
checked = formState.value.sx126XRxBoostedGain,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { sx126XRxBoostedGain = it } },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
item {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.override_frequency_mhz),
|
||||
value =
|
||||
if (isFocused || formState.value.overrideFrequency != 0f) {
|
||||
formState.value.overrideFrequency
|
||||
} else {
|
||||
primaryChannel.radioFreq
|
||||
},
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onFocusChanged = { isFocused = it.isFocused },
|
||||
onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } },
|
||||
)
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
item {
|
||||
SignedIntegerEditTextPreference(
|
||||
title = stringResource(R.string.tx_power_dbm),
|
||||
value = formState.value.txPower,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { txPower = it } },
|
||||
)
|
||||
}
|
||||
|
||||
if (viewModel.hasPaFan) {
|
||||
item {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.pa_fan_disabled),
|
||||
checked = formState.value.paFanDisabled,
|
||||
title = stringResource(R.string.region_frequency_plan),
|
||||
summary = stringResource(id = R.string.config_lora_region_summary),
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { paFanDisabled = it } },
|
||||
items = RegionInfo.entries.map { it.regionCode to it.description },
|
||||
selectedItem = formState.value.region,
|
||||
onItemSelected = { formState.value = formState.value.copy { region = it } },
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.use_modem_preset),
|
||||
checked = formState.value.usePreset,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { usePreset = it } },
|
||||
containerColor = Color.Transparent,
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
if (formState.value.usePreset) {
|
||||
DropDownPreference(
|
||||
title = stringResource(R.string.modem_preset),
|
||||
summary = stringResource(id = R.string.config_lora_modem_preset_summary),
|
||||
enabled = state.connected && formState.value.usePreset,
|
||||
items = ChannelOption.entries.map { it.modemPreset to stringResource(it.labelRes) },
|
||||
selectedItem = formState.value.modemPreset,
|
||||
onItemSelected = { formState.value = formState.value.copy { modemPreset = it } },
|
||||
)
|
||||
} else {
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.bandwidth),
|
||||
value = formState.value.bandwidth,
|
||||
enabled = state.connected && !formState.value.usePreset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { bandwidth = it } },
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.spread_factor),
|
||||
value = formState.value.spreadFactor,
|
||||
enabled = state.connected && !formState.value.usePreset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } },
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.coding_rate),
|
||||
value = formState.value.codingRate,
|
||||
enabled = state.connected && !formState.value.usePreset,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { codingRate = it } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(modifier = Modifier.height(16.dp)) }
|
||||
|
||||
item {
|
||||
TitledCard(title = stringResource(R.string.advanced)) {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.ignore_mqtt),
|
||||
checked = formState.value.ignoreMqtt,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { ignoreMqtt = it } },
|
||||
containerColor = Color.Transparent,
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.ok_to_mqtt),
|
||||
checked = formState.value.configOkToMqtt,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { configOkToMqtt = it } },
|
||||
containerColor = Color.Transparent,
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.tx_enabled),
|
||||
checked = formState.value.txEnabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { txEnabled = it } },
|
||||
containerColor = Color.Transparent,
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.hop_limit),
|
||||
summary = stringResource(id = R.string.config_lora_hop_limit_summary),
|
||||
value = formState.value.hopLimit,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { hopLimit = it } },
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
var isFocusedSlot by remember { mutableStateOf(false) }
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.frequency_slot),
|
||||
summary = stringResource(id = R.string.config_lora_frequency_slot_summary),
|
||||
value =
|
||||
if (isFocusedSlot || formState.value.channelNum != 0) {
|
||||
formState.value.channelNum
|
||||
} else {
|
||||
primaryChannel.channelNum
|
||||
},
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onFocusChanged = { isFocusedSlot = it.isFocused },
|
||||
onValueChanged = {
|
||||
if (it <= formState.value.numChannels) { // total num of LoRa channels
|
||||
formState.value = formState.value.copy { channelNum = it }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.sx126x_rx_boosted_gain),
|
||||
checked = formState.value.sx126XRxBoostedGain,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { sx126XRxBoostedGain = it } },
|
||||
containerColor = Color.Transparent,
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
var isFocusedOverride by remember { mutableStateOf(false) }
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.override_frequency_mhz),
|
||||
value =
|
||||
if (isFocusedOverride || formState.value.overrideFrequency != 0f) {
|
||||
formState.value.overrideFrequency
|
||||
} else {
|
||||
primaryChannel.radioFreq
|
||||
},
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onFocusChanged = { isFocusedOverride = it.isFocused },
|
||||
onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } },
|
||||
)
|
||||
|
||||
PreferenceDivider()
|
||||
|
||||
SignedIntegerEditTextPreference(
|
||||
title = stringResource(R.string.tx_power_dbm),
|
||||
value = formState.value.txPower,
|
||||
enabled = state.connected,
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { txPower = it } },
|
||||
)
|
||||
|
||||
if (viewModel.hasPaFan) {
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.pa_fan_disabled),
|
||||
checked = formState.value.paFanDisabled,
|
||||
enabled = state.connected,
|
||||
onCheckedChange = { formState.value = formState.value.copy { paFanDisabled = it } },
|
||||
containerColor = Color.Transparent,
|
||||
)
|
||||
}
|
||||
}
|
||||
item { HorizontalDivider() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
package com.geeksville.mesh.ui.settings.radio.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
|
@ -25,9 +27,12 @@ import androidx.compose.material3.Scaffold
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.geeksville.mesh.ui.common.components.MainAppBar
|
||||
import com.geeksville.mesh.ui.settings.radio.ResponseState
|
||||
import com.google.protobuf.MessageLite
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.PreferenceFooter
|
||||
|
||||
@Composable
|
||||
|
|
@ -61,21 +66,24 @@ fun <T : MessageLite> RadioConfigScreenList(
|
|||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
|
||||
content()
|
||||
item {
|
||||
PreferenceFooter(
|
||||
enabled = enabled && configState.isDirty,
|
||||
onCancelClicked = {
|
||||
focusManager.clearFocus()
|
||||
configState.reset()
|
||||
},
|
||||
onSaveClicked = {
|
||||
focusManager.clearFocus()
|
||||
onSave(configState.value)
|
||||
},
|
||||
)
|
||||
Column(modifier = Modifier.padding(innerPadding)) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize().weight(1f), contentPadding = PaddingValues(16.dp)) {
|
||||
content()
|
||||
}
|
||||
|
||||
PreferenceFooter(
|
||||
enabled = enabled && configState.isDirty,
|
||||
negativeText = stringResource(R.string.discard_changes),
|
||||
onNegativeClicked = {
|
||||
focusManager.clearFocus()
|
||||
configState.reset()
|
||||
},
|
||||
positiveText = stringResource(R.string.save_changes),
|
||||
onPositiveClicked = {
|
||||
focusManager.clearFocus()
|
||||
onSave(configState.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue