Clean up descriptive text to be the same font and color on all the config views

This commit is contained in:
Garth Vander Houwen 2024-02-21 20:41:27 -08:00
parent 07768d98cd
commit a5d4f62ca4
9 changed files with 364 additions and 277 deletions

View file

@ -26,7 +26,7 @@ struct AppSettings: View {
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if provideLocation {
Toggle(isOn: $enableSmartPosition) {
Label("appsettings.smartposition", systemImage: "brain.fill")
Label("appsettings.smartposition", systemImage: "brain")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
VStack {

View file

@ -33,26 +33,28 @@ struct DeviceConfig: View {
ConfigHeader(title: "Device", config: \.deviceConfig, node: node, onAppear: setDeviceValues)
Section(header: Text("options")) {
Picker("Device Role", selection: $deviceRole ) {
ForEach(DeviceRoles.allCases) { dr in
Text(dr.name)
VStack(alignment: .leading) {
Picker("Device Role", selection: $deviceRole ) {
ForEach(DeviceRoles.allCases) { dr in
Text(dr.name)
}
}
.pickerStyle(DefaultPickerStyle())
Text(DeviceRoles(rawValue: deviceRole)?.description ?? "")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
.padding(.top, 10)
Text(DeviceRoles(rawValue: deviceRole)?.description ?? "")
.foregroundColor(.gray)
.font(.caption)
Picker("Rebroadcast Mode", selection: $rebroadcastMode ) {
ForEach(RebroadcastModes.allCases) { rm in
Text(rm.name)
VStack(alignment: .leading) {
Picker("Rebroadcast Mode", selection: $rebroadcastMode ) {
ForEach(RebroadcastModes.allCases) { rm in
Text(rm.name)
}
}
.pickerStyle(DefaultPickerStyle())
Text(RebroadcastModes(rawValue: rebroadcastMode)?.description ?? "")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
.padding(.top, 10)
Text(RebroadcastModes(rawValue: rebroadcastMode)?.description ?? "")
.foregroundColor(.gray)
.font(.caption)
Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) {
ForEach(UpdateIntervals.allCases) { ui in
if ui.rawValue >= 3600 {
@ -61,20 +63,24 @@ struct DeviceConfig: View {
}
}
.pickerStyle(DefaultPickerStyle())
.padding(.top, 10)
Toggle(isOn: $doubleTapAsButtonPress) {
Label("Double Tap as Button", systemImage: "hand.tap")
VStack(alignment: .leading) {
Toggle(isOn: $doubleTapAsButtonPress) {
Label("Double Tap as Button", systemImage: "hand.tap")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Treat double tap on supported accelerometers as a user button press.")
.foregroundColor(.gray)
.font(.caption)
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Treat double tap on supported accelerometers as a user button press.")
.font(.caption)
Toggle(isOn: $isManaged) {
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
VStack(alignment: .leading) {
Toggle(isOn: $isManaged) {
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.")
.foregroundColor(.gray)
.font(.caption)
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.")
.font(.caption)
}
Section(header: Text("Debug")) {
Toggle(isOn: $serialEnabled) {

View file

@ -31,83 +31,108 @@ struct DisplayConfig: View {
ConfigHeader(title: "Display", config: \.displayConfig, node: node, onAppear: setDisplayValues)
Section(header: Text("Device Screen")) {
Picker("Display Mode", selection: $displayMode ) {
ForEach(DisplayModes.allCases) { dm in
Text(dm.description)
VStack(alignment: .leading) {
Picker("Display Mode", selection: $displayMode ) {
ForEach(DisplayModes.allCases) { dm in
Text(dm.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Override automatic OLED screen detection.")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
Text("Override automatic OLED screen detection.")
.font(.caption)
Toggle(isOn: $compassNorthTop) {
Label("Always point north", systemImage: "location.north.circle")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("The compass heading on the screen outside of the circle will always point north.")
.font(.caption)
Toggle(isOn: $wakeOnTapOrMotion) {
Label("Wake Screen on tap or motion", systemImage: "gyroscope")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Requires that there be an accelerometer on your device.")
.font(.caption)
Toggle(isOn: $flipScreen) {
Label("Flip Screen", systemImage: "pip.swap")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Flip screen vertically")
.font(.caption)
Picker("OLED Type", selection: $oledType ) {
ForEach(OledTypes.allCases) { ot in
Text(ot.description)
VStack(alignment: .leading) {
Toggle(isOn: $compassNorthTop) {
Label("Always point north", systemImage: "location.north.circle")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("The compass heading on the screen outside of the circle will always point north.")
.foregroundColor(.gray)
.font(.caption)
}
VStack(alignment: .leading) {
Toggle(isOn: $wakeOnTapOrMotion) {
Label("Wake Screen on tap or motion", systemImage: "gyroscope")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Requires that there be an accelerometer on your device.")
.foregroundColor(.gray)
.font(.caption)
}
VStack(alignment: .leading) {
Toggle(isOn: $flipScreen) {
Label("Flip Screen", systemImage: "pip.swap")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Flip screen vertically")
.foregroundColor(.gray)
.font(.caption)
}
VStack(alignment: .leading) {
Picker("OLED Type", selection: $oledType ) {
ForEach(OledTypes.allCases) { ot in
Text(ot.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Override automatic OLED screen detection.")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
Text("Override automatic OLED screen detection.")
.font(.caption)
}
Section(header: Text("Timing & Format")) {
Picker("Screen on for", selection: $screenOnSeconds ) {
ForEach(ScreenOnIntervals.allCases) { soi in
Text(soi.description)
VStack(alignment: .leading) {
Picker("Screen on for", selection: $screenOnSeconds ) {
ForEach(ScreenOnIntervals.allCases) { soi in
Text(soi.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("How long the screen remains on after the user button is pressed or messages are received.")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
Text("How long the screen remains on after the user button is pressed or messages are received.")
.font(.caption)
Picker("Carousel Interval", selection: $screenCarouselInterval ) {
ForEach(ScreenCarouselIntervals.allCases) { sci in
Text(sci.description)
VStack(alignment: .leading) {
Picker("Carousel Interval", selection: $screenCarouselInterval ) {
ForEach(ScreenCarouselIntervals.allCases) { sci in
Text(sci.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.")
.font(.caption)
Picker("GPS Format", selection: $gpsFormat ) {
ForEach(GpsFormats.allCases) { lu in
Text(lu.description)
VStack(alignment: .leading) {
Picker("GPS Format", selection: $gpsFormat ) {
ForEach(GpsFormats.allCases) { lu in
Text(lu.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("The format used to display GPS coordinates on the device screen.")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
Text("The format used to display GPS coordinates on the device screen.")
.font(.caption)
.listRowSeparator(.visible)
Picker("Display Units", selection: $units ) {
ForEach(Units.allCases) { un in
Text(un.description)
VStack(alignment: .leading) {
Picker("Display Units", selection: $units ) {
ForEach(Units.allCases) { un in
Text(un.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Units displayed on the device screen")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
Text("Units displayed on the device screen")
.font(.caption)
}
}
.disabled(self.bleManager.connectedPeripheral == nil || node?.displayConfig == nil)

View file

@ -57,32 +57,37 @@ struct LoRaConfig: View {
Section(header: Text("Options")) {
Picker("Region", selection: $region ) {
ForEach(RegionCodes.allCases) { r in
Text(r.description)
VStack(alignment: .leading) {
Picker("Region", selection: $region ) {
ForEach(RegionCodes.allCases) { r in
Text(r.description)
}
}
.pickerStyle(DefaultPickerStyle())
.fixedSize()
Text("The region where you will be using your radios.")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
.fixedSize()
Text("The region where you will be using your radios.")
.font(.caption)
Toggle(isOn: $usePreset) {
Label("Use Preset", systemImage: "list.bullet.rectangle")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if usePreset {
Picker("Presets", selection: $modemPreset ) {
ForEach(ModemPresets.allCases) { m in
Text(m.description)
VStack(alignment: .leading) {
Picker("Presets", selection: $modemPreset ) {
ForEach(ModemPresets.allCases) { m in
Text(m.description)
}
}
.pickerStyle(DefaultPickerStyle())
.fixedSize()
Text("Available modem presets, default is Long Fast.")
.foregroundColor(.gray)
.font(.caption)
}
.pickerStyle(DefaultPickerStyle())
.fixedSize()
Text("Available modem presets, default is Long Fast.")
.font(.caption)
}
}
Section(header: Text("Advanced")) {
@ -123,36 +128,40 @@ struct LoRaConfig: View {
}
}
}
Picker("Number of hops", selection: $hopLimit) {
ForEach(1..<8) {
Text("\($0)")
.tag($0 == 0 ? 3 : $0)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully.")
.font(.caption)
HStack {
Text("Frequency Slot")
.fixedSize()
TextField("Frequency Slot", value: $channelNum, formatter: formatter)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("dismiss.keyboard") {
focusedField = nil
}
.font(.subheadline)
}
VStack(alignment: .leading) {
Picker("Number of hops", selection: $hopLimit) {
ForEach(1..<8) {
Text("\($0)")
.tag($0 == 0 ? 3 : $0)
}
.keyboardType(.decimalPad)
.scrollDismissesKeyboard(.immediately)
.focused($focusedField, equals: .channelNum)
.disabled(overrideFrequency > 0.0)
}
.pickerStyle(DefaultPickerStyle())
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully.")
.foregroundColor(.gray)
.font(.caption)
}
VStack(alignment: .leading) {
HStack {
Text("Frequency Slot")
.fixedSize()
TextField("Frequency Slot", value: $channelNum, formatter: formatter)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("dismiss.keyboard") {
focusedField = nil
}
.font(.subheadline)
}
}
.keyboardType(.decimalPad)
.scrollDismissesKeyboard(.immediately)
.focused($focusedField, equals: .channelNum)
.disabled(overrideFrequency > 0.0)
}
Text("This determines the actual frequency you are transmitting on in the band. If set to 0 this value will be calculated automatically based on the primary channel name.")
.foregroundColor(.gray)
.font(.caption)
}
Text("This determines the actual frequency you are transmitting on in the band. If set to 0 this value will be calculated automatically based on the primary channel name.")
.font(.caption)
Toggle(isOn: $rxBoostedGain) {
Label("RX Boosted Gain", systemImage: "waveform.badge.plus")
}

View file

@ -31,10 +31,15 @@ struct NetworkConfig: View {
if (node != nil && node?.metadata?.hasWifi ?? false) {
Section(header: Text("WiFi Options")) {
Toggle(isOn: $wifiEnabled) {
Label("enabled", systemImage: "wifi")
VStack(alignment: .leading) {
Toggle(isOn: $wifiEnabled) {
Label("enabled", systemImage: "wifi")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Enabling WiFi will disable the bluetooth connection to the app.")
.foregroundColor(.gray)
.font(.caption)
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
HStack {
Label("ssid", systemImage: "network")
TextField("ssid", text: $wifiSsid)
@ -77,18 +82,20 @@ struct NetworkConfig: View {
.foregroundColor(.gray)
}
.keyboardType(.default)
Text("Enabling WiFi will disable the bluetooth connection to the app.")
.font(.callout)
}
}
if (node != nil && node?.metadata?.hasEthernet ?? false) {
Section(header: Text("Ethernet Options")) {
Toggle(isOn: $ethEnabled) {
Label("enabled", systemImage: "network")
VStack(alignment: .leading) {
Toggle(isOn: $ethEnabled) {
Label("enabled", systemImage: "network")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Enabling Ethernet will disable the bluetooth connection to the app.")
.foregroundColor(.gray)
.font(.caption)
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Enabling Ethernet will disable the bluetooth connection to the app.")
.font(.callout)
}
}
}

View file

@ -79,44 +79,55 @@ struct PositionConfig: View {
Section(header: Text("Position Packet")) {
Picker("Interval", selection: $positionBroadcastSeconds) {
ForEach(UpdateIntervals.allCases) { at in
if at.rawValue >= 900 {
Text(at.description)
}
}
}
.pickerStyle(DefaultPickerStyle())
Text("The maximum interval that can elapse without a node broadcasting a position")
.font(.caption)
Toggle(isOn: $smartPositionEnabled) {
Label("Smart Position", systemImage: "location.fill.viewfinder")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if smartPositionEnabled {
Picker("Minimum Broadcast Interval", selection: $broadcastSmartMinimumIntervalSecs) {
VStack(alignment: .leading) {
Picker("Broadcast Interval", selection: $positionBroadcastSeconds) {
ForEach(UpdateIntervals.allCases) { at in
Text(at.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("The fastest that position updates will be sent if the minimum distance has been satisfied")
.font(.caption)
Picker("Minimum Distance", selection: $broadcastSmartMinimumDistance) {
ForEach(10..<151) {
if $0 == 0 {
Text("unset")
} else {
if $0.isMultiple(of: 5) {
Text("\($0)")
.tag($0)
}
if at.rawValue >= 900 {
Text(at.description)
}
}
}
.pickerStyle(DefaultPickerStyle())
Text("The minimum distance change in meters to be considered for a smart position broadcast.")
.font(.caption)
Text("The maximum interval that can elapse without a node broadcasting a position")
.foregroundColor(.gray)
.font(.caption)
}
Toggle(isOn: $smartPositionEnabled) {
Label("Smart Position", systemImage: "brain")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if smartPositionEnabled {
VStack(alignment: .leading) {
Picker("Minimum Interval", selection: $broadcastSmartMinimumIntervalSecs) {
ForEach(UpdateIntervals.allCases) { at in
Text(at.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("The fastest that position updates will be sent if the minimum distance has been satisfied")
.foregroundColor(.gray)
.font(.caption)
}
VStack(alignment: .leading) {
Picker("Minimum Distance", selection: $broadcastSmartMinimumDistance) {
ForEach(10..<151) {
if $0 == 0 {
Text("unset")
} else {
if $0.isMultiple(of: 5) {
Text("\($0)")
.tag($0)
}
}
}
}
.pickerStyle(DefaultPickerStyle())
Text("The minimum distance change in meters to be considered for a smart position broadcast.")
.foregroundColor(.gray)
.font(.caption)
}
}
}
Section(header: Text("Device GPS")) {
@ -131,27 +142,33 @@ struct PositionConfig: View {
.padding(.bottom, 5)
if gpsMode == 1 {
Picker("Update Interval", selection: $gpsUpdateInterval) {
ForEach(GpsUpdateIntervals.allCases) { ui in
Text(ui.description)
VStack(alignment: .leading) {
Picker("Update Interval", selection: $gpsUpdateInterval) {
ForEach(GpsUpdateIntervals.allCases) { ui in
Text(ui.description)
}
}
Text("How often should we try to get a GPS position.")
.foregroundColor(.gray)
.font(.caption)
}
Text("How often should we try to get a GPS position.")
.font(.caption)
} else {
Toggle(isOn: $fixedPosition) {
Label("Fixed Position", systemImage: "location.square.fill")
VStack(alignment: .leading) {
Toggle(isOn: $fixedPosition) {
Label("Fixed Position", systemImage: "location.square.fill")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval. Fixed position will always use the most recent position the device has.")
.foregroundColor(.gray)
.font(.caption)
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval. Fixed position will always use the most recent position the device has.")
.font(.caption)
}
}
Section(header: Text("Position Flags")) {
Text("Optional fields to include when assembling position messages. the more fields are included, the larger the message will be - leading to longer airtime and a higher risk of packet loss")
.foregroundColor(.gray)
.font(.caption)
.listRowSeparator(.visible)
Toggle(isOn: $includeAltitude) {
Label("Altitude", systemImage: "arrow.up")

View file

@ -17,7 +17,9 @@ struct PowerConfig: View {
@State private var waitBluetoothSecs = 60
@State private var lsSecs = 300
@State private var minWakeSecs = 10
@State private var currentDevice: DeviceHardware?
@State private var hasChanges: Bool = false
@FocusState private var isFocused: Bool
@ -25,14 +27,16 @@ struct PowerConfig: View {
Form {
ConfigHeader(title: "Power", config: \.powerConfig, node: node, onAppear: setPowerValues)
Section(header: Text("power")) {
Section {
Toggle(isOn: $isPowerSaving) {
Text("power.save")
Text("power.solar")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Text("For use when powered from a low-current source in addition to the battery, minimizes power consumption as much as possible even if the deviced appears to be powered.")
.foregroundColor(.gray)
.font(.caption)
Section {
Toggle(isOn: $shutdownOnPowerLoss) {
Text("power.shutdown.on.power.loss")
}
@ -47,53 +51,56 @@ struct PowerConfig: View {
.pickerStyle(DefaultPickerStyle())
}
} header: {
Text("Shutdown")
Text("power")
}
if currentDevice?.architecture == .esp32 || currentDevice?.architecture == .esp32S3 {
Section {
Toggle(isOn: $adcOverride) {
Text("power.adc.override")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Section {
Toggle(isOn: $adcOverride) {
Text("power.adc.override")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if adcOverride {
HStack {
Text("power.adc.multiplier")
Spacer()
FloatField(title: "power.adc.multiplier", number: $adcMultiplier) {
(2.0 ... 6.0).contains($0)
if adcOverride {
HStack {
Text("power.adc.multiplier")
Spacer()
FloatField(title: "power.adc.multiplier", number: $adcMultiplier) {
(2.0 ... 6.0).contains($0)
}
.focused($isFocused)
Spacer()
}
.focused($isFocused)
Spacer()
}
} header: {
Text("Battery")
}
} header: {
Text("Battery")
}
Section {
Picker("power.wait.bluetooth.secs", selection: $waitBluetoothSecs) {
ForEach(PowerIntervals.allCases) {
Text($0.description)
Section {
Picker("power.wait.bluetooth.secs", selection: $waitBluetoothSecs) {
ForEach(PowerIntervals.allCases) {
Text($0.description)
}
}
}
.pickerStyle(DefaultPickerStyle())
Picker("power.ls.secs", selection: $lsSecs) {
ForEach(PowerIntervals.allCases) {
Text($0.description)
.pickerStyle(DefaultPickerStyle())
Picker("power.ls.secs", selection: $lsSecs) {
ForEach(PowerIntervals.allCases) {
Text($0.description)
}
}
}
.pickerStyle(DefaultPickerStyle())
Picker("power.min.wake.secs", selection: $minWakeSecs) {
ForEach(PowerIntervals.allCases) {
Text($0.description)
.pickerStyle(DefaultPickerStyle())
Picker("power.min.wake.secs", selection: $minWakeSecs) {
ForEach(PowerIntervals.allCases) {
Text($0.description)
}
}
.pickerStyle(DefaultPickerStyle())
} header: {
Text("Sleep")
}
.pickerStyle(DefaultPickerStyle())
} header: {
Text("Sleep")
}
}
.disabled(self.bleManager.connectedPeripheral == nil || node?.powerConfig == nil)
@ -118,6 +125,16 @@ struct PowerConfig: View {
if self.bleManager.context == nil {
self.bleManager.context = context
}
Api().loadDeviceHardwareData { (hw) in
for device in hw {
let currentHardware = node?.user?.hwModel ?? "UNSET"
let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "")
if deviceString == currentHardware {
currentDevice = device
}
}
}
setPowerValues()
// Need to request a Power config from the remote node before allowing changes

View file

@ -42,53 +42,59 @@ struct UserConfig: View {
VStack {
Form {
Section(header: Text("User Details")) {
HStack {
Label(isLicensed ? "Call Sign" : "Long Name", systemImage: "person.crop.rectangle.fill")
VStack(alignment: .leading) {
HStack {
Label(isLicensed ? "Call Sign" : "Long Name", systemImage: "person.crop.rectangle.fill")
TextField("Long Name", text: $longName)
.onChange(of: longName, perform: { _ in
let totalBytes = longName.utf8.count
// Only mess with the value if it is too big
if totalBytes > (isLicensed ? 6 : 36) {
let firstNBytes = Data(longName.utf8.prefix(isLicensed ? 6 : 36))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the longName back to the last place where it was the right size
longName = maxBytesString
}
}
})
}
.keyboardType(.default)
.disableAutocorrection(true)
if longName.isEmpty && isLicensed {
Label("Call Sign must not be empty", systemImage: "exclamationmark.square")
.foregroundColor(.red)
}
Text("\(String(isLicensed ? "Call Sign" : "Long Name")) can be up to \(isLicensed ? "8" : "36") bytes long.")
.foregroundColor(.gray)
.font(.caption)
TextField("Long Name", text: $longName)
.onChange(of: longName, perform: { _ in
let totalBytes = longName.utf8.count
// Only mess with the value if it is too big
if totalBytes > (isLicensed ? 6 : 36) {
let firstNBytes = Data(longName.utf8.prefix(isLicensed ? 6 : 36))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the longName back to the last place where it was the right size
longName = maxBytesString
}
VStack(alignment: .leading) {
HStack {
Label("Short Name", systemImage: "circlebadge.fill")
TextField("Short Name", text: $shortName)
.foregroundColor(.gray)
.onChange(of: shortName, perform: { _ in
let totalBytes = shortName.utf8.count
// Only mess with the value if it is too big
if totalBytes > 4 {
let firstNBytes = Data(shortName.utf8.prefix(4))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
shortName = maxBytesString
}
}
}
})
}
.keyboardType(.default)
.disableAutocorrection(true)
if longName.isEmpty && isLicensed {
Label("Call Sign must not be empty", systemImage: "exclamationmark.square")
.foregroundColor(.red)
}
Text("\(String(isLicensed ? "Call Sign" : "Long Name")) can be up to \(isLicensed ? "8" : "36") bytes long.")
.font(.caption2)
HStack {
Label("Short Name", systemImage: "circlebadge.fill")
TextField("Short Name", text: $shortName)
.foregroundColor(.gray)
.onChange(of: shortName, perform: { _ in
let totalBytes = shortName.utf8.count
// Only mess with the value if it is too big
if totalBytes > 4 {
let firstNBytes = Data(shortName.utf8.prefix(4))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
shortName = maxBytesString
}
}
})
})
.foregroundColor(.gray)
}
.keyboardType(.default)
.disableAutocorrection(true)
Text("The last 4 of the device MAC address will be appended to the short name to set the device's BLE Name. Short name can be up to 4 bytes long.")
.foregroundColor(.gray)
.font(.caption)
}
.keyboardType(.default)
.disableAutocorrection(true)
Text("The last 4 of the device MAC address will be appended to the short name to set the device's BLE Name. Short name can be up to 4 bytes long.")
.font(.caption2)
// Only manage ham mode for the locally connected node
if node?.num ?? 0 > 0 && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
Toggle(isOn: $isLicensed) {

View file

@ -239,7 +239,7 @@
"power.config"="Power Config";
"power.ls.secs"="Light Sleep Interval";
"power.min.wake.secs"="Minimum Wake Interval";
"power.save"="Power Save";
"power.solar"="Solar Powered";
"power.shutdown.on.power.loss"="Shutdown on Power Loss";
"power.shutdown.after.secs"="After";
"power.wait.bluetooth.secs"="Bluetooth Off After";