From 496451c15caf551a60d79d5eddb5648591ae82fb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 26 Mar 2024 07:54:16 -0700 Subject: [PATCH] Add node filters --- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Enums/AppSettingsEnums.swift | 2 +- Meshtastic/Enums/DeviceEnums.swift | 6 +- .../CoreData/PositionEntityExtension.swift | 1 - .../Map/MapContent/MeshMapContent.swift | 1 - .../Views/Nodes/Helpers/NodeListFilter.swift | 117 +++++++++ .../Views/Nodes/Helpers/NodeListItem.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 129 +++++----- .../Settings/Config/Module/MQTTConfig.swift | 2 +- Meshtastic/Views/Settings/Settings.swift | 226 +++++++++++------- 10 files changed, 344 insertions(+), 146 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 67e51a8e..5e714267 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -186,6 +186,7 @@ DDDB445229F8ACF900EE2349 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445129F8ACF900EE2349 /* Date.swift */; }; DDDB445429F8AD1600EE2349 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB445329F8AD1600EE2349 /* Data.swift */; }; DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC22372BA92344002C44F1 /* MeshMapContent.swift */; }; + DDDCD5702BB26F5C00BE6B60 /* NodeListFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */; }; DDDE59F529AF163D00490C6C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */; }; DDDE59F629AF163D00490C6C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */; }; DDDE59F929AF163D00490C6C /* WidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */; }; @@ -458,6 +459,7 @@ DDDC22312BA76701002C44F1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; DDDC22322BA76961002C44F1 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = ""; }; DDDC22372BA92344002C44F1 /* MeshMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMapContent.swift; sourceTree = ""; }; + DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListFilter.swift; sourceTree = ""; }; DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = ""; }; DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; DDDE59F829AF163D00490C6C /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = ""; }; @@ -960,6 +962,7 @@ DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */, DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */, DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, + DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */, ); path = Helpers; sourceTree = ""; @@ -1355,6 +1358,7 @@ DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */, DD5E520E298EE33B00D21B61 /* mqtt.pb.swift in Sources */, DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */, + DDDCD5702BB26F5C00BE6B60 /* NodeListFilter.swift in Sources */, DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */, diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 4169444e..0250119a 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -56,7 +56,7 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable { case twoHundredMiles = 321869 case fiveHundredMiles = 804672 case oneThousandMiles = 1609000 - case twoThousandMiles = 3218688 + case twentyFiveHundredMiles = 4023360 var id: Double { self.rawValue } var description: String { let distanceFormatter = MKDistanceFormatter() diff --git a/Meshtastic/Enums/DeviceEnums.swift b/Meshtastic/Enums/DeviceEnums.swift index b754e986..8f2bbc7b 100644 --- a/Meshtastic/Enums/DeviceEnums.swift +++ b/Meshtastic/Enums/DeviceEnums.swift @@ -74,11 +74,13 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { var systemName: String { switch self { case .client: - return "iphone.gen3.radiowaves.left.and.right" + return "apps.iphone" case .clientMute: return "speaker.slash" - case .router, .routerClient, .repeater: + case .router, .routerClient: return "wifi.router" + case .repeater: + return "repeat" case .tracker: return "mappin.and.ellipse.circle" case .sensor: diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 20fbbd35..b24ec821 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -26,7 +26,6 @@ extension PositionEntity { let pointOfInterest = LocationHelper.currentLocation if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude { - /// Lets just get nodes within about 500 miles let D: Double = UserDefaults.meshMapDistance * 1.1 let R: Double = 6371009 let meanLatitidue = pointOfInterest.latitude * .pi / 180 diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 19bcd6ca..2655fbd4 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -205,6 +205,5 @@ struct MeshMapContent: MapContent { @MapContentBuilder var body: some MapContent { meshMap - } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift new file mode 100644 index 00000000..03e04d84 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -0,0 +1,117 @@ +// +// NodeListFilter.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 3/25/24. +// + +import Foundation +import SwiftUI + +struct NodeListFilter: View { + @Environment(\.dismiss) private var dismiss + /// Filters + @Binding var viaLora: Bool + @Binding var viaMqtt: Bool + @Binding var distanceFilter: Bool + @Binding var maximumDistance: Double + @Binding var hopsAway: Int + @Binding var deviceRole: Int + + var body: some View { + + NavigationStack { + Form { + Section(header: Text("Node Filters")) { + Toggle(isOn: $viaLora) { + + Label { + Text("Via Lora") + } icon: { + Image(systemName: "dot.radiowaves.left.and.right") + .rotationEffect(.degrees(-90)) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $viaMqtt) { + + Label { + Text("Via Mqtt") + } icon: { + Image(systemName: "dot.radiowaves.up.forward") + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + +// Toggle(isOn: $distanceFilter) { +// +// Label { +// Text("Distance") +// } icon: { +// Image(systemName: "map") +// } +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// +// .listRowSeparator(distanceFilter ? .hidden : .visible) +// if distanceFilter { +// HStack { +// Label("Show nodes", systemImage: "lines.measurement.horizontal") +// Picker("", selection: $maximumDistance) { +// ForEach(MeshMapDistances.allCases) { di in +// Text(di.description) +// .tag(di.id) +// } +// } +// .pickerStyle(DefaultPickerStyle()) +// } +// } + HStack { + Label("Hops Away", systemImage: "hare") + Picker("", selection: $hopsAway) { + Text("Any") + .tag(-1) + Text("Direct") + .tag(0) + ForEach(1..<8) { + Text("\($0)") + .tag($0) + } + } + .pickerStyle(DefaultPickerStyle()) + } + HStack { + Label("Device Role", systemImage: "apps.iphone") + Picker("", selection: $deviceRole) { + Text("All Roles") + .tag(-1) + ForEach(DeviceRoles.allCases) { dr in + Label { + Text(" \(dr.name)") + } icon: { + Image(systemName: dr.systemName) + } + } + } + .pickerStyle(DefaultPickerStyle()) + } + } + } +#if targetEnvironment(macCatalyst) + Spacer() + Button { + dismiss() + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + } + .presentationDetents([.fraction(0.35), .fraction(0.45)]) + .presentationDragIndicator(.visible) + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index feb528f6..f7ea4ffc 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -132,7 +132,7 @@ struct NodeListItem: View { } if node.viaMqtt && connectedNode != node.num { - Image(systemName: "network") + Image(systemName: "dot.radiowaves.up.forward") .symbolRenderingMode(.hierarchical) .font(.callout) .frame(width: 30) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 902d7df5..4efad7a4 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -7,28 +7,6 @@ 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 @@ -38,11 +16,17 @@ struct NodeList: View { @State private var isPresentingDeleteNodeAlert = false @State private var isPresentingPositionSentAlert = false @State private var deleteNodeId: Int64 = 0 - @State private var searchState = NodeSearchState() + @State private var searchText = "" + @State private var viaLora = true + @State private var viaMqtt = true + @State private var distanceFilter = false + @State private var maxDistance: Double = 800000 + @State private var hopsAway: Int = -1 + @State private var deviceRole: Int = -1 + + @State var isEditingFilters = false @SceneStorage("selectedDetailView") var selectedDetailView: String? - - @State private var searchText = "" @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -159,14 +143,17 @@ struct NodeList: View { Text("Any missed messages will be delivered again.") } } + .sheet(isPresented: $isEditingFilters) { + NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole) + } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { Button(action: { withAnimation { - //isEditingSettings = !isEditingSettings + isEditingFilters = !isEditingFilters } }) { - Image(systemName: true ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") + Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") .padding(.vertical, 5) } .tint(Color(UIColor.secondarySystemBackground)) @@ -178,14 +165,9 @@ struct NodeList: View { .padding(5) } .padding(.bottom, 5) - .searchable(text: $searchState.searchText, placement: nodes.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a node") + .searchable(text: $searchText, placement: .automatic, prompt: "Find a node") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) - .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( @@ -256,10 +238,19 @@ struct NodeList: View { } .navigationSplitViewStyle(.balanced) - .onChange(of: searchState.searchText) { _ in + .onChange(of: searchText) { _ in searchNodeList() } - .onChange(of: searchState.searchScope) { _ in + .onChange(of: viaLora) { _ in + searchNodeList() + } + .onChange(of: viaMqtt) { _ in + searchNodeList() + } + .onChange(of: deviceRole) { _ in + searchNodeList() + } + .onChange(of: hopsAway) { _ in searchNodeList() } .onAppear { @@ -272,35 +263,63 @@ struct NodeList: View { private func searchNodeList() { /// Case Insensitive Search Text Predicates let searchPredicates = ["user.userId", "user.hwModel", "user.longName", "user.shortName"].map { property in - return NSPredicate(format: "%K CONTAINS[c] %@", property, searchState.searchText) + return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) } /// Create a compound predicate using each text search preicate as an OR let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) + /// Create an array of predicates to hold our AND predicates + var predicates: [NSPredicate] = [] + /// Mqtt + if !(viaLora && viaMqtt) { + if viaLora { + let loraPredicate = NSPredicate(format: "viaMqtt == NO") + predicates.append(loraPredicate) + } else { + let mqttPredicate = NSPredicate(format: "viaMqtt == YES") + predicates.append(mqttPredicate) + } + } + /// Role + if deviceRole > 0 { + let rolePredicate = NSPredicate(format: "user.role == %i", Int32(deviceRole)) + predicates.append(rolePredicate) + } + /// Hops Away + if hopsAway > 0 { + let hopsAwayPredicate = NSPredicate(format: "hopsAway == %i", Int32(hopsAway)) + predicates.append(hopsAwayPredicate) + } + /// Distance + if distanceFilter { + let pointOfInterest = LocationHelper.currentLocation - /// Set the predicate to nil if the search string is empty - if searchState.searchText.isEmpty { - nodes.nsPredicate = nil - return + if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude { + let D: Double = maxDistance * 1.1 + let R: Double = 6371009 + let meanLatitidue = pointOfInterest.latitude * .pi / 180 + let deltaLatitude = D / R * 180 / .pi + let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi + let minLatitude: Double = pointOfInterest.latitude - deltaLatitude + let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude + let minLongitude: Double = pointOfInterest.longitude - deltaLongitude + let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude + let distancePredicate = NSPredicate(format: "(%lf <= (positions[first].longitudeI / 1e7))", minLongitude, maxLongitude,minLatitude, maxLatitude) + //let distancePredicate = NSPredicate(format: "(%lf <= (positions[LAST].longitudeI / 1e7)) AND ((positions[LAST].longitudeI / 1e7) <= %lf) AND (%lf <= (positions[LAST].latitudeI / 1e7)) AND ((positions[LAST].latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude) + + //predicates.append(distancePredicate) + } } - /// Add a predicate for the search scope if selected - if searchState.searchScope != .all { + if predicates.count > 0 { - 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 + if !searchText.isEmpty { + let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) + nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates]) + } else { + nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) } } else { - /// Use the text search predicate - nodes.nsPredicate = textSearchPredicate + nodes.nsPredicate = nil } } } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index cfcaa930..8191aec9 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -47,7 +47,7 @@ struct MQTTConfig: View { Section(header: Text("options")) { Toggle(isOn: $enabled) { - Label("enabled", systemImage: "dot.radiowaves.right") + Label("enabled", systemImage: "dot.radiowaves.up.forward") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index b98893e9..b1883c1c 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -50,34 +50,43 @@ struct Settings: View { NavigationLink { AboutMeshtastic() } label: { - Image(systemName: "questionmark.app") - .symbolRenderingMode(.hierarchical) - Text("about.meshtastic") + Label { + Text("about.meshtastic") + } icon: { + Image(systemName: "questionmark.app") + } } .tag(SettingsSidebar.about) NavigationLink { AppSettings() } label: { - Image(systemName: "gearshape") - .symbolRenderingMode(.hierarchical) - Text("appsettings") + Label { + Text("appsettings") + } icon: { + Image(systemName: "gearshape") + } } .tag(SettingsSidebar.appSettings) if #available(iOS 17.0, macOS 14.0, *) { NavigationLink { Routes() } label: { - Image(systemName: "road.lanes.curved.right") - .symbolRenderingMode(.hierarchical) - Text("routes") + Label { + Text("routes") + } icon: { + Image(systemName: "road.lanes.curved.right") + } } .tag(SettingsSidebar.routes) NavigationLink { RouteRecorder() } label: { - Image(systemName: "record.circle") - .symbolRenderingMode(.hierarchical) - Text("route.recorder") + Label { + Text("route.recorder") + } icon: { + Image(systemName: "record.circle") + .foregroundColor(.red) + } } .tag(SettingsSidebar.routeRecorder) } @@ -152,26 +161,33 @@ struct Settings: View { NavigationLink { LoRaConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "dot.radiowaves.left.and.right") - .symbolRenderingMode(.hierarchical) - Text("lora") + Label { + Text("lora") + } icon: { + Image(systemName: "dot.radiowaves.left.and.right") + .rotationEffect(.degrees(-90)) + } } .tag(SettingsSidebar.loraConfig) NavigationLink { Channels(node: nodes.first(where: { $0.num == preferredNodeNum })) } label: { - Image(systemName: "fibrechannel") - .symbolRenderingMode(.hierarchical) - Text("channels") + Label { + Text("channels") + } icon: { + Image(systemName: "fibrechannel") + } } .tag(SettingsSidebar.channelConfig) .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) NavigationLink { ShareChannels(node: nodes.first(where: { $0.num == preferredNodeNum })) } label: { - Image(systemName: "qrcode") - .symbolRenderingMode(.hierarchical) - Text("share.channels") + Label { + Text("share.channels") + } icon: { + Image(systemName: "qrcode") + } } .tag(SettingsSidebar.shareChannels) .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) @@ -180,58 +196,72 @@ struct Settings: View { NavigationLink { UserConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "person.crop.rectangle.fill") - .symbolRenderingMode(.hierarchical) - Text("user") + Label { + Text("user") + } icon: { + Image(systemName: "person.crop.rectangle.fill") + } } .tag(SettingsSidebar.userConfig) NavigationLink { BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "antenna.radiowaves.left.and.right") - .symbolRenderingMode(.hierarchical) - Text("bluetooth") + Label { + Text("bluetooth") + } icon: { + Image(systemName: "antenna.radiowaves.left.and.right") + } } .tag(SettingsSidebar.bluetoothConfig) NavigationLink { DeviceConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "flipphone") - .symbolRenderingMode(.hierarchical) - Text("device") + Label { + Text("device") + } icon: { + Image(systemName: "flipphone") + } } .tag(SettingsSidebar.deviceConfig) NavigationLink { DisplayConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "display") - .symbolRenderingMode(.hierarchical) - Text("display") + Label { + Text("display") + } icon: { + Image(systemName: "display") + } } .tag(SettingsSidebar.displayConfig) NavigationLink { NetworkConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "network") - .symbolRenderingMode(.hierarchical) - Text("network") + Label { + Text("network") + } icon: { + Image(systemName: "network") + } } .tag(SettingsSidebar.networkConfig) NavigationLink { PositionConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "location") - .symbolRenderingMode(.hierarchical) - Text("position") + Label { + Text("position") + } icon: { + Image(systemName: "location") + } } .tag(SettingsSidebar.positionConfig) NavigationLink { PowerConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "bolt.fill") - .symbolRenderingMode(.hierarchical) - Text("config.power.settings") + Label { + Text("config.power.settings") + } icon: { + Image(systemName: "bolt.fill") + } } .tag(SettingsSidebar.powerConfig) } @@ -240,92 +270,114 @@ struct Settings: View { NavigationLink { AmbientLightingConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "light.max") - .symbolRenderingMode(.hierarchical) - Text("ambient.lighting") + Label { + Text("ambient.lighting") + } icon: { + Image(systemName: "light.max") + } } .tag(SettingsSidebar.ambientLightingConfig) } NavigationLink { CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "list.bullet.rectangle.fill") - .symbolRenderingMode(.hierarchical) - Text("canned.messages") + Label { + Text("canned.messages") + } icon: { + Image(systemName: "list.bullet.rectangle.fill") + } } .tag(SettingsSidebar.cannedMessagesConfig) NavigationLink { DetectionSensorConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "sensor") - .symbolRenderingMode(.hierarchical) - Text("detection.sensor") + Label { + Text("detection.sensor") + } icon: { + Image(systemName: "sensor") + } } .tag(SettingsSidebar.detectionSensorConfig) NavigationLink { ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "megaphone") - .symbolRenderingMode(.hierarchical) - Text("external.notification") + Label { + Text("external.notification") + } icon: { + Image(systemName: "megaphone") + } } .tag(SettingsSidebar.externalNotificationConfig) NavigationLink { MQTTConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "dot.radiowaves.right") - .symbolRenderingMode(.hierarchical) - Text("mqtt") + Label { + Text("mqtt") + } icon: { + Image(systemName: "dot.radiowaves.up.forward") + } } .tag(SettingsSidebar.mqttConfig) NavigationLink { RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "point.3.connected.trianglepath.dotted") - .symbolRenderingMode(.hierarchical) - Text("range.test") + Label { + Text("range.test") + } icon: { + Image(systemName: "point.3.connected.trianglepath.dotted") + } } .tag(SettingsSidebar.rangeTestConfig) if node?.metadata?.hasWifi ?? false { NavigationLink { PaxCounterConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "figure.walk.motion") - .symbolRenderingMode(.hierarchical) - Text("config.module.paxcounter.settings") + Label { + Text("config.module.paxcounter.setting") + } icon: { + Image(systemName: "figure.walk.motion") + } } .tag(SettingsSidebar.paxCounterConfig) } NavigationLink { RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "music.note.list") - .symbolRenderingMode(.hierarchical) - Text("ringtone") + Label { + Text("ringtone") + } icon: { + Image(systemName: "music.note.list") + } } .tag(SettingsSidebar.ringtoneConfig) NavigationLink { SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "terminal") - .symbolRenderingMode(.hierarchical) - Text("serial") + Label { + Text("serial") + } icon: { + Image(systemName: "terminal") + } } .tag(SettingsSidebar.serialConfig) NavigationLink { StoreForwardConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "envelope.arrow.triangle.branch") - .symbolRenderingMode(.hierarchical) - Text("storeforward") + Label { + Text("storeforward") + } icon: { + Image(systemName: "envelope.arrow.triangle.branch") + } } .tag(SettingsSidebar.storeAndForwardConfig) NavigationLink { TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { - Image(systemName: "chart.xyaxis.line") - .symbolRenderingMode(.hierarchical) - Text("telemetry") + Label { + Text("telemetry") + } icon: { + Image(systemName: "chart.xyaxis.line") + } } .tag(SettingsSidebar.telemetryConfig) } @@ -333,18 +385,22 @@ struct Settings: View { NavigationLink { MeshLog() } label: { - Image(systemName: "list.bullet.rectangle") - .symbolRenderingMode(.hierarchical) - Text("mesh.log") + Label { + Text("mesh.log") + } icon: { + Image(systemName: "list.bullet.rectangle") + } } .tag(SettingsSidebar.meshLog) NavigationLink { let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) AdminMessageList(user: connectedNode?.user) } label: { - Image(systemName: "building.columns") - .symbolRenderingMode(.hierarchical) - Text("admin.log") + Label { + Text("admin.log") + } icon: { + Image(systemName: "building.columns") + } } .tag(SettingsSidebar.adminMessageLog) } @@ -352,9 +408,11 @@ struct Settings: View { NavigationLink { Firmware(node: nodes.first(where: { $0.num == preferredNodeNum })) } label: { - Image(systemName: "arrow.up.arrow.down.square") - .symbolRenderingMode(.hierarchical) - Text("Firmware Updates") + Label { + Text("Firmware Updates") + } icon: { + Image(systemName: "arrow.up.arrow.down.square") + } } .tag(SettingsSidebar.about) .disabled(selectedNode > 0 && selectedNode != preferredNodeNum)