diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 8b279cbd..156262d1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1583,7 +1583,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.26; + MARKETING_VERSION = 2.2.27; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1617,7 +1617,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.26; + MARKETING_VERSION = 2.2.27; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1727,7 +1727,7 @@ CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 840; DEVELOPMENT_TEAM = GCH7VS5Y9R; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; @@ -1739,7 +1739,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.26; + MARKETING_VERSION = 2.2.27; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1760,7 +1760,7 @@ CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 840; DEVELOPMENT_TEAM = GCH7VS5Y9R; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; @@ -1772,7 +1772,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.26; + MARKETING_VERSION = 2.2.27; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index a0e6af59..163e19a3 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -607,11 +607,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .adminApp: adminAppPacket(packet: decodedInfo.packet, context: context!) case .replyApp: - MeshLogger.log("🕸️ MESH PACKET received for Reply App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Reply App handling as a text message") + textMessageAppPacket(packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) case .ipTunnelApp: - MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED UNHANDLED") case .serialApp: - MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED UNHANDLED") case .storeForwardApp: if wantStoreAndForwardPackets { storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) @@ -628,17 +631,23 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .telemetryApp: if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } case .textMessageCompressedApp: - MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED") case .zpsApp: - MeshLogger.log("🕸️ MESH PACKET received for ZPS App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + // MeshLogger.log("🕸️ MESH PACKET received for Zero Positioning System App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Zero Positioning System App UNHANDLED") case .privateApp: - MeshLogger.log("🕸️ MESH PACKET received for Private App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Private App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Private App UNHANDLED UNHANDLED") case .atakForwarder: - MeshLogger.log("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED UNHANDLED") case .simulatorApp: - MeshLogger.log("🕸️ MESH PACKET received for Simulator App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Simulator App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Simulator App UNHANDLED UNHANDLED") case .audioApp: - MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED UNHANDLED") case .tracerouteApp: if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context!) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents index 00f91752..7c468a65 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents @@ -230,8 +230,7 @@ - - + @@ -239,31 +238,31 @@ - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -422,8 +421,8 @@ - - + + diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 4dd1bf7c..053622ae 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -170,11 +170,13 @@ struct NodeListItem: View { .font(.callout) .frame(width: 30) } - if node.hasTraceRoutes { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - .font(.callout) - .frame(width: 30) + if #available(iOS 17.0, macOS 14.0, *) { + if node.hasTraceRoutes { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + .font(.callout) + .frame(width: 30) + } } } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 48bfeb80..569b6651 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -7,6 +7,28 @@ import SwiftUI import CoreLocation +struct NodeSearchState { + var searchText = "" + var searchScope = SearchScopes.all + var predicate: NSPredicate = .init() + + enum SearchScopes: CaseIterable, Identifiable { + case all + case lora + case mqtt + + var id: Self { self } + + var title: LocalizedStringKey { + switch self { + case .all: return "All" + case .lora: return "LoRa" + case .mqtt: return "MQTT" + } + } + } +} + struct NodeList: View { @State private var columnVisibility = NavigationSplitViewVisibility.all @@ -15,18 +37,11 @@ struct NodeList: View { @State private var isPresentingClientHistorySentAlert = false @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 + @State private var searchState = NodeSearchState() @SceneStorage("selectedDetailView") var selectedDetailView: String? @State private var searchText = "" - var nodesQuery: Binding { - Binding { - searchText - } set: { newValue in - searchText = newValue - nodes.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "user.longName CONTAINS[c] %@ OR user.shortName CONTAINS[c] %@", newValue, newValue) - } - } @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -37,8 +52,6 @@ struct NodeList: View { var nodes: FetchedResults - - var body: some View { NavigationSplitView(columnVisibility: $columnVisibility) { @@ -124,7 +137,12 @@ struct NodeList: View { Text("Any missed messages will be delivered again.") } } - .searchable(text: nodesQuery, prompt: "Find a node") + .searchable(text: $searchState.searchText, placement: nodes.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a node") + .searchScopes($searchState.searchScope) { + ForEach(NodeSearchState.SearchScopes.allCases) { scope in + Text(scope.title).tag(scope) + } + } .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) .listStyle(.plain) .confirmationDialog( @@ -195,33 +213,51 @@ struct NodeList: View { } .navigationSplitViewStyle(.balanced) -// .onChange(of: selectedNode) { _ in -// if selectedNode == nil { -// columnVisibility = .all -// } else { -// columnVisibility = .doubleColumn -// } -// } + .onChange(of: searchState.searchText) { _ in + runSearch() + } + .onChange(of: searchState.searchScope) { _ in + runSearch() + } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context } } - -// } detail: { -// VStack { -// Button("Detail Only") { -// columnVisibility = .detailOnly -// } -// -// Button("Content and Detail") { -// columnVisibility = .doubleColumn -// } -// -// Button("Show All") { -// columnVisibility = .all -// } -// } -// } + } + + private func runSearch() { + /// Case Insensitive Search Text Predicates + var searchPredicates = ["user.userId", "user.hwModel", "user.longName", "user.shortName"].map { property in + return NSPredicate(format: "%K CONTAINS[c] %@", property, searchState.searchText) + } + /// Create a compound predicate using each text search preicate as an OR + let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) + + /// Set the predicate to nil if the search string is empty + if searchState.searchText.isEmpty { + nodes.nsPredicate = nil + return + } + + /// Add a predicate for the search scope if selected + if searchState.searchScope != .all { + + if searchState.searchScope == .lora { + let loraPredicate = NSPredicate(format: "viaMqtt == NO") + let scopePredicate = NSCompoundPredicate(type: .and, subpredicates: [loraPredicate]) + nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, scopePredicate]) + return + + } else if searchState.searchScope == .mqtt { + let mqttPredicate = NSPredicate(format: "viaMqtt == YES") + let scopePredicate = NSCompoundPredicate(type: .and, subpredicates: [mqttPredicate]) + nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, scopePredicate]) + return + } + } else { + /// Use the text search predicate + nodes.nsPredicate = textSearchPredicate + } } } diff --git a/Meshtastic/Views/Nodes/PaxCounterLog.swift b/Meshtastic/Views/Nodes/PaxCounterLog.swift index 3b04d662..b47ba1a1 100644 --- a/Meshtastic/Views/Nodes/PaxCounterLog.swift +++ b/Meshtastic/Views/Nodes/PaxCounterLog.swift @@ -75,8 +75,8 @@ struct PaxCounterLog: View { .chartXAxis(.automatic) .chartYScale(domain: 0...maxValue) .chartForegroundStyleScale([ - "paxcounter.ble": .blue, - "paxcounter.wifi": .orange, + "paxcounter.ble".localized: .blue, + "paxcounter.wifi".localized: .orange, "paxcounter.total".localized: .green ]) .chartLegend(position: .automatic, alignment: .bottom)