mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Add node filters
This commit is contained in:
parent
525b1b2509
commit
496451c15c
10 changed files with 344 additions and 146 deletions
|
|
@ -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 = "<group>"; };
|
||||
DDDC22322BA76961002C44F1 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
DDDC22372BA92344002C44F1 /* MeshMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMapContent.swift; sourceTree = "<group>"; };
|
||||
DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListFilter.swift; sourceTree = "<group>"; };
|
||||
DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
|
|
@ -960,6 +962,7 @@
|
|||
DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */,
|
||||
DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */,
|
||||
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
|
||||
DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -205,6 +205,5 @@ struct MeshMapContent: MapContent {
|
|||
@MapContentBuilder
|
||||
var body: some MapContent {
|
||||
meshMap
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift
Normal file
117
Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue