mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-23 12:15:05 -04:00
feat(settings): label enum pickers from the schema (#7298)
This commit is contained in:
1 parent
8c22e3b2ae
commit
720c7a0850
20 files changed
+353
-226
No files matched your search
Generated
-44
@@ -1430,13 +1430,6 @@ range_test
|
||||
range_test_config
|
||||
react
|
||||
reboot
|
||||
### REBROADCAST ###
|
||||
rebroadcast_mode_all_desc
|
||||
rebroadcast_mode_all_skip_decoding_desc
|
||||
rebroadcast_mode_core_portnums_only_desc
|
||||
rebroadcast_mode_known_only_desc
|
||||
rebroadcast_mode_local_only_desc
|
||||
rebroadcast_mode_none_desc
|
||||
recent_network_devices
|
||||
reconnecting
|
||||
redacted
|
||||
@@ -1493,30 +1486,17 @@ ringtone_imported
|
||||
role
|
||||
role_client
|
||||
role_client_base
|
||||
role_client_base_desc
|
||||
role_client_desc
|
||||
role_client_hidden
|
||||
role_client_hidden_desc
|
||||
role_client_mute
|
||||
role_client_mute_desc
|
||||
role_lost_and_found
|
||||
role_lost_and_found_desc
|
||||
role_repeater
|
||||
role_repeater_desc
|
||||
role_router
|
||||
role_router_client
|
||||
role_router_client_desc
|
||||
role_router_desc
|
||||
role_router_late
|
||||
role_router_late_desc
|
||||
role_sensor
|
||||
role_sensor_desc
|
||||
role_tak
|
||||
role_tak_desc
|
||||
role_tak_tracker
|
||||
role_tak_tracker_desc
|
||||
role_tracker
|
||||
role_tracker_desc
|
||||
router_role_confirmation_text
|
||||
### ROUTING ###
|
||||
routing_error_admin_bad_session_key
|
||||
@@ -1702,15 +1682,6 @@ system_settings
|
||||
tak
|
||||
tak_config
|
||||
tak_role
|
||||
tak_role_forwardobserver
|
||||
tak_role_hq
|
||||
tak_role_k9
|
||||
tak_role_medic
|
||||
tak_role_rto
|
||||
tak_role_sniper
|
||||
tak_role_teamlead
|
||||
tak_role_teammember
|
||||
tak_role_unspecified
|
||||
tak_server
|
||||
tak_server_channel
|
||||
tak_server_channel_desc
|
||||
@@ -1743,21 +1714,6 @@ tak_server_test_run
|
||||
tak_server_test_running
|
||||
tak_server_v1_fallback_notice
|
||||
tak_team
|
||||
tak_team_blue
|
||||
tak_team_brown
|
||||
tak_team_cyan
|
||||
tak_team_dark_blue
|
||||
tak_team_dark_green
|
||||
tak_team_green
|
||||
tak_team_magenta
|
||||
tak_team_maroon
|
||||
tak_team_orange
|
||||
tak_team_purple
|
||||
tak_team_red
|
||||
tak_team_teal
|
||||
tak_team_unspecified_color
|
||||
tak_team_white
|
||||
tak_team_yellow
|
||||
telemetry
|
||||
telemetry_config
|
||||
temperature
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2026 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.model
|
||||
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.allStringResources
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.MemberRole
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.Team
|
||||
|
||||
// Generated by ./gradlew :schema-strings:sync from org.meshtastic:protobufs 2.8.0.116-g51028ca-SNAPSHOT.
|
||||
// Do not edit: change the schema, then run sync.
|
||||
|
||||
/**
|
||||
* The schema's label for this enum value, or null where the schema does not name it. A picker shows this in place of
|
||||
* the constant's Kotlin name, and falls back to that name when it is null, so a value the schema has not reached still
|
||||
* renders.
|
||||
*/
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun Enum<*>.schemaLabelRes(): StringResource? = when (this) {
|
||||
is Config.BluetoothConfig.PairingMode -> schemaLabel("schema_bluetooth_pairingmode_")
|
||||
is Config.DeviceConfig.RebroadcastMode -> schemaLabel("schema_device_rebroadcastmode_")
|
||||
is Config.DeviceConfig.Role -> schemaLabel("schema_device_role_")
|
||||
is Config.DisplayConfig.CompassOrientation -> schemaLabel("schema_display_compassorientation_")
|
||||
is Config.DisplayConfig.DisplayMode -> schemaLabel("schema_display_displaymode_")
|
||||
is Config.DisplayConfig.DisplayUnits -> schemaLabel("schema_display_displayunits_")
|
||||
is Config.DisplayConfig.OledType -> schemaLabel("schema_display_oledtype_")
|
||||
is Config.LoRaConfig.ModemPreset -> schemaLabel("schema_lora_modempreset_")
|
||||
is Config.LoRaConfig.RegionCode -> schemaLabel("schema_lora_regioncode_")
|
||||
is Config.NetworkConfig.AddressMode -> schemaLabel("schema_network_addressmode_")
|
||||
is Config.NetworkConfig.ProtocolFlags -> schemaLabel("schema_network_protocolflags_")
|
||||
is Config.PositionConfig.GpsMode -> schemaLabel("schema_position_gpsmode_")
|
||||
is Config.PositionConfig.PositionFlags -> schemaLabel("schema_position_positionflags_")
|
||||
is Config.SecurityConfig.PacketSignaturePolicy -> schemaLabel("schema_security_packetsignaturepolicy_")
|
||||
is MemberRole -> schemaLabel("schema_memberrole_")
|
||||
is ModuleConfig.AudioConfig.Audio_Baud -> schemaLabel("schema_audio_audio_baud_")
|
||||
is ModuleConfig.CannedMessageConfig.InputEventChar -> schemaLabel("schema_cannedmessage_inputeventchar_")
|
||||
is ModuleConfig.DetectionSensorConfig.TriggerType -> schemaLabel("schema_detectionsensor_triggertype_")
|
||||
is ModuleConfig.SerialConfig.Serial_Baud -> schemaLabel("schema_serial_serial_baud_")
|
||||
is ModuleConfig.SerialConfig.Serial_Mode -> schemaLabel("schema_serial_serial_mode_")
|
||||
is Team -> schemaLabel("schema_team_")
|
||||
else -> null
|
||||
}
|
||||
|
||||
/** The schema's one-sentence explanation of this enum value, or null where it has none. */
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun Enum<*>.schemaDescriptionRes(): StringResource? = when (this) {
|
||||
is Config.DeviceConfig.RebroadcastMode -> schemaDescription("schema_device_rebroadcastmode_")
|
||||
is Config.DeviceConfig.Role -> schemaDescription("schema_device_role_")
|
||||
is Config.PositionConfig.PositionFlags -> schemaDescription("schema_position_positionflags_")
|
||||
is Config.SecurityConfig.PacketSignaturePolicy -> schemaDescription("schema_security_packetsignaturepolicy_")
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun Enum<*>.schemaLabel(prefix: String): StringResource? = Res.allStringResources[prefix + name.lowercase()]
|
||||
|
||||
private fun Enum<*>.schemaDescription(prefix: String): StringResource? =
|
||||
Res.allStringResources[prefix + name.lowercase() + "_description"]
|
||||
@@ -16,66 +16,8 @@
|
||||
*/
|
||||
package org.meshtastic.core.model
|
||||
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.tak_role_forwardobserver
|
||||
import org.meshtastic.core.resources.tak_role_hq
|
||||
import org.meshtastic.core.resources.tak_role_k9
|
||||
import org.meshtastic.core.resources.tak_role_medic
|
||||
import org.meshtastic.core.resources.tak_role_rto
|
||||
import org.meshtastic.core.resources.tak_role_sniper
|
||||
import org.meshtastic.core.resources.tak_role_teamlead
|
||||
import org.meshtastic.core.resources.tak_role_teammember
|
||||
import org.meshtastic.core.resources.tak_role_unspecified
|
||||
import org.meshtastic.core.resources.tak_team_blue
|
||||
import org.meshtastic.core.resources.tak_team_brown
|
||||
import org.meshtastic.core.resources.tak_team_cyan
|
||||
import org.meshtastic.core.resources.tak_team_dark_blue
|
||||
import org.meshtastic.core.resources.tak_team_dark_green
|
||||
import org.meshtastic.core.resources.tak_team_green
|
||||
import org.meshtastic.core.resources.tak_team_magenta
|
||||
import org.meshtastic.core.resources.tak_team_maroon
|
||||
import org.meshtastic.core.resources.tak_team_orange
|
||||
import org.meshtastic.core.resources.tak_team_purple
|
||||
import org.meshtastic.core.resources.tak_team_red
|
||||
import org.meshtastic.core.resources.tak_team_teal
|
||||
import org.meshtastic.core.resources.tak_team_unspecified_color
|
||||
import org.meshtastic.core.resources.tak_team_white
|
||||
import org.meshtastic.core.resources.tak_team_yellow
|
||||
import org.meshtastic.proto.MemberRole
|
||||
import org.meshtastic.proto.Team
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun getStringResFrom(team: Team): StringResource = when (team) {
|
||||
Team.Unspecifed_Color -> Res.string.tak_team_unspecified_color
|
||||
Team.White -> Res.string.tak_team_white
|
||||
Team.Yellow -> Res.string.tak_team_yellow
|
||||
Team.Orange -> Res.string.tak_team_orange
|
||||
Team.Magenta -> Res.string.tak_team_magenta
|
||||
Team.Red -> Res.string.tak_team_red
|
||||
Team.Maroon -> Res.string.tak_team_maroon
|
||||
Team.Purple -> Res.string.tak_team_purple
|
||||
Team.Dark_Blue -> Res.string.tak_team_dark_blue
|
||||
Team.Blue -> Res.string.tak_team_blue
|
||||
Team.Cyan -> Res.string.tak_team_cyan
|
||||
Team.Teal -> Res.string.tak_team_teal
|
||||
Team.Green -> Res.string.tak_team_green
|
||||
Team.Dark_Green -> Res.string.tak_team_dark_green
|
||||
Team.Brown -> Res.string.tak_team_brown
|
||||
}
|
||||
|
||||
fun getStringResFrom(role: MemberRole): StringResource = when (role) {
|
||||
MemberRole.Unspecifed -> Res.string.tak_role_unspecified
|
||||
MemberRole.TeamMember -> Res.string.tak_role_teammember
|
||||
MemberRole.TeamLead -> Res.string.tak_role_teamlead
|
||||
MemberRole.HQ -> Res.string.tak_role_hq
|
||||
MemberRole.Sniper -> Res.string.tak_role_sniper
|
||||
MemberRole.Medic -> Res.string.tak_role_medic
|
||||
MemberRole.ForwardObserver -> Res.string.tak_role_forwardobserver
|
||||
MemberRole.RTO -> Res.string.tak_role_rto
|
||||
MemberRole.K9 -> Res.string.tak_role_k9
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "MagicNumber")
|
||||
fun getColorFrom(team: Team): Long = when (team) {
|
||||
Team.Unspecifed_Color -> 0xFF00FFFF
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<!-- Generated by ./gradlew :schema-strings:sync from org.meshtastic:protobufs 2.8.0.114-g8f97d66-SNAPSHOT. Do not edit: change the schema, then run sync. -->
|
||||
<!-- Generated by ./gradlew :schema-strings:sync from org.meshtastic:protobufs 2.8.0.116-g51028ca-SNAPSHOT. Do not edit: change the schema, then run sync. -->
|
||||
<string name="schema_ambientlighting_blue">Blue</string>
|
||||
<string name="schema_ambientlighting_blue_description">The blue level of the ambient lighting LED.</string>
|
||||
<string name="schema_ambientlighting_current">Current</string>
|
||||
|
||||
@@ -1478,13 +1478,6 @@
|
||||
<string name="range_test_config">Range Test Config</string>
|
||||
<string name="react">React</string>
|
||||
<string name="reboot">Reboot</string>
|
||||
<!-- REBROADCAST -->
|
||||
<string name="rebroadcast_mode_all_desc">Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora parameters.</string>
|
||||
<string name="rebroadcast_mode_all_skip_decoding_desc">Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior.</string>
|
||||
<string name="rebroadcast_mode_core_portnums_only_desc">Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc. Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing.</string>
|
||||
<string name="rebroadcast_mode_known_only_desc">Ignores observed messages from foreign meshes like LOCAL ONLY, but takes it step further by also ignoring messages from nodes not already in the node's known list.</string>
|
||||
<string name="rebroadcast_mode_local_only_desc">Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels.</string>
|
||||
<string name="rebroadcast_mode_none_desc">Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.</string>
|
||||
<string name="recent_network_devices">Recent Network Devices</string>
|
||||
<string name="reconnecting">Reconnecting…</string>
|
||||
<string name="redacted" translatable="false">***[REDACTED]***</string>
|
||||
@@ -1544,30 +1537,17 @@
|
||||
<string name="role">Device Role</string>
|
||||
<string name="role_client">Client</string>
|
||||
<string name="role_client_base">Client Base</string>
|
||||
<string name="role_client_base_desc">Treats packets from or to favorited nodes as ROUTER_LATE, and all other packets as CLIENT.</string>
|
||||
<string name="role_client_desc">App connected or standalone messaging device.</string>
|
||||
<string name="role_client_hidden">Client Hidden</string>
|
||||
<string name="role_client_hidden_desc">Device that only broadcasts as needed for stealth or power savings.</string>
|
||||
<string name="role_client_mute">Client Mute</string>
|
||||
<string name="role_client_mute_desc">Device that does not forward packets from other devices.</string>
|
||||
<string name="role_lost_and_found">Lost and Found</string>
|
||||
<string name="role_lost_and_found_desc">Broadcasts location as message to default channel regularly to assist with device recovery.</string>
|
||||
<string name="role_repeater">Repeater</string>
|
||||
<string name="role_repeater_desc">Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in nodes list.</string>
|
||||
<string name="role_router">Router</string>
|
||||
<string name="role_router_client">Router Client</string>
|
||||
<string name="role_router_client_desc">Combination of both ROUTER and CLIENT. Not for mobile devices.</string>
|
||||
<string name="role_router_desc">Infrastructure node for extending network coverage by relaying messages. Visible in nodes list.</string>
|
||||
<string name="role_router_late">Router Late</string>
|
||||
<string name="role_router_late_desc">Infrastructure node that always rebroadcasts packets once but only after all other modes, ensuring additional coverage for local clusters. Visible in nodes list.</string>
|
||||
<string name="role_sensor">Sensor</string>
|
||||
<string name="role_sensor_desc">Broadcasts telemetry packets as priority.</string>
|
||||
<string name="role_tak">TAK</string>
|
||||
<string name="role_tak_desc">Optimized for ATAK system communication, reduces routine broadcasts.</string>
|
||||
<string name="role_tak_tracker">TAK Tracker</string>
|
||||
<string name="role_tak_tracker_desc">Enables automatic TAK PLI broadcasts and reduces routine broadcasts.</string>
|
||||
<string name="role_tracker">Tracker</string>
|
||||
<string name="role_tracker_desc">Broadcasts GPS position packets as priority.</string>
|
||||
<string name="router_role_confirmation_text">I have read the <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">Device Role Documentation</a> and the blog post about <a href="http://meshtastic.org/blog/choosing-the-right-device-role">Choosing The Right Device Role</a>.</string>
|
||||
<!-- ROUTING -->
|
||||
<string name="routing_error_admin_bad_session_key">Admin session expired</string>
|
||||
@@ -1753,15 +1733,6 @@
|
||||
<string name="tak">TAK (ATAK)</string>
|
||||
<string name="tak_config">TAK Configuration</string>
|
||||
<string name="tak_role">Member Role</string>
|
||||
<string name="tak_role_forwardobserver">Forward Observer</string>
|
||||
<string name="tak_role_hq">Headquarters</string>
|
||||
<string name="tak_role_k9">Doggo (K9)</string>
|
||||
<string name="tak_role_medic">Medic</string>
|
||||
<string name="tak_role_rto">Radio Telephone Operator</string>
|
||||
<string name="tak_role_sniper">Sniper</string>
|
||||
<string name="tak_role_teamlead">Team Lead</string>
|
||||
<string name="tak_role_teammember">Team Member</string>
|
||||
<string name="tak_role_unspecified">Unspecified</string>
|
||||
<string name="tak_server">TAK Server</string>
|
||||
<string name="tak_server_channel">TAK Mesh Channel</string>
|
||||
<string name="tak_server_channel_desc">Meshtastic channel used for outgoing TAK traffic</string>
|
||||
@@ -1797,21 +1768,6 @@
|
||||
<string name="tak_server_test_running">Running: %1$s</string>
|
||||
<string name="tak_server_v1_fallback_notice">Connected node firmware doesn't support full TAK integration — only location and chat messages will bridge to ATAK. Markers and other event types need firmware 2.8.0 or newer.</string>
|
||||
<string name="tak_team">Team Color</string>
|
||||
<string name="tak_team_blue">Blue</string>
|
||||
<string name="tak_team_brown">Brown</string>
|
||||
<string name="tak_team_cyan">Cyan</string>
|
||||
<string name="tak_team_dark_blue">Dark Blue</string>
|
||||
<string name="tak_team_dark_green">Dark Green</string>
|
||||
<string name="tak_team_green">Green</string>
|
||||
<string name="tak_team_magenta">Magenta</string>
|
||||
<string name="tak_team_maroon">Maroon</string>
|
||||
<string name="tak_team_orange">Orange</string>
|
||||
<string name="tak_team_purple">Purple</string>
|
||||
<string name="tak_team_red">Red</string>
|
||||
<string name="tak_team_teal">Teal</string>
|
||||
<string name="tak_team_unspecified_color">Unspecified</string>
|
||||
<string name="tak_team_white">White</string>
|
||||
<string name="tak_team_yellow">Yellow</string>
|
||||
<string name="telemetry">Telemetry</string>
|
||||
<string name="telemetry_config">Telemetry Config</string>
|
||||
<string name="temperature">Temp</string>
|
||||
|
||||
+11
-2
@@ -44,6 +44,8 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.schemaLabelRes
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
@@ -59,14 +61,18 @@ fun <T : Enum<T>> DropDownPreference(
|
||||
itemColor: @Composable ((T) -> Color)? = null,
|
||||
itemLabel: @Composable ((T) -> String)? = null,
|
||||
) {
|
||||
// A deprecated value the radio currently holds stays on the list: dropping it leaves the field showing nothing and
|
||||
// writes a different value on the next save.
|
||||
val enumConstants =
|
||||
remember(selectedItem) {
|
||||
enumEntriesOf(selectedItem).filter { it.name != "UNRECOGNIZED" && !it.isDeprecatedEnumEntry() }
|
||||
enumEntriesOf(selectedItem).filter {
|
||||
it.name != "UNRECOGNIZED" && (it == selectedItem || !it.isDeprecatedEnumEntry())
|
||||
}
|
||||
}
|
||||
|
||||
val items =
|
||||
enumConstants.map {
|
||||
val label = itemLabel?.invoke(it) ?: it.name
|
||||
val label = itemLabel?.invoke(it) ?: it.schemaLabel()
|
||||
val icon = itemIcon?.invoke(it)
|
||||
val color = itemColor?.invoke(it)
|
||||
DropDownItem(it, label, icon, color)
|
||||
@@ -218,6 +224,9 @@ internal expect fun <T : Enum<T>> enumEntriesOf(selectedItem: T): List<T>
|
||||
|
||||
internal expect fun Enum<*>.isDeprecatedEnumEntry(): Boolean
|
||||
|
||||
/** The label the schema gives this value, falling back to the constant's name where the schema does not name it. */
|
||||
@Composable private fun Enum<*>.schemaLabel(): String = schemaLabelRes()?.let { stringResource(it) } ?: name
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DropDownPreferencePreview() {
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2026 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.component
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.v2.runComposeUiTest
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import kotlin.test.Test
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class DropDownPreferenceUiTest {
|
||||
|
||||
@Test
|
||||
fun deprecatedValueTheRadioHoldsStaysOnTheList() = runComposeUiTest {
|
||||
setContent {
|
||||
DropDownPreference(
|
||||
title = "Bitrate",
|
||||
enabled = true,
|
||||
selectedItem = ModuleConfig.AudioConfig.Audio_Baud.CODEC2_700,
|
||||
onItemSelected = {},
|
||||
)
|
||||
}
|
||||
|
||||
onNodeWithText("CODEC2_700").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun valueTheSchemaNamesIsShownByItsLabel() = runComposeUiTest {
|
||||
setContent {
|
||||
DropDownPreference(
|
||||
title = "Rebroadcast Mode",
|
||||
enabled = true,
|
||||
selectedItem = Config.DeviceConfig.RebroadcastMode.LOCAL_ONLY,
|
||||
onItemSelected = {},
|
||||
)
|
||||
}
|
||||
|
||||
onNodeWithText("Local Only").assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
-1
@@ -86,7 +86,6 @@ fun AudioConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.schema_audio_bitrate),
|
||||
enabled = state.connected,
|
||||
items = ModuleConfig.AudioConfig.Audio_Baud.entries.map { it to it.name },
|
||||
selectedItem = formState.value.bitrate,
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.bitrate = it }.build()
|
||||
|
||||
-3
@@ -144,7 +144,6 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.schema_cannedmessage_inputbroker_event_press),
|
||||
enabled = state.connected,
|
||||
items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name },
|
||||
selectedItem = formState.value.inputbroker_event_press,
|
||||
onItemSelected = {
|
||||
formState.value =
|
||||
@@ -155,7 +154,6 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.schema_cannedmessage_inputbroker_event_cw),
|
||||
enabled = state.connected,
|
||||
items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name },
|
||||
selectedItem = formState.value.inputbroker_event_cw,
|
||||
onItemSelected = {
|
||||
formState.value =
|
||||
@@ -166,7 +164,6 @@ fun CannedMessageConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Uni
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.schema_cannedmessage_inputbroker_event_ccw),
|
||||
enabled = state.connected,
|
||||
items = ModuleConfig.CannedMessageConfig.InputEventChar.entries.map { it to it.name },
|
||||
selectedItem = formState.value.inputbroker_event_ccw,
|
||||
onItemSelected = {
|
||||
formState.value =
|
||||
|
||||
-1
@@ -147,7 +147,6 @@ fun DetectionSensorConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> U
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.schema_detectionsensor_detection_trigger_type),
|
||||
enabled = state.connected,
|
||||
items = ModuleConfig.DetectionSensorConfig.TriggerType.entries.map { it to it.name },
|
||||
selectedItem = formState.value.detection_trigger_type,
|
||||
onItemSelected = {
|
||||
formState.value =
|
||||
|
||||
+3
-59
@@ -50,8 +50,8 @@ import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.schemaDescriptionRes
|
||||
import org.meshtastic.core.model.util.isDebug
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.accept
|
||||
@@ -73,26 +73,7 @@ import org.meshtastic.core.resources.i_know_what_i_m_doing
|
||||
import org.meshtastic.core.resources.led_heartbeat
|
||||
import org.meshtastic.core.resources.no_files_manifested
|
||||
import org.meshtastic.core.resources.options
|
||||
import org.meshtastic.core.resources.rebroadcast_mode_all_desc
|
||||
import org.meshtastic.core.resources.rebroadcast_mode_all_skip_decoding_desc
|
||||
import org.meshtastic.core.resources.rebroadcast_mode_core_portnums_only_desc
|
||||
import org.meshtastic.core.resources.rebroadcast_mode_known_only_desc
|
||||
import org.meshtastic.core.resources.rebroadcast_mode_local_only_desc
|
||||
import org.meshtastic.core.resources.rebroadcast_mode_none_desc
|
||||
import org.meshtastic.core.resources.role
|
||||
import org.meshtastic.core.resources.role_client_base_desc
|
||||
import org.meshtastic.core.resources.role_client_desc
|
||||
import org.meshtastic.core.resources.role_client_hidden_desc
|
||||
import org.meshtastic.core.resources.role_client_mute_desc
|
||||
import org.meshtastic.core.resources.role_lost_and_found_desc
|
||||
import org.meshtastic.core.resources.role_repeater_desc
|
||||
import org.meshtastic.core.resources.role_router_client_desc
|
||||
import org.meshtastic.core.resources.role_router_desc
|
||||
import org.meshtastic.core.resources.role_router_late_desc
|
||||
import org.meshtastic.core.resources.role_sensor_desc
|
||||
import org.meshtastic.core.resources.role_tak_desc
|
||||
import org.meshtastic.core.resources.role_tak_tracker_desc
|
||||
import org.meshtastic.core.resources.role_tracker_desc
|
||||
import org.meshtastic.core.resources.router_role_confirmation_text
|
||||
import org.meshtastic.core.resources.schema_device_button_gpio
|
||||
import org.meshtastic.core.resources.schema_device_buzzer_gpio
|
||||
@@ -119,42 +100,6 @@ import org.meshtastic.proto.Config
|
||||
|
||||
@Composable expect fun rememberSystemTimeZonePosixString(): String
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private val Config.DeviceConfig.Role.description: StringResource
|
||||
get() =
|
||||
when (this) {
|
||||
Config.DeviceConfig.Role.CLIENT -> Res.string.role_client_desc
|
||||
Config.DeviceConfig.Role.CLIENT_BASE -> Res.string.role_client_base_desc
|
||||
Config.DeviceConfig.Role.CLIENT_MUTE -> Res.string.role_client_mute_desc
|
||||
Config.DeviceConfig.Role.ROUTER -> Res.string.role_router_desc
|
||||
Config.DeviceConfig.Role.ROUTER_CLIENT -> Res.string.role_router_client_desc
|
||||
Config.DeviceConfig.Role.REPEATER -> Res.string.role_repeater_desc
|
||||
Config.DeviceConfig.Role.TRACKER -> Res.string.role_tracker_desc
|
||||
Config.DeviceConfig.Role.SENSOR -> Res.string.role_sensor_desc
|
||||
Config.DeviceConfig.Role.TAK -> Res.string.role_tak_desc
|
||||
Config.DeviceConfig.Role.CLIENT_HIDDEN -> Res.string.role_client_hidden_desc
|
||||
Config.DeviceConfig.Role.LOST_AND_FOUND -> Res.string.role_lost_and_found_desc
|
||||
Config.DeviceConfig.Role.TAK_TRACKER -> Res.string.role_tak_tracker_desc
|
||||
Config.DeviceConfig.Role.ROUTER_LATE -> Res.string.role_router_late_desc
|
||||
}
|
||||
|
||||
private val Config.DeviceConfig.RebroadcastMode.description: StringResource
|
||||
get() =
|
||||
when (this) {
|
||||
Config.DeviceConfig.RebroadcastMode.ALL -> Res.string.rebroadcast_mode_all_desc
|
||||
|
||||
Config.DeviceConfig.RebroadcastMode.ALL_SKIP_DECODING -> Res.string.rebroadcast_mode_all_skip_decoding_desc
|
||||
|
||||
Config.DeviceConfig.RebroadcastMode.LOCAL_ONLY -> Res.string.rebroadcast_mode_local_only_desc
|
||||
|
||||
Config.DeviceConfig.RebroadcastMode.KNOWN_ONLY -> Res.string.rebroadcast_mode_known_only_desc
|
||||
|
||||
Config.DeviceConfig.RebroadcastMode.NONE -> Res.string.rebroadcast_mode_none_desc
|
||||
|
||||
Config.DeviceConfig.RebroadcastMode.CORE_PORTNUMS_ONLY ->
|
||||
Res.string.rebroadcast_mode_core_portnums_only_desc
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION", "LongMethod")
|
||||
@Composable
|
||||
fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
@@ -197,9 +142,8 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit
|
||||
enabled = state.connected,
|
||||
selectedItem = currentRole,
|
||||
onItemSelected = { selectedRole = it },
|
||||
summary = stringResource(currentRole.description),
|
||||
summary = currentRole.schemaDescriptionRes()?.let { stringResource(it) },
|
||||
itemIcon = { MeshtasticIcons.role(it) },
|
||||
itemLabel = { it.name },
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
@@ -212,7 +156,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.rebroadcast_mode = it }.build()
|
||||
},
|
||||
summary = stringResource(currentRebroadcastMode.description),
|
||||
summary = currentRebroadcastMode.schemaDescriptionRes()?.let { stringResource(it) },
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
-4
@@ -129,7 +129,6 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
title = stringResource(Res.string.schema_display_units),
|
||||
summary = stringResource(Res.string.schema_display_units_description),
|
||||
enabled = state.connected,
|
||||
items = Config.DisplayConfig.DisplayUnits.entries.map { it to it.name },
|
||||
selectedItem = formState.value.units,
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.units = it }.build()
|
||||
@@ -199,7 +198,6 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
title = stringResource(Res.string.schema_display_displaymode),
|
||||
summary = stringResource(Res.string.schema_display_displaymode_description),
|
||||
enabled = state.connected,
|
||||
items = Config.DisplayConfig.DisplayMode.entries.map { it to it.name },
|
||||
selectedItem = formState.value.displaymode,
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.displaymode = it }.build()
|
||||
@@ -210,7 +208,6 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
title = stringResource(Res.string.schema_display_oled),
|
||||
summary = stringResource(Res.string.schema_display_oled_description),
|
||||
enabled = state.connected,
|
||||
items = Config.DisplayConfig.OledType.entries.map { it to it.name },
|
||||
selectedItem = formState.value.oled,
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.oled = it }.build()
|
||||
@@ -220,7 +217,6 @@ fun DisplayConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.schema_display_compass_orientation),
|
||||
enabled = state.connected,
|
||||
items = Config.DisplayConfig.CompassOrientation.entries.map { it to it.name },
|
||||
selectedItem = formState.value.compass_orientation,
|
||||
onItemSelected = {
|
||||
formState.value =
|
||||
|
||||
-1
@@ -331,7 +331,6 @@ fun NetworkConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit, onO
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.address_mode = it }.build()
|
||||
},
|
||||
itemLabel = { it.name },
|
||||
)
|
||||
if (formState.value.address_mode == Config.NetworkConfig.AddressMode.STATIC) {
|
||||
HorizontalDivider()
|
||||
|
||||
-1
@@ -300,7 +300,6 @@ fun PositionConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Un
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.schema_position_gps_mode),
|
||||
enabled = state.connected,
|
||||
items = Config.PositionConfig.GpsMode.entries.map { it to it.name },
|
||||
selectedItem = formState.value.gps_mode,
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.gps_mode = it }.build()
|
||||
|
||||
-2
@@ -108,7 +108,6 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.schema_serial_baud),
|
||||
enabled = state.connected,
|
||||
items = ModuleConfig.SerialConfig.Serial_Baud.entries.map { it to it.name },
|
||||
selectedItem = formState.value.baud,
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.baud = it }.build()
|
||||
@@ -128,7 +127,6 @@ fun SerialConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
|
||||
DropDownPreference(
|
||||
title = stringResource(Res.string.schema_serial_mode),
|
||||
enabled = state.connected,
|
||||
items = ModuleConfig.SerialConfig.Serial_Mode.entries.map { it to it.name },
|
||||
selectedItem = formState.value.mode,
|
||||
onItemSelected = {
|
||||
formState.value = formState.value.newBuilder().also { wb -> wb.mode = it }.build()
|
||||
|
||||
-3
@@ -55,7 +55,6 @@ import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.model.Capabilities
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.getColorFrom
|
||||
import org.meshtastic.core.model.getStringResFrom
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.TakPrefs
|
||||
@@ -174,7 +173,6 @@ internal fun TakConfigCard(
|
||||
title = stringResource(Res.string.tak_team),
|
||||
enabled = enabled,
|
||||
selectedItem = team,
|
||||
itemLabel = { stringResource(getStringResFrom(it)) },
|
||||
itemColor = { Color(getColorFrom(it)) },
|
||||
onItemSelected = onTeamSelected,
|
||||
)
|
||||
@@ -183,7 +181,6 @@ internal fun TakConfigCard(
|
||||
title = stringResource(Res.string.tak_role),
|
||||
enabled = enabled,
|
||||
selectedItem = role,
|
||||
itemLabel = { stringResource(getStringResFrom(it)) },
|
||||
onItemSelected = onRoleSelected,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (c) 2026 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.schemastrings
|
||||
|
||||
/**
|
||||
* The Kotlin half of the sync: a lookup from an enum value to the resource holding its schema label, so a picker shows
|
||||
* the label every client shares instead of the constant's Kotlin name.
|
||||
*
|
||||
* The keys these build are [SchemaCatalog.keyFor]'s, so this file and `schema_strings.xml` cannot name a value
|
||||
* differently. What the lookup cannot do is fail at compile time: a resource that is not there reads as null, which is
|
||||
* why `SchemaEnumLabelsTest` walks every labelled constant.
|
||||
*/
|
||||
object EnumLabelsKt {
|
||||
|
||||
/** One enum the schema labels: its proto path, the prefix its values share, and whether any value is described. */
|
||||
data class LabelledEnum(val path: String, val prefix: String, val hasDescriptions: Boolean)
|
||||
|
||||
/**
|
||||
* The whole generated file. Spotless reformats it afterwards, so this is the input to the formatter rather than the
|
||||
* committed bytes - which is why `RepositorySyncTest` compares the branches it declares, not its text. The notice
|
||||
* sits below the imports because the licence-header step deletes anything between the header and `package`.
|
||||
*/
|
||||
fun render(header: String, pin: String, enums: List<LabelledEnum>): String = buildString {
|
||||
append(header)
|
||||
append("package $PACKAGE\n\n")
|
||||
imports(enums).forEach { append("import ").append(it).append('\n') }
|
||||
append('\n').append(notice(pin)).append('\n')
|
||||
append(LABEL_DOC)
|
||||
append("@Suppress(\"CyclomaticComplexMethod\")\n")
|
||||
append("fun Enum<*>.schemaLabelRes(): StringResource? = when (this) {\n")
|
||||
enums.forEach { append(branch(it, "schemaLabel")) }
|
||||
append(" else -> null\n}\n")
|
||||
append(DESCRIPTION_DOC)
|
||||
append("@Suppress(\"CyclomaticComplexMethod\")\n")
|
||||
append("fun Enum<*>.schemaDescriptionRes(): StringResource? = when (this) {\n")
|
||||
enums.filter { it.hasDescriptions }.forEach { append(branch(it, "schemaDescription")) }
|
||||
append(" else -> null\n}\n")
|
||||
append(HELPER)
|
||||
}
|
||||
|
||||
/**
|
||||
* One `when` branch. A branch that would not fit on a line is an error rather than a wrap: a wrapped branch makes
|
||||
* the formatter space out every other branch in the same `when`, and the fix belongs in the name, not here.
|
||||
*/
|
||||
private fun branch(enum: LabelledEnum, function: String): String {
|
||||
val line = " is ${enum.path} -> $function(\"${enum.prefix}\")"
|
||||
require(line.length <= MAX_LINE) { "branch for ${enum.path} is ${line.length} characters, over $MAX_LINE" }
|
||||
return "$line\n"
|
||||
}
|
||||
|
||||
/** `is Config.DeviceConfig.Role -> ...`, as the committed file spells it, for the drift check. */
|
||||
val branchPattern: Regex =
|
||||
Regex("""^\s*is ([\w.]+) -> schema(Label|Description)\("([^"]+)"\)""", RegexOption.MULTILINE)
|
||||
|
||||
/** `Config.DeviceConfig.Role` needs only `Config` imported; a top-level enum needs itself. */
|
||||
private fun imports(enums: List<LabelledEnum>): List<String> =
|
||||
(FIXED_IMPORTS + enums.map { "$PROTO_PACKAGE.${it.path.substringBefore('.')}" }).distinct().sorted()
|
||||
|
||||
fun notice(pin: String): String =
|
||||
"// Generated by ./gradlew :schema-strings:sync from org.meshtastic:protobufs $pin.\n" +
|
||||
"// Do not edit: change the schema, then run sync."
|
||||
|
||||
private const val MAX_LINE = 120
|
||||
private const val PACKAGE = "org.meshtastic.core.model"
|
||||
private const val PROTO_PACKAGE = "org.meshtastic.proto"
|
||||
|
||||
private val FIXED_IMPORTS =
|
||||
listOf(
|
||||
"org.jetbrains.compose.resources.StringResource",
|
||||
"org.meshtastic.core.resources.Res",
|
||||
"org.meshtastic.core.resources.allStringResources",
|
||||
)
|
||||
|
||||
private val LABEL_DOC =
|
||||
"""
|
||||
|
|
||||
|/**
|
||||
| * The schema's label for this enum value, or null where the schema does not name it. A picker shows this in
|
||||
| * place of the constant's Kotlin name, and falls back to that name when it is null, so a value the schema has
|
||||
| * not reached still renders.
|
||||
| */
|
||||
|"""
|
||||
.trimMargin()
|
||||
|
||||
private val DESCRIPTION_DOC =
|
||||
"""
|
||||
|
|
||||
|/** The schema's one-sentence explanation of this enum value, or null where it has none. */
|
||||
|"""
|
||||
.trimMargin()
|
||||
|
||||
private val HELPER =
|
||||
"""
|
||||
|
|
||||
|private fun Enum<*>.schemaLabel(prefix: String): StringResource? =
|
||||
| Res.allStringResources[prefix + name.lowercase()]
|
||||
|
|
||||
|private fun Enum<*>.schemaDescription(prefix: String): StringResource? =
|
||||
| Res.allStringResources[prefix + name.lowercase() + "_description"]
|
||||
|"""
|
||||
.trimMargin()
|
||||
}
|
||||
@@ -73,6 +73,22 @@ object SchemaCatalog {
|
||||
return out
|
||||
}
|
||||
|
||||
/** Every enum the schema labels, in proto-path order, for the Kotlin accessors that read those labels. */
|
||||
fun labelledEnums(): List<EnumLabelsKt.LabelledEnum> = generatedTypePaths().mapNotNull { path ->
|
||||
val type = Class.forName(GENERATED_PACKAGE + path.replace('.', '$'))
|
||||
val constants = type.enumConstants ?: return@mapNotNull null
|
||||
val metadata =
|
||||
constants.filterIsInstance<WireEnum>().mapNotNull {
|
||||
FieldMetadataRegistry.forEnumValue(PROTO_PACKAGE + path, it.value)
|
||||
}
|
||||
if (metadata.none { !it.label.isNullOrBlank() }) return@mapNotNull null
|
||||
EnumLabelsKt.LabelledEnum(
|
||||
path = path,
|
||||
prefix = keyFor(path, ""),
|
||||
hasDescriptions = metadata.any { !it.description.isNullOrBlank() },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The resource for a field or enum value. Each message segment is lowercased with a trailing `Config` dropped, and
|
||||
* the `Config`/`ModuleConfig` container is dropped: `ModuleConfig.MQTTConfig.address` is `schema_mqtt_address`.
|
||||
|
||||
@@ -17,13 +17,15 @@
|
||||
package org.meshtastic.schemastrings
|
||||
|
||||
import java.io.File
|
||||
import java.time.Year
|
||||
|
||||
/** The repository view of the sync: where the files live, what the English file should contain, how to write it. */
|
||||
class SchemaStringsSync(rootDir: File) {
|
||||
class SchemaStringsSync(private val rootDir: File) {
|
||||
private val valuesRoot = rootDir.resolve("core/resources/src/commonMain/composeResources")
|
||||
private val catalog = rootDir.resolve("gradle/libs.versions.toml")
|
||||
val englishStrings: File = valuesRoot.resolve("values/$HAND_WRITTEN")
|
||||
val englishSchemaStrings: File = valuesRoot.resolve("values/$GENERATED")
|
||||
val enumLabels: File = rootDir.resolve("core/model/src/commonMain/kotlin/$GENERATED_KT")
|
||||
|
||||
/** Every hand-written strings file: the English one and one per locale. */
|
||||
val handWrittenFiles: List<File>
|
||||
@@ -51,6 +53,25 @@ class SchemaStringsSync(rootDir: File) {
|
||||
notice = notice(catalogPin),
|
||||
)
|
||||
|
||||
/**
|
||||
* The Kotlin accessors the registry implies. The licence header is the committed file's own where there is one, so
|
||||
* a rerun does not restamp a year Spotless is happy with - everything before `package` is that header.
|
||||
*/
|
||||
fun expectedEnumLabels(): String = EnumLabelsKt.render(
|
||||
header = enumLabels.takeIf { it.isFile }?.let { licenceHeader(it.readText()) } ?: freshLicenceHeader(),
|
||||
pin = catalogPin,
|
||||
enums = SchemaCatalog.labelledEnums(),
|
||||
)
|
||||
|
||||
/** The pin the committed Kotlin file says it was built from, or null when it is absent or unmarked. */
|
||||
val recordedEnumLabelsPin: String?
|
||||
get() = enumLabels.takeIf { it.isFile }?.let { noticePin.find(it.readText())?.groupValues?.get(1) }
|
||||
|
||||
private fun licenceHeader(kotlin: String): String = kotlin.substringBefore("package ").substringBefore("// ")
|
||||
|
||||
private fun freshLicenceHeader(): String =
|
||||
rootDir.resolve("config/spotless/copyright.kt").readText().replace("\$YEAR", YEAR) + "\n"
|
||||
|
||||
/** Schema-named keys that someone wrote into a hand-written file, by file. Those files must never carry one. */
|
||||
fun strays(): Map<File, Set<String>> = handWrittenFiles
|
||||
.associateWith { file -> schemaKeys(StringsXml.bodies(file.readText()).keys) }
|
||||
@@ -59,7 +80,9 @@ class SchemaStringsSync(rootDir: File) {
|
||||
fun apply(): String {
|
||||
val text = expectedEnglish()
|
||||
englishSchemaStrings.writeText(text)
|
||||
return "wrote ${englishSchemaStrings.path} (${StringsXml.bodies(text).size} strings from protobufs $catalogPin)"
|
||||
enumLabels.writeText(expectedEnumLabels())
|
||||
return "wrote ${StringsXml.bodies(text).size} strings and " +
|
||||
"${SchemaCatalog.labelledEnums().size} enum accessors from protobufs $catalogPin"
|
||||
}
|
||||
|
||||
private fun schemaKeys(names: Set<String>): Set<String> = names.filterTo(HashSet()) { it.startsWith("schema_") }
|
||||
@@ -67,6 +90,8 @@ class SchemaStringsSync(rootDir: File) {
|
||||
companion object {
|
||||
const val HAND_WRITTEN = "strings.xml"
|
||||
const val GENERATED = "schema_strings.xml"
|
||||
const val GENERATED_KT = "org/meshtastic/core/model/SchemaEnumLabels.kt"
|
||||
private val YEAR = Year.now().toString()
|
||||
|
||||
private val catalogPinLine = Regex("""^meshtastic-protobufs = "([^"]+)"$""", RegexOption.MULTILINE)
|
||||
private val noticePin = Regex("""from org\.meshtastic:protobufs (\S+)\.""")
|
||||
|
||||
@@ -51,6 +51,54 @@ class RepositorySyncTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the generated Kotlin records the pin it was built from`() {
|
||||
assertNotNull(
|
||||
sync.recordedEnumLabelsPin,
|
||||
"core/model SchemaEnumLabels.kt carries no pin: run ./gradlew :schema-strings:sync",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the accessors the file declares rather than its bytes: Spotless reformats what the sync writes, so its
|
||||
* text is the formatter's and only the set of enums and prefixes is the sync's to promise.
|
||||
*/
|
||||
@Test
|
||||
fun `at a matching pin the generated Kotlin declares the accessors the registry implies`() {
|
||||
if (sync.recordedEnumLabelsPin != sync.catalogPin) {
|
||||
println("protobufs moved to ${sync.catalogPin}; scheduled-updates re-syncs SchemaEnumLabels.kt")
|
||||
return
|
||||
}
|
||||
val committed =
|
||||
EnumLabelsKt.branchPattern
|
||||
.findAll(sync.enumLabels.readText())
|
||||
.map { Triple(it.groupValues[1], it.groupValues[2], it.groupValues[3]) }
|
||||
.toSet()
|
||||
val expected =
|
||||
SchemaCatalog.labelledEnums()
|
||||
.flatMap { enum ->
|
||||
listOfNotNull(
|
||||
Triple(enum.path, "Label", enum.prefix),
|
||||
Triple(enum.path, "Description", enum.prefix).takeIf { enum.hasDescriptions },
|
||||
)
|
||||
}
|
||||
.toSet()
|
||||
|
||||
assertEquals(expected, committed, "core/model SchemaEnumLabels.kt is stale: run ./gradlew :schema-strings:sync")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every labelled enum builds a prefix the generated strings carry`() {
|
||||
val strings = StringsXml.bodies(sync.englishSchemaStrings.readText()).keys
|
||||
val missing = SchemaCatalog.labelledEnums().filter { enum -> strings.none { it.startsWith(enum.prefix) } }
|
||||
|
||||
assertTrue(
|
||||
missing.isEmpty(),
|
||||
"these enums build a resource prefix nothing in schema_strings.xml matches: " +
|
||||
missing.joinToString { "${it.path} (${it.prefix})" },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no schema key is written by hand`() {
|
||||
val strays = sync.strays()
|
||||
|
||||
Reference in new issue
Block a user