diff --git a/Localizable.xcstrings b/Localizable.xcstrings index aa22cc67..2fd86fed 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -139,9 +139,15 @@ }, "%d%%" : { + }, + "%lf" : { + }, "%lld" : { + }, + "%lld or less hops away" : { + }, "%lld Readings Total" : { @@ -166,6 +172,15 @@ }, "1 byte" : { + }, + "1 hop away" : { + + }, + "7" : { + + }, + "8" : { + }, "25" : { @@ -666,10 +681,10 @@ "Alert when receiving a message" : { }, - "All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth." : { + "All" : { }, - "All Roles" : { + "All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth." : { }, "Allow Position Requests" : { @@ -873,9 +888,6 @@ }, "An open source, off-grid, decentralized, mesh network that runs on affordable, low-power radios." : { - }, - "Any" : { - }, "Any missed messages will be delivered again." : { @@ -17386,6 +17398,9 @@ }, "Role: %@" : { + }, + "Roles" : { + }, "Root Topic" : { @@ -22405,4 +22420,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 8fddb80a..f3718afc 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -645,6 +645,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate do { disconnectPeripheral(reconnect: false) try container.restorePersistentStore(from: databasePath) + context?.refreshAllObjects() let request = MyInfoEntity.fetchRequest() try context?.fetch(request) UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index a7bab2a2..f6e30e81 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -24,8 +24,9 @@ struct UserList: View { @State private var isFavorite = false @State private var distanceFilter = false @State private var maxDistance: Double = 800000 - @State private var hopsAway: Int = -1 - @State private var deviceRole: Int = -1 + @State private var hopsAway: Double = -1.0 + @State private var roleFilter = false + @State private var deviceRoles: Set = [] @State var isEditingFilters = false @FetchRequest( @@ -170,7 +171,7 @@ struct UserList: View { .listStyle(.plain) .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count - 1))) .sheet(isPresented: $isEditingFilters) { - NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole) + NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } .onChange(of: searchText) { _ in searchUserList() @@ -187,7 +188,7 @@ struct UserList: View { } searchUserList() } - .onChange(of: deviceRole) { _ in + .onChange(of: [deviceRoles]) { _ in searchUserList() } .onChange(of: hopsAway) { _ in @@ -259,17 +260,25 @@ struct UserList: View { predicates.append(mqttPredicate) } } - /// Role - if deviceRole > -1 { - let rolePredicate = NSPredicate(format: "role == %i", Int32(deviceRole)) - predicates.append(rolePredicate) + /// Roles + if roleFilter && deviceRoles.count > 0 { + var rolesArray: [NSPredicate] = [] + for dr in deviceRoles { + let deviceRolePredicate = NSPredicate(format: "role == %i", Int32(dr)) + rolesArray.append(deviceRolePredicate) + } + let compoundPredicate = NSCompoundPredicate(type: .or, subpredicates: rolesArray) + predicates.append(compoundPredicate) } /// Hops Away - if hopsAway > 0 { + if hopsAway == 0.0 { let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) + } else if hopsAway > -1.0 { + let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway > 0 AND userNode.hopsAway <= %i", Int32(hopsAway)) + predicates.append(hopsAwayPredicate) + } } - /// Online if isOnline { let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 2c49a7df..247cb954 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -10,7 +10,7 @@ import SwiftUI struct NodeListFilter: View { @Environment(\.dismiss) private var dismiss - /// Filters + @State var editMode = EditMode.active var filterTitle = "Node Filters" @Binding var viaLora: Bool @Binding var viaMqtt: Bool @@ -18,8 +18,9 @@ struct NodeListFilter: View { @Binding var isFavorite: Bool @Binding var distanceFilter: Bool @Binding var maximumDistance: Double - @Binding var hopsAway: Int - @Binding var deviceRole: Int + @Binding var hopsAway: Double + @Binding var roleFilter: Bool + @Binding var deviceRoles: Set var body: some View { @@ -97,34 +98,50 @@ struct NodeListFilter: View { .pickerStyle(DefaultPickerStyle()) } } - HStack { + VStack(alignment: .leading) { Label("Hops Away", systemImage: "hare") - Picker("", selection: $hopsAway) { - Text("Any") - .tag(-1) - Text("Direct") - .tag(0) - ForEach(1..<8) { - Text("\($0)") - .tag($0) - } + Slider( + value: $hopsAway, + in: -1...7, + step: 1 + ) { + Text("Speed") + } minimumValueLabel: { + Text("All") + } maximumValueLabel: { + Text("7") + } + if hopsAway >= 0 { + if hopsAway == 0 { + Text("Direct") + } else if hopsAway == 1 { + Text("1 hop away") + } else { + Text("\(Int(hopsAway)) or less hops away") } } - .pickerStyle(DefaultPickerStyle()) } - HStack { - Label("Device Role", systemImage: "apps.iphone") - Picker("", selection: $deviceRole) { - Text("All Roles") - .tag(-1) - ForEach(DeviceRoles.allCases) { dr in + Toggle(isOn: $roleFilter) { + + Label { + Text("Roles") + } icon: { + Image(systemName: "apps.iphone") + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if roleFilter { + VStack { + List(DeviceRoles.allCases, selection: $deviceRoles) { dr in Label { Text(" \(dr.name)") } icon: { Image(systemName: dr.systemName) } } + .listStyle(.plain) + .environment(\.editMode, $editMode) /// bind it here! + .frame(minHeight: 490, maxHeight: .infinity) } - .pickerStyle(DefaultPickerStyle()) } } } @@ -141,7 +158,7 @@ struct NodeListFilter: View { .padding(.bottom) #endif } - .presentationDetents([.fraction(0.6), .fraction(0.75)]) + .presentationDetents([.fraction(roleFilter ? 1.0 : 0.65)]) .presentationDragIndicator(.visible) } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index f31755ff..9b69af2a 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -25,8 +25,9 @@ struct NodeList: View { @State private var isFavorite = false @State private var distanceFilter = false @State private var maxDistance: Double = 800000 - @State private var hopsAway: Int = -1 - @State private var deviceRole: Int = -1 + @State private var hopsAway: Double = -1.0 + @State private var roleFilter = false + @State private var deviceRoles: Set = [] @State var isEditingFilters = false @@ -188,7 +189,7 @@ struct NodeList: View { } } .sheet(isPresented: $isEditingFilters) { - NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole) + NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { @@ -283,37 +284,55 @@ struct NodeList: View { } .navigationSplitViewStyle(.balanced) .onChange(of: searchText) { _ in - searchNodeList() + Task { + await searchNodeList() + } } .onChange(of: viaLora) { _ in if !viaLora && !viaMqtt { viaMqtt = true } - searchNodeList() + Task { + await searchNodeList() + } } .onChange(of: viaMqtt) { _ in if !viaLora && !viaMqtt { viaLora = true } - searchNodeList() + Task { + await searchNodeList() + } } - .onChange(of: deviceRole) { _ in - searchNodeList() + .onChange(of: [deviceRoles]) { _ in + Task { + await searchNodeList() + } } .onChange(of: hopsAway) { _ in - searchNodeList() + Task { + await searchNodeList() + } } .onChange(of: isOnline) { _ in - searchNodeList() + Task { + await searchNodeList() + } } .onChange(of: isFavorite) { _ in - searchNodeList() + Task { + await searchNodeList() + } } .onChange(of: maxDistance) { _ in - searchNodeList() + Task { + await searchNodeList() + } } .onChange(of: distanceFilter) { _ in - searchNodeList() + Task { + await searchNodeList() + } } .onChange(of: (appState.navigationPath)) { newPath in @@ -338,11 +357,13 @@ struct NodeList: View { if self.bleManager.context == nil { self.bleManager.context = context } - searchNodeList() + Task { + await searchNodeList() + } } } - private func searchNodeList() { + private func searchNodeList() async -> Void { /// Case Insensitive Search Text Predicates let searchPredicates = ["user.userId", "user.numString", "user.hwModel", "user.longName", "user.shortName"].map { property in return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) @@ -362,14 +383,23 @@ struct NodeList: View { } } /// Role - if deviceRole > -1 { - let rolePredicate = NSPredicate(format: "user.role == %i", Int32(deviceRole)) - predicates.append(rolePredicate) + if roleFilter && deviceRoles.count > 0 { + var rolesArray: [NSPredicate] = [] + for dr in deviceRoles { + let deviceRolePredicate = NSPredicate(format: "user.role == %i", Int32(dr)) + rolesArray.append(deviceRolePredicate) + } + let compoundPredicate = NSCompoundPredicate(type: .or, subpredicates: rolesArray) + predicates.append(compoundPredicate) } /// Hops Away - if hopsAway > 0 { + if hopsAway == 0.0 { let hopsAwayPredicate = NSPredicate(format: "hopsAway == %i", Int32(hopsAway)) predicates.append(hopsAwayPredicate) + } else if hopsAway > -1.0 { + let hopsAwayPredicate = NSPredicate(format: "hopsAway > 0 AND hopsAway <= %i", Int32(hopsAway)) + predicates.append(hopsAwayPredicate) + } } /// Online diff --git a/Meshtastic/Views/Settings/Logs/AppLogFilter.swift b/Meshtastic/Views/Settings/Logs/AppLogFilter.swift index 46d6f838..9943c2de 100644 --- a/Meshtastic/Views/Settings/Logs/AppLogFilter.swift +++ b/Meshtastic/Views/Settings/Logs/AppLogFilter.swift @@ -101,7 +101,7 @@ struct AppLogFilter: View { var filterTitle = "App Log Filters" @Binding var categories: Set @Binding var levels: Set - @State var editMode = EditMode.active /// the edit mode + @State var editMode = EditMode.active var body: some View {