Merge branch 'main'

This commit is contained in:
Garth Vander Houwen 2025-05-06 13:34:57 -07:00
commit 29d7f594e6
33 changed files with 1250 additions and 1323 deletions

File diff suppressed because it is too large Load diff

View file

@ -21,15 +21,15 @@ enum MeshMapTypes: Int, CaseIterable, Identifiable {
case .standard:
return "Standard".localized
case .mutedStandard:
return "standard.muted".localized
return "Standard Muted".localized
case .hybrid:
return "hybrid".localized
return "Hybrid".localized
case .hybridFlyover:
return "hybrid.flyover".localized
return "Hybrid Flyover".localized
case .satellite:
return "satellite".localized
return "Satellite".localized
case .satelliteFlyover:
return "satellite.flyover".localized
return "Satellite Flyover".localized
}
}
func MKMapTypeValue() -> MKMapType {

View file

@ -168,17 +168,17 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
var description: String {
switch self {
case .all:
return "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params."
return "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params.".localized
case .allSkipDecoding:
return "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."
return "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.".localized
case .localOnly:
return "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."
return "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.".localized
case .knownOnly:
return "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."
return "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.".localized
case .none:
return "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role."
return "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.".localized
case .corePortnums:
return "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing."
return "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing.".localized
}
}
func protoEnumValue() -> Config.DeviceConfig.RebroadcastMode {

View file

@ -32,31 +32,31 @@ enum RoutingError: Int, CaseIterable, Identifiable {
switch self {
case .none:
return "routing.acknowledged".localized
return "Acknowledged".localized
case .noRoute:
return "routing.noroute".localized
return "No Route".localized
case .gotNak:
return "routing.gotnak".localized
return "Received a negative acknowledgment".localized
case .timeout:
return "routing.timeout".localized
return "Timeout".localized
case .noInterface:
return "routing.nointerface".localized
return "No Interface".localized
case .maxRetransmit:
return "routing.maxretransmit".localized
return "Max Retransmission Reached".localized
case .noChannel:
return "routing.nochannel".localized
return "No Channel".localized
case .tooLarge:
return "routing.toolarge".localized
return "The packet is too large".localized
case .noResponse:
return "routing.noresponse".localized
return "No Response".localized
case .dutyCycleLimit:
return "routing.dutycyclelimit".localized
return "Regional Duty Cycle Limit Reached".localized
case .badRequest:
return "routing.badRequest".localized
return "Bad Request".localized
case .notAuthorized:
return "routing.notauthorized".localized
return "Not Authorized".localized
case .pkiFailed:
return "routing.pkifailed".localized
return "Encrypted Send Failed".localized
case .pkiUnknownPubkey:
return "Unknown public key".localized
case .adminBadSessionKey:

View file

@ -100,7 +100,7 @@ extension UserDefaults {
@UserDefault(.meshMapDistance, defaultValue: 800000)
static var meshMapDistance: Double
@UserDefault(.enableMapWaypoints, defaultValue: false)
@UserDefault(.enableMapWaypoints, defaultValue: true)
static var enableMapWaypoints: Bool
@UserDefault(.enableMapRecentering, defaultValue: false)

View file

@ -227,6 +227,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
let myInfoEntity = MyInfoEntity(context: context)
myInfoEntity.myNodeNum = Int64(packet.from)
myInfoEntity.rebootCount = 0
newNode.myInfo = myInfoEntity
do {
try context.save()
Logger.data.info("💾 [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)")
@ -236,7 +237,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
let nsError = error as NSError
Logger.data.error("💥 [MyInfoEntity] Error Inserting New Core Data: \(nsError, privacy: .public)")
}
newNode.myInfo = myInfoEntity
} else {
// Update an existing node
@ -1101,7 +1101,7 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN
func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.paxcounter.config %@".localized, String(nodeNum))
let logString = String.localizedStringWithFormat("PAX Counter config received: %@".localized, String(nodeNum))
Logger.data.info("🧑‍🤝‍🧑 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()

View file

@ -144,7 +144,7 @@ struct ChannelList: View {
context.refresh(myInfo, mergeChanges: true)
channelSelection = nil
} label: {
Text("delete")
Text("Delete")
}
}
}

View file

@ -25,6 +25,8 @@ struct ChannelMessageList: View {
@State private var hasReachedBottom = false
@State private var gotFirstUnreadMessage: Bool = false
@State private var messageToHighlight: Int64 = 0
var body: some View {
VStack {
ScrollViewReader { scrollView in
@ -36,16 +38,33 @@ struct ChannelMessageList: View {
if message.replyID > 0 {
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
HStack {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
Button {
if let messageNum = messageReply?.messageId {
withAnimation(.easeInOut(duration: 0.5)) {
messageToHighlight = messageNum
}
scrollView.scrollTo(messageNum, anchor: .center)
// Reset highlight after delay
Task {
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
withAnimation(.easeInOut(duration: 0.5)) {
messageToHighlight = -1
}
}
}
} label: {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
}
}
}
HStack(alignment: .bottom) {
@ -111,6 +130,12 @@ struct ChannelMessageList: View {
Spacer(minLength: 50)
}
}
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 2)
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)

View file

@ -104,7 +104,7 @@ struct MessageContextMenuItems: View {
Button(role: .destructive) {
isShowingDeleteConfirmation = true
} label: {
Text("delete")
Text("Delete")
Image(systemName: "trash")
}
}

View file

@ -65,7 +65,7 @@ struct Messages: View {
TipView(MessagesTip(), arrowEdge: .top)
}
.navigationTitle("messages")
.navigationTitle("Messages")
.navigationBarTitleDisplayMode(.large)
.navigationBarItems(leading: MeshtasticLogo())
} content: {

View file

@ -15,78 +15,93 @@ struct TextMessageField: View {
@State private var sendPositionWithMessage = false
var body: some View {
#if targetEnvironment(macCatalyst)
HStack {
if destination.showAlertButton {
VStack {
#if targetEnvironment(macCatalyst)
HStack {
if destination.showAlertButton {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" }
}
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" }
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
}
Spacer()
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
}
#endif
#endif
HStack(alignment: .top) {
ZStack {
TextField("Message", text: $typingMessage, axis: .vertical)
.onChange(of: typingMessage) { _, value in
totalBytes = value.utf8.count
// Only mess with the value if it is too big
while totalBytes > Self.maxbytes {
typingMessage = String(typingMessage.dropLast())
totalBytes = typingMessage.utf8.count
}
}
.keyboardType(.default)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Dismiss") {
isFocused = false
HStack(alignment: .top) {
if replyMessageId != 0 {
HStack {
Button {
withAnimation(.easeInOut(duration: 0.2)) {
replyMessageId = 0
}
.font(.subheadline)
isFocused = false
} label: {
Image(systemName: "x.circle.fill")
}
Text("Replying to a message")
}
}
ZStack {
TextField("Message", text: $typingMessage, axis: .vertical)
.onChange(of: typingMessage) { _, value in
totalBytes = value.utf8.count
while totalBytes > Self.maxbytes {
typingMessage = String(typingMessage.dropLast())
totalBytes = typingMessage.utf8.count
}
}
.keyboardType(.default)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Button("Dismiss") {
isFocused = false
}
.font(.subheadline)
if destination.showAlertButton {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
}
if destination.showAlertButton {
Spacer()
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
}
Spacer()
RequestPositionButton(action: requestPosition)
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
}
}
.padding(.horizontal, 8)
.focused($isFocused)
.multilineTextAlignment(.leading)
.frame(minHeight: 50)
.keyboardShortcut(.defaultAction)
.onSubmit {
#if targetEnvironment(macCatalyst)
sendMessage()
#endif
}
.padding(.horizontal, 8)
.focused($isFocused)
.multilineTextAlignment(.leading)
.frame(minHeight: 50)
.keyboardShortcut(.defaultAction)
.onSubmit {
#if targetEnvironment(macCatalyst)
sendMessage()
#endif
}
Text(typingMessage)
.opacity(0)
.padding(.all, 0)
}
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
.padding(.bottom, 15)
Text(typingMessage)
.opacity(0)
.padding(.all, 0)
}
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
.padding(.bottom, 15)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle.fill")
.font(.largeTitle)
.foregroundColor(.accentColor)
Button(action: sendMessage) {
Image(systemName: "arrow.up.circle.fill")
.font(.largeTitle)
.foregroundColor(.accentColor)
}
}
.padding(.all, 15)
}
.padding(.all, 15)
}
private func requestPosition() {
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
sendPositionWithMessage = true
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
}
private func sendMessage() {

View file

@ -187,7 +187,7 @@ struct UserList: View {
deleteUserMessages(user: userSelection!, context: context)
context.refresh(node!.user!, mergeChanges: true)
} label: {
Text("delete")
Text("Delete")
}
}
}

View file

@ -25,6 +25,8 @@ struct UserMessageList: View {
@State private var hasReachedBottom = false
@State private var gotFirstUnreadMessage: Bool = false
@State private var messageToHighlight: Int64 = 0
var body: some View {
VStack {
ScrollViewReader { scrollView in
@ -38,16 +40,33 @@ struct UserMessageList: View {
if message.replyID > 0 {
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
HStack {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
Button {
if let messageNum = messageReply?.messageId {
withAnimation(.easeInOut(duration: 0.5)) {
messageToHighlight = messageNum
}
scrollView.scrollTo(messageNum, anchor: .center)
// Reset highlight after delay
Task {
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
withAnimation(.easeInOut(duration: 0.5)) {
messageToHighlight = -1
}
}
}
} label: {
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 18)
.stroke(Color.blue, lineWidth: 0.5)
)
Image(systemName: "arrowshape.turn.up.left.fill")
.symbolRenderingMode(.hierarchical)
.imageScale(.large).foregroundColor(.accentColor)
.padding(.trailing)
}
}
}
HStack(alignment: .top) {
@ -100,6 +119,11 @@ struct UserMessageList: View {
Spacer(minLength: 50)
}
}
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(.blue, lineWidth: 2)
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
}
.padding([.leading, .trailing])
.frame(maxWidth: .infinity)
.id(message.messageId)

View file

@ -110,7 +110,7 @@ struct DetectionSensorLog: View {
exportString = detectionsToCsv(detections: chartData)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)

View file

@ -222,7 +222,7 @@ struct DeviceMetricsLog: View {
exportString = telemetryToCsvFile(telemetry: deviceMetrics, metricsType: 0)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)

View file

@ -145,7 +145,7 @@ struct EnvironmentMetricsLog: View {
exportString = telemetryToCsvFile(telemetry: environmentMetrics, metricsType: 1)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
.imageScale(imageScale)
}
.buttonStyle(.bordered)

View file

@ -20,7 +20,7 @@ struct MeshMapContent: MapContent {
@Binding var selectedMapLayer: MapLayer
// Map Configuration
@Binding var selectedPosition: PositionEntity?
@AppStorage("enableMapWaypoints") private var showWaypoints = false
@AppStorage("enableMapWaypoints") private var showWaypoints = true
@Binding var selectedWaypoint: WaypointEntity?
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
@ -74,9 +74,9 @@ struct MeshMapContent: MapContent {
}
}
}
.onTapGesture { _ in
.highPriorityGesture(TapGesture().onEnded { _ in
selectedPosition = (selectedPosition == position ? nil : position)
}
})
}
/// Node History and Route Lines for favorites
if let nodePosition = position.nodePosition,
@ -186,7 +186,7 @@ struct MeshMapContent: MapContent {
LazyVStack {
ZStack {
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40)
.onTapGesture(perform: { _ in
.highPriorityGesture(TapGesture().onEnded { _ in
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
})
}

View file

@ -16,7 +16,7 @@ struct NodeMapContent: MapContent {
/// Map State User Defaults
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
@AppStorage("enableMapWaypoints") private var showWaypoints = false
@AppStorage("enableMapWaypoints") private var showWaypoints = true
@AppStorage("enableMapConvexHull") private var showConvexHull = false
@AppStorage("enableMapTraffic") private var showTraffic: Bool = false
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false

View file

@ -52,7 +52,7 @@ struct PositionPopover: View {
/// Time
Label {
if idiom != .phone {
Text("heard".localized + ":")
Text("Heard".localized + ":")
}
Text(position.time?.lastHeard ?? "unknown")
.foregroundColor(.primary)

View file

@ -134,40 +134,44 @@ struct WaypointForm: View {
.scrollDismissesKeyboard(.immediately)
HStack {
Button {
/// Send a new or exiting waypoint
var newWaypoint = Waypoint()
if waypoint.id == 0 {
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
waypoint.id = Int64(newWaypoint.id)
} else {
newWaypoint.id = UInt32(waypoint.id)
}
newWaypoint.latitudeI = waypoint.latitudeI
newWaypoint.longitudeI = waypoint.longitudeI
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
newWaypoint.description_p = description
// Unicode scalar value for the icon emoji string
let unicodeScalers = icon.unicodeScalars
// First element as an UInt32
let unicode = unicodeScalers[unicodeScalers.startIndex].value
newWaypoint.icon = unicode
if locked {
if lockedTo == 0 {
newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num)
if bleManager.isConnected {
/// Send a new or exiting waypoint
var newWaypoint = Waypoint()
if waypoint.id == 0 {
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
waypoint.id = Int64(newWaypoint.id)
} else {
newWaypoint.lockedTo = UInt32(lockedTo)
newWaypoint.id = UInt32(waypoint.id)
}
newWaypoint.latitudeI = waypoint.latitudeI
newWaypoint.longitudeI = waypoint.longitudeI
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
newWaypoint.description_p = description
// Unicode scalar value for the icon emoji string
let unicodeScalers = icon.unicodeScalars
// First element as an UInt32
let unicode = unicodeScalers[unicodeScalers.startIndex].value
newWaypoint.icon = unicode
if locked {
if lockedTo == 0 {
newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num)
} else {
newWaypoint.lockedTo = UInt32(lockedTo)
}
}
if expires {
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
} else {
newWaypoint.expire = 0
}
if bleManager.sendWaypoint(waypoint: newWaypoint) {
dismiss()
} else {
dismiss()
Logger.mesh.warning("Send waypoint failed")
}
}
if expires {
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
} else {
newWaypoint.expire = 0
}
if bleManager.sendWaypoint(waypoint: newWaypoint) {
dismiss()
} else {
dismiss()
Logger.mesh.warning("Send waypoint failed")
Logger.mesh.warning("Send waypoint failed, node not connected")
}
} label: {
Label("Send", systemImage: "arrow.up")
@ -235,7 +239,7 @@ struct WaypointForm: View {
})
}
label: {
Label("delete", systemImage: "trash")
Label("Delete", systemImage: "trash")
.foregroundColor(.red)
}
.buttonStyle(.bordered)
@ -364,6 +368,18 @@ struct WaypointForm: View {
}
}
}
.onDisappear {
if waypoint.id == 0 {
// New, unsent waypoint created by the user: delete it
bleManager.context.delete(waypoint)
do {
try bleManager.context.save()
} catch {
bleManager.context.rollback()
Logger.mesh.error("Failed to save context on waypoint deletion: \(error)")
}
}
}
.onAppear {
if waypoint.id > 0 {
let waypoint = getWaypoint(id: Int64(waypoint.id), context: bleManager.context)

View file

@ -187,7 +187,7 @@ struct PaxCounterLog: View {
exportString = paxToCsvFile(pax: pax)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)

View file

@ -67,7 +67,7 @@ struct TraceRouteLog: View {
Logger.data.error("\(error.localizedDescription, privacy: .public)")
}
} label: {
Label("delete", systemImage: "trash")
Label("Delete", systemImage: "trash")
}
}
}

View file

@ -41,7 +41,7 @@ struct AppData: View {
Logger.services.error("🗑️ Delete file error: \(error, privacy: .public)")
}
} label: {
Label("delete", systemImage: "trash")
Label("Delete", systemImage: "trash")
}
}
} icon: {
@ -61,7 +61,7 @@ struct AppData: View {
Logger.services.error("🗑️ Delete file error: \(error, privacy: .public)")
}
} label: {
Label("delete", systemImage: "trash")
Label("Delete", systemImage: "trash")
}
}
} icon: {

View file

@ -84,7 +84,7 @@ struct AppLog: View {
.foregroundStyle(value.level.color)
}
.width(min: 85, max: 110)
TableColumn("log.category", value: \.category)
TableColumn("Category", value: \.category)
.width(min: 80, max: 130)
TableColumn("Message", value: \.composedMessage) { value in
Text(value.composedMessage)

View file

@ -219,7 +219,7 @@ struct Channels: View {
hasChanges = false
}
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil)// || !hasChanges)// !hasValidKey)
.buttonStyle(.bordered)

View file

@ -22,7 +22,7 @@ struct PaxCounterConfig: View {
var body: some View {
Form {
ConfigHeader(title: "config.module.paxcounter.title", config: \.powerConfig, node: node, onAppear: setPaxValues)
ConfigHeader(title: "PAX Counter Config", config: \.powerConfig, node: node, onAppear: setPaxValues)
Section {
Toggle(isOn: $enabled) {

View file

@ -45,7 +45,7 @@ struct RangeTestConfig: View {
.font(.callout)
Toggle(isOn: $save) {
Label("save", systemImage: "square.and.arrow.down.fill")
Label("Save", systemImage: "square.and.arrow.down.fill")
Text("Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))

View file

@ -12,7 +12,7 @@ struct SaveConfigButton: View {
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
.buttonStyle(.bordered)

View file

@ -107,7 +107,7 @@ struct LogDetail: View {
/// Category
Label {
HStack {
Text("log.category".localized + ":")
Text("Category".localized + ":")
.font(idiom == .phone ? .caption : .title)
.frame(width: idiom == .phone ? 115 : 190, alignment: .trailing)
Text(log.category)

View file

@ -154,7 +154,7 @@ struct Routes: View {
Logger.data.error("\(error.localizedDescription, privacy: .public)")
}
} label: {
Label("delete", systemImage: "trash")
Label("Delete", systemImage: "trash")
}
}
@ -215,7 +215,7 @@ struct Routes: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
Button("save") {
Button("Save") {
selectedRoute?.name = name
selectedRoute?.notes = notes
selectedRoute?.enabled = enabled
@ -279,7 +279,7 @@ struct Routes: View {
exportString = routeToCsvFile(locations: selectedRoute!.locations!.array as? [LocationEntity] ?? [])
isExporting = true
} label: {
Label("export", systemImage: "square.and.arrow.down")
Label("Export", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)

View file

@ -43,7 +43,7 @@ struct SaveChannelQRCode: View {
showError = true
}
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)

View file

@ -105,7 +105,7 @@ struct Settings: View {
}
var deviceConfigurationSection: some View {
Section("device.configuration") {
Section("Device Configuration") {
NavigationLink(value: SettingsNavigationState.user) {
Label {
Text("User")

View file

@ -141,7 +141,7 @@ struct UserConfig: View {
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
.buttonStyle(.bordered)