mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2025-12-24 00:07:48 -05:00
Generate a POSIX timezone string from a ZoneID (#3514)
This commit is contained in:
@@ -28,7 +28,6 @@
|
||||
<ID>FinalNewline:BLEException.kt$com.geeksville.mesh.service.BLEException.kt</ID>
|
||||
<ID>FinalNewline:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt</ID>
|
||||
<ID>FinalNewline:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt</ID>
|
||||
<ID>FinalNewline:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt</ID>
|
||||
<ID>FinalNewline:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
|
||||
<ID>FinalNewline:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
|
||||
<ID>FinalNewline:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
|
||||
@@ -74,10 +73,8 @@
|
||||
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$6790</ID>
|
||||
<ID>MagicNumber:ProbeTableProvider.kt$ProbeTableProvider$9114</ID>
|
||||
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$10</ID>
|
||||
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$100</ID>
|
||||
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$1000</ID>
|
||||
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth$2500</ID>
|
||||
<ID>MagicNumber:SafeBluetooth.kt$SafeBluetooth.<no name provided>$2500</ID>
|
||||
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$115200</ID>
|
||||
<ID>MagicNumber:SerialConnectionImpl.kt$SerialConnectionImpl$200</ID>
|
||||
<ID>MagicNumber:ServiceClient.kt$ServiceClient$500</ID>
|
||||
@@ -161,7 +158,6 @@
|
||||
<ID>NewLineAtEndOfFile:BLEException.kt$com.geeksville.mesh.service.BLEException.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:BluetoothInterfaceFactory.kt$com.geeksville.mesh.repository.radio.BluetoothInterfaceFactory.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:BluetoothRepositoryModule.kt$com.geeksville.mesh.repository.bluetooth.BluetoothRepositoryModule.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:CoroutineDispatchers.kt$com.geeksville.mesh.CoroutineDispatchers.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:Coroutines.kt$com.geeksville.mesh.concurrent.Coroutines.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:DateUtils.kt$com.geeksville.mesh.android.DateUtils.kt</ID>
|
||||
<ID>NewLineAtEndOfFile:DebugLogFile.kt$com.geeksville.mesh.android.DebugLogFile.kt</ID>
|
||||
@@ -195,8 +191,6 @@
|
||||
<ID>SwallowedException:MeshService.kt$MeshService$ex: BLEException</ID>
|
||||
<ID>SwallowedException:MeshService.kt$MeshService$ex: CancellationException</ID>
|
||||
<ID>SwallowedException:NsdManager.kt$ex: IllegalArgumentException</ID>
|
||||
<ID>SwallowedException:SafeBluetooth.kt$SafeBluetooth$ex: DeadObjectException</ID>
|
||||
<ID>SwallowedException:SafeBluetooth.kt$SafeBluetooth$ex: NullPointerException</ID>
|
||||
<ID>SwallowedException:ServiceClient.kt$ServiceClient$ex: IllegalArgumentException</ID>
|
||||
<ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID>
|
||||
<ID>TooGenericExceptionCaught:BTScanModel.kt$BTScanModel$ex: Throwable</ID>
|
||||
@@ -206,8 +200,6 @@
|
||||
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MeshService.kt$MeshService.<no name provided>$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:MeshServiceStarter.kt$ServiceStarter$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SafeBluetooth.kt$SafeBluetooth$ex: Exception</ID>
|
||||
<ID>TooGenericExceptionCaught:SafeBluetooth.kt$SafeBluetooth$ex: NullPointerException</ID>
|
||||
<ID>TooGenericExceptionCaught:SyncContinuation.kt$Continuation$ex: Throwable</ID>
|
||||
<ID>TooGenericExceptionCaught:TCPInterface.kt$TCPInterface$ex: Throwable</ID>
|
||||
<ID>TooGenericExceptionThrown:MeshService.kt$MeshService$throw Exception("Can't set user without a NodeInfo")</ID>
|
||||
|
||||
@@ -86,6 +86,7 @@ dependencies {
|
||||
kover(projects.core.navigation)
|
||||
kover(projects.core.network)
|
||||
kover(projects.core.prefs)
|
||||
kover(projects.core.ui)
|
||||
kover(projects.feature.intro)
|
||||
kover(projects.feature.messaging)
|
||||
kover(projects.feature.map)
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
<string name="config_device_tripleClickAsAdHocPing_summary">Send a position on the primary channel when the user button is triple clicked.</string>
|
||||
<string name="config_device_ledHeartbeatEnabled_summary">Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDs, the charger and GPS LEDs are not controllable.</string>
|
||||
<string name="config_device_tzdef_summary">Time zone for dates on the device screen and log.</string>
|
||||
<string name="config_device_use_phone_tz">Use phone time zone</string>
|
||||
<string name="config_device_transmitOverLora_summary">Whether in addition to sending it to MQTT and the PhoneAPI, our NeighborInfo should be transmitted over LoRa. Not available on a channel with default key and name.</string>
|
||||
|
||||
<string name="config_display_screen_on_secs_summary">How long the screen remains on after the user button is pressed or messages are received.</string>
|
||||
|
||||
@@ -19,6 +19,7 @@ plugins {
|
||||
alias(libs.plugins.meshtastic.android.library)
|
||||
alias(libs.plugins.meshtastic.android.library.compose)
|
||||
alias(libs.plugins.meshtastic.hilt)
|
||||
alias(libs.plugins.kover)
|
||||
}
|
||||
|
||||
android { namespace = "org.meshtastic.core.ui" }
|
||||
@@ -49,4 +50,6 @@ dependencies {
|
||||
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
androidTestImplementation(libs.androidx.test.runner)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ fun <T> DropDownPreference(
|
||||
enabled = enabled,
|
||||
supportingText =
|
||||
if (summary != null) {
|
||||
{ Text(text = summary, modifier = Modifier.padding(bottom = 8.dp)) }
|
||||
{ Text(text = summary) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
||||
@@ -82,7 +82,7 @@ fun SwitchPreference(
|
||||
},
|
||||
supportingContent = {
|
||||
if (summary.isNotEmpty()) {
|
||||
Text(text = summary, modifier = Modifier.padding(bottom = 16.dp))
|
||||
Text(text = summary)
|
||||
}
|
||||
},
|
||||
headlineContent = { Text(text = title) },
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("Wrapping", "UnusedImports", "SpacingAroundColon")
|
||||
|
||||
package org.meshtastic.core.ui.timezone
|
||||
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoField
|
||||
import java.time.temporal.WeekFields
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Generates a POSIX time zone string from a [ZoneId]. Uses the specification found
|
||||
* [here](https://www.postgresql.org/docs/current/datetime-posix-timezone-specs.html).
|
||||
*/
|
||||
fun ZoneId.toPosixString(): String {
|
||||
val now = Instant.now()
|
||||
val upcomingTransition = rules.nextTransition(now)
|
||||
|
||||
// No upcoming transition means this time zone does not support DST.
|
||||
if (upcomingTransition == null) {
|
||||
with(now.asZonedDateTime()) {
|
||||
return "${timeZoneShortName()}${formattedOffsetString()}"
|
||||
}
|
||||
}
|
||||
|
||||
val upcomingInstant = upcomingTransition.instant
|
||||
val followingTransition = rules.nextTransition(upcomingInstant)
|
||||
|
||||
val (stdTransition, dstTransition) =
|
||||
if (rules.isDaylightSavings(upcomingInstant)) {
|
||||
followingTransition to upcomingTransition
|
||||
} else {
|
||||
upcomingTransition to followingTransition
|
||||
}
|
||||
|
||||
val stdDate = stdTransition.instant.asZonedDateTime()
|
||||
val dstDate = dstTransition.instant.asZonedDateTime()
|
||||
|
||||
return buildString {
|
||||
append(stdDate.timeZoneShortName())
|
||||
append(stdDate.formattedOffsetString())
|
||||
append(dstDate.timeZoneShortName())
|
||||
|
||||
// Don't append the DST offset if it is only 1 hour off.
|
||||
@Suppress("MagicNumber")
|
||||
if (abs(stdDate.offset.totalSeconds - dstDate.offset.totalSeconds) != 3600) {
|
||||
append(dstDate.formattedOffsetString())
|
||||
}
|
||||
|
||||
append(dstTransition.dateTimeBefore.transitionRuleString())
|
||||
append(stdTransition.dateTimeBefore.transitionRuleString())
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the time zone short. e.g. "EST" or "EDT". */
|
||||
private fun ZonedDateTime.timeZoneShortName(): String {
|
||||
val formatter = DateTimeFormatter.ofPattern("zzz", Locale.ENGLISH)
|
||||
val shortName = format(formatter)
|
||||
return if (shortName.startsWith("GMT")) "GMT" else shortName
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time zone offset string with the format "<HOURS>:<MINUTES>:<SECONDS>". Minutes and seconds are only shown
|
||||
* if they are non-zero.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun ZonedDateTime.formattedOffsetString(): String {
|
||||
val offsetSeconds = -offset.totalSeconds
|
||||
|
||||
val hours = offsetSeconds / 3600
|
||||
val remainingSeconds = abs(offsetSeconds) % 3600
|
||||
val minutes = remainingSeconds / 60
|
||||
val seconds = remainingSeconds % 60
|
||||
|
||||
return buildString {
|
||||
append(hours)
|
||||
appendMinSec(minutes = minutes, seconds = seconds) { ":%02d".format(Locale.ENGLISH, it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a transition rule string with the format
|
||||
* ",M<MONTH>.<WEEK_OF_MONTH>.<DAY_OF_WEEK>/<HOURS>:<MINUTES>:<SECONDS>". Time is omitted if it is 2:00:00, since that
|
||||
* is the spec default. Otherwise, append time with non-zero values.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun LocalDateTime.transitionRuleString() = buildString {
|
||||
val weekOfMonth = get(ChronoField.ALIGNED_WEEK_OF_MONTH)
|
||||
val dayOfWeek = get(WeekFields.of(DayOfWeek.SUNDAY, 7).dayOfWeek()) - 1
|
||||
append(",M$monthValue.$weekOfMonth.$dayOfWeek")
|
||||
|
||||
when {
|
||||
// No-op for spec default
|
||||
hour == 2 && minute == 0 && second == 0 -> Unit
|
||||
else -> {
|
||||
append("/$hour")
|
||||
appendMinSec(minutes = minute, seconds = second) { ":$it" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun StringBuilder.appendMinSec(minutes: Int, seconds: Int, format: (Int) -> String) {
|
||||
if (minutes != 0 || seconds != 0) {
|
||||
// This covers both "30m:30s" and "00m:30s"
|
||||
append(format(minutes))
|
||||
// This prevents "30m:00s"
|
||||
if (seconds != 0) append(format(seconds))
|
||||
}
|
||||
}
|
||||
|
||||
context(zoneId: ZoneId)
|
||||
private fun Instant.asZonedDateTime() = ZonedDateTime.ofInstant(this, zoneId)
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 org.meshtastic.core.ui.timezone
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.time.ZoneId
|
||||
|
||||
class ZoneIdExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `test POSIX string generation`() {
|
||||
val zoneMap =
|
||||
mapOf(
|
||||
"US/Hawaii" to "HST10",
|
||||
"US/Alaska" to "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"US/Pacific" to "PST8PDT,M3.2.0,M11.1.0",
|
||||
"US/Arizona" to "MST7",
|
||||
"US/Mountain" to "MST7MDT,M3.2.0,M11.1.0",
|
||||
"US/Central" to "CST6CDT,M3.2.0,M11.1.0",
|
||||
"US/Eastern" to "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Sao_Paulo" to "BRT3",
|
||||
"UTC" to "UTC0",
|
||||
"Europe/London" to "GMT0BST,M3.5.0/1,M10.4.0",
|
||||
"Europe/Lisbon" to "WET0WEST,M3.5.0/1,M10.4.0",
|
||||
"Europe/Budapest" to "CET-1CEST,M3.5.0,M10.4.0/3",
|
||||
"Europe/Kiev" to "EET-2EEST,M3.5.0/3,M10.4.0/4",
|
||||
"Africa/Cairo" to "EET-2EEST,M4.4.5/0,M10.5.5/0",
|
||||
"Asia/Kolkata" to "IST-5:30",
|
||||
"Asia/Hong_Kong" to "HKT-8",
|
||||
"Asia/Tokyo" to "JST-9",
|
||||
"Australia/Perth" to "AWST-8",
|
||||
"Australia/Adelaide" to "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Sydney" to "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Pacific/Auckland" to "NZST-12NZDT,M9.4.0,M4.1.0/3",
|
||||
)
|
||||
|
||||
zoneMap.forEach { (tz, expected) -> assertEquals(expected, ZoneId.of(tz).toPosixString()) }
|
||||
}
|
||||
}
|
||||
@@ -17,13 +17,14 @@
|
||||
<ID>LongMethod:AudioConfigItemList.kt$@Composable fun AudioConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:CannedMessageConfigItemList.kt$@Composable fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DetectionSensorConfigItemList.kt$@Composable fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DeviceConfigItemList.kt$@Composable fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DeviceConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:DisplayConfigItemList.kt$@Composable fun DisplayConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:LoRaConfigItemList.kt$@Composable fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:PositionConfigItemList.kt$@OptIn(ExperimentalPermissionsApi::class) @Composable fun PositionConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:RadioConfigScreenList.kt$@Composable fun <T : MessageLite> RadioConfigScreenList( title: String, onBack: () -> Unit, responseState: ResponseState<Any>, onDismissPacketResponse: () -> Unit, configState: ConfigState<T>, enabled: Boolean, onSave: (T) -> Unit, content: LazyListScope.() -> Unit, )</ID>
|
||||
<ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
|
||||
<ID>LongMethod:SecurityConfigItemList.kt$@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SecurityConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
<ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit)</ID>
|
||||
@@ -35,6 +36,7 @@
|
||||
<ID>MagicNumber:EditChannelDialog.kt$32</ID>
|
||||
<ID>MagicNumber:PacketResponseStateDialog.kt$100</ID>
|
||||
<ID>ModifierMissing:CleanNodeDatabaseScreen.kt$CleanNodeDatabaseScreen</ID>
|
||||
<ID>ModifierMissing:DeviceConfigItemList.kt$DeviceConfigScreen</ID>
|
||||
<ID>ModifierMissing:MapReportingPreference.kt$MapReportingPreference</ID>
|
||||
<ID>ModifierMissing:NetworkConfigItemList.kt$NetworkConfigScreen</ID>
|
||||
<ID>ModifierMissing:PositionConfigItemList.kt$PositionConfigScreen</ID>
|
||||
|
||||
@@ -17,27 +17,43 @@
|
||||
|
||||
package org.meshtastic.feature.settings.radio.component
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Clear
|
||||
import androidx.compose.material.icons.rounded.PhoneAndroid
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonDefaults.MediumContainerHeight
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -47,19 +63,23 @@ import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.DropDownPreference
|
||||
import org.meshtastic.core.ui.component.EditTextPreference
|
||||
import org.meshtastic.core.ui.component.InsetDivider
|
||||
import org.meshtastic.core.ui.component.SwitchPreference
|
||||
import org.meshtastic.core.ui.component.TitledCard
|
||||
import org.meshtastic.core.ui.timezone.toPosixString
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.util.IntervalConfiguration
|
||||
import org.meshtastic.feature.settings.util.toDisplayString
|
||||
import org.meshtastic.proto.ConfigProtos.Config.DeviceConfig
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import java.time.ZoneId
|
||||
|
||||
private val DeviceConfig.Role.description: Int
|
||||
get() =
|
||||
@@ -91,6 +111,7 @@ private val DeviceConfig.RebroadcastMode.description: Int
|
||||
else -> R.string.unrecognized
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack: () -> Unit) {
|
||||
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
|
||||
@@ -131,6 +152,7 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
||||
onItemSelected = { selectedRole = it },
|
||||
summary = stringResource(id = formState.value.role.description),
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
DropDownPreference(
|
||||
@@ -140,6 +162,7 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
||||
onItemSelected = { formState.value = formState.value.copy { rebroadcastMode = it } },
|
||||
summary = stringResource(id = formState.value.rebroadcastMode.description),
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
val nodeInfoBroadcastIntervals = remember { IntervalConfiguration.NODE_INFO_BROADCAST.allowedIntervals }
|
||||
@@ -152,6 +175,7 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
TitledCard(title = stringResource(R.string.hardware)) {
|
||||
SwitchPreference(
|
||||
@@ -162,7 +186,8 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
||||
onCheckedChange = { formState.value = formState.value.copy { doubleTapAsButtonPress = it } },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
InsetDivider()
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.triple_click_adhoc_ping),
|
||||
@@ -172,7 +197,9 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
||||
onCheckedChange = { formState.value = formState.value.copy { disableTripleClick = !it } },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
InsetDivider()
|
||||
|
||||
SwitchPreference(
|
||||
title = stringResource(R.string.led_heartbeat),
|
||||
summary = stringResource(id = R.string.config_device_ledHeartbeatEnabled_summary),
|
||||
@@ -181,13 +208,27 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
||||
onCheckedChange = { formState.value = formState.value.copy { ledHeartbeatDisabled = !it } },
|
||||
containerColor = CardDefaults.cardColors().containerColor,
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
item {
|
||||
TitledCard(title = stringResource(R.string.debug)) {
|
||||
TitledCard(title = stringResource(R.string.time_zone)) {
|
||||
val context = LocalContext.current
|
||||
val appTzPosixString by
|
||||
produceState(initialValue = ZoneId.systemDefault().toPosixString()) {
|
||||
val receiver =
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_TIMEZONE_CHANGED) {
|
||||
value = ZoneId.systemDefault().toPosixString()
|
||||
}
|
||||
}
|
||||
}
|
||||
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
|
||||
awaitDispose { context.unregisterReceiver(receiver) }
|
||||
}
|
||||
|
||||
EditTextPreference(
|
||||
title = stringResource(R.string.time_zone),
|
||||
title = "",
|
||||
value = formState.value.tzdef,
|
||||
summary = stringResource(id = R.string.config_device_tzdef_summary),
|
||||
maxSize = 64, // tzdef max_size:65
|
||||
@@ -197,7 +238,27 @@ fun DeviceConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
|
||||
onValueChanged = { formState.value = formState.value.copy { tzdef = it } },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { formState.value = formState.value.copy { tzdef = "" } }) {
|
||||
Icon(imageVector = Icons.Rounded.Clear, contentDescription = null)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
TextButton(
|
||||
modifier = Modifier.height(MediumContainerHeight).fillMaxWidth(),
|
||||
enabled = state.connected,
|
||||
shape = RectangleShape,
|
||||
onClick = { formState.value = formState.value.copy { tzdef = appTzPosixString } },
|
||||
) {
|
||||
Icon(imageVector = Icons.Rounded.PhoneAndroid, contentDescription = null)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Text(text = stringResource(R.string.config_device_use_phone_tz))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -78,7 +79,11 @@ fun <T : MessageLite> RadioConfigScreenList(
|
||||
val showFooterButtons = configState.isDirty
|
||||
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(16.dp)) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
content()
|
||||
|
||||
item {
|
||||
|
||||
Reference in New Issue
Block a user