diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 9fbb2d07..e2a7efd6 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -116,7 +116,6 @@ DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F40F2A9EE5B400230ECE /* Messages.swift */; }; DDB8F4122A9EE5DD00230ECE /* UserList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F4112A9EE5DD00230ECE /* UserList.swift */; }; DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F4132A9EE5F000230ECE /* ChannelList.swift */; }; - DDC18AD12AAAE5920083FE1E /* NodeListDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC18AD02AAAE5920083FE1E /* NodeListDetail.swift */; }; DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */; }; DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; }; DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; }; @@ -136,6 +135,9 @@ DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6EEAE29BC024700383354 /* Firmware.swift */; }; DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */; }; DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */; }; + DDDB263F2AABEE20003AFCB7 /* NodeListSplit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB263E2AABEE20003AFCB7 /* NodeListSplit.swift */; }; + DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26412AABF655003AFCB7 /* NodeListItem.swift */; }; + DDDB26442AAC0206003AFCB7 /* NodeDetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */; }; DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443529F6287000EE2349 /* MapButtons.swift */; }; DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443C29F6592F00EE2349 /* NetworkManager.swift */; }; DDDB444029F79AB000EE2349 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443F29F79AB000EE2349 /* UserDefaults.swift */; }; @@ -324,7 +326,6 @@ DDB8F4112A9EE5DD00230ECE /* UserList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserList.swift; sourceTree = ""; }; DDB8F4132A9EE5F000230ECE /* ChannelList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelList.swift; sourceTree = ""; }; DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = ""; }; - DDC18AD02AAAE5920083FE1E /* NodeListDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListDetail.swift; sourceTree = ""; }; DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; }; DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticApp.swift; sourceTree = ""; }; DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ../Assets.xcassets; sourceTree = ""; }; @@ -353,6 +354,9 @@ DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = ""; }; DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = ""; }; DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = ""; }; + DDDB263E2AABEE20003AFCB7 /* NodeListSplit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListSplit.swift; sourceTree = ""; }; + DDDB26412AABF655003AFCB7 /* NodeListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListItem.swift; sourceTree = ""; }; + DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetailItem.swift; sourceTree = ""; }; DDDB443529F6287000EE2349 /* MapButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapButtons.swift; sourceTree = ""; }; DDDB443C29F6592F00EE2349 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; DDDB443F29F79AB000EE2349 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; @@ -460,6 +464,7 @@ DD47E3CA26F0E50300029299 /* Nodes */ = { isa = PBXGroup; children = ( + DDDB26402AABEF7B003AFCB7 /* Helpers */, DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */, DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, DD2E65252767A01F00E45FC5 /* NodeDetail.swift */, @@ -468,6 +473,7 @@ DD73FD1028750779000852D6 /* PositionLog.swift */, DD14E72D2A82A614006E39BC /* RemoteHardware.swift */, 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, + DDDB263E2AABEE20003AFCB7 /* NodeListSplit.swift */, ); path = Nodes; sourceTree = ""; @@ -656,6 +662,7 @@ DDDB443E29F79A9400EE2349 /* Extensions */, DDC2E1A526CEB32B0042C5E4 /* Helpers */, DDC2E18826CE24EE0042C5E4 /* Model */, + DDDB263D2AABD34F003AFCB7 /* Navigation */, DDC4D5662754996200A4208E /* Persistence */, DDAF8C5626ED07740058C060 /* Protobufs */, DDC2E18926CE24F70042C5E4 /* Resources */, @@ -792,6 +799,22 @@ path = Mqtt; sourceTree = ""; }; + DDDB263D2AABD34F003AFCB7 /* Navigation */ = { + isa = PBXGroup; + children = ( + ); + path = Navigation; + sourceTree = ""; + }; + DDDB26402AABEF7B003AFCB7 /* Helpers */ = { + isa = PBXGroup; + children = ( + DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, + DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */, + ); + path = Helpers; + sourceTree = ""; + }; DDDB443E29F79A9400EE2349 /* Extensions */ = { isa = PBXGroup; children = ( @@ -833,7 +856,6 @@ isa = PBXGroup; children = ( DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */, - DDC18AD02AAAE5920083FE1E /* NodeListDetail.swift */, ); path = Node; sourceTree = ""; @@ -1075,6 +1097,7 @@ DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */, DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, + DDDB263F2AABEE20003AFCB7 /* NodeListSplit.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, @@ -1085,7 +1108,6 @@ DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */, DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */, DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */, - DDC18AD12AAAE5920083FE1E /* NodeListDetail.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, @@ -1101,6 +1123,7 @@ DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */, DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */, DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */, + DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */, DDDB444629F8A96500EE2349 /* Character.swift in Sources */, DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */, DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */, @@ -1163,6 +1186,7 @@ DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, + DDDB26442AAC0206003AFCB7 /* NodeDetailItem.swift in Sources */, DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */, DD77093F2AA1B146007A8BF0 /* UIColor.swift in Sources */, DD5E5205298EE33B00D21B61 /* mesh.pb.swift in Sources */, diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 16ce02b8..b4daff4b 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -41,7 +41,6 @@ extension UserDefaults { UserDefaults.standard.set(newValue, forKey: "meshtasticUsername") } } - static var preferredPeripheralId: String { get { UserDefaults.standard.string(forKey: "preferredPeripheralId") ?? "" diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 35b2a9ea..751b7a38 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -24,6 +24,11 @@ struct ContentView: View { Label("nodes", systemImage: "flipphone") } .tag(Tab.nodes) + NodeListSplit() + .tabItem { + Label("nodes", systemImage: "flipphone") + } + .tag(Tab.nodes2) NodeMap() .tabItem { Label("map", systemImage: "map") @@ -51,5 +56,6 @@ enum Tab { case map case ble case nodes + case nodes2 case settings } diff --git a/Meshtastic/Views/Helpers/Node/NodeListDetail.swift b/Meshtastic/Views/Helpers/Node/NodeListDetail.swift deleted file mode 100644 index 43dd76f9..00000000 --- a/Meshtastic/Views/Helpers/Node/NodeListDetail.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// NodeListDetail.swift -// Meshtastic -// -// Copyright(c) Garth Vander Houwen 9/7/23. -// - -//import SwiftUI -//import CoreLocation -//import MapKit -// -//struct NodeListDetail: View { -// -// var node: NodeInfoEntity -// -// var body: some View { -// -// } -//} diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index df4be238..9a03ac96 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -24,8 +24,8 @@ struct ChannelMessageList: View { var maxbytes = 228 @FocusState var focusedField: Field? - @ObservedObject var myInfo: MyInfoEntity - @ObservedObject var channel: ChannelEntity + @StateObject var myInfo: MyInfoEntity + @StateObject var channel: ChannelEntity @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 @@ -233,7 +233,7 @@ struct ChannelMessageList: View { message.read = true do { try context.save() - print("Read message \(message.messageId) ") + print("📖 Read message \(message.messageId) ") appState.unreadChannelMessages = myInfo.unreadMessages UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages context.refresh(myInfo, mergeChanges: true) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index e93bffd5..6fd5c6d3 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -222,7 +222,7 @@ struct UserMessageList: View { message.read = true do { try context.save() - print("Read message \(message.messageId) ") + print("📖 Read message \(message.messageId) ") appState.unreadDirectMessages = user.unreadMessages UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift new file mode 100644 index 00000000..dc22ca2d --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift @@ -0,0 +1,30 @@ +// +// NodeDetailItem.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/8/23. +// + +import SwiftUI +import WeatherKit +import MapKit +import CoreLocation + +struct NodeDetailItem: View { + + var node: NodeInfoEntity + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], + predicate: NSPredicate( + format: "expire == nil || expire >= %@", Date() as NSDate + ), animation: .none) + private var waypoints: FetchedResults + + + + var body: some View { + + NavigationStack { + + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift new file mode 100644 index 00000000..5574e1dc --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -0,0 +1,89 @@ +// +// NodeListItem.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/8/23. +// + +import SwiftUI + + + + +struct NodeListItem: View { + + @StateObject var node: NodeInfoEntity + + var body: some View { + + NavigationLink(value: node) { + let connected: Bool = false //(bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num) + LazyVStack(alignment: .leading) { + HStack { + VStack(alignment: .leading) { + CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) + .padding(.trailing, 5) + let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) + if deviceMetrics?.count ?? 0 >= 1 { + let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + BatteryLevelCompact(batteryLevel: mostRecent?.batteryLevel, font: .caption2, iconFont: .callout, color: .accentColor) + } + } + VStack(alignment: .leading) { + Text(node.user?.longName ?? "unknown".localized) + .fontWeight(.medium) + .font(.callout) + if connected { + HStack(alignment: .bottom) { + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .font(.footnote) + .symbolRenderingMode(.hierarchical) + .foregroundColor(.green) + Text("connected").font(.caption) + } + } + HStack(alignment: .bottom) { + Image(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill") + .font(.footnote) + .symbolRenderingMode(.hierarchical) + .foregroundColor(node.isOnline ? .green : .orange) + LastHeardText(lastHeard: node.lastHeard) + .font(.caption) + } +// if node.positions?.count ?? 0 > 0 && (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 != node.num) { +// HStack(alignment: .bottom) { +// let lastPostion = node.positions!.reversed()[0] as! PositionEntity +// let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude) +// if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude { +// let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) +// let metersAway = nodeCoord.distance(from: myCoord) +// Image(systemName: "lines.measurement.horizontal") +// .font(.footnote) +// .symbolRenderingMode(.hierarchical) +// DistanceText(meters: metersAway).font(.caption) +// } +// } +// } + if node.channel > 0 { + HStack(alignment: .bottom) { + Image(systemName: "fibrechannel") + .font(.footnote) + .symbolRenderingMode(.hierarchical) + Text("Channel: \(node.channel)") + .font(.footnote) + } + } + +// if !connected { +// HStack(alignment: .bottom) { let preset = ModemPresets(rawValue: Int(connectedNode?.loRaConfig?.modemPreset ?? 0)) +// LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true) +// } +// } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + .padding([.top, .bottom]) + } +} diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 104412c9..e949b7a2 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -12,6 +12,10 @@ import SwiftUI import CoreLocation struct NodeList: View { + + init () { + //self.bleManager.context = context + } @State private var searchText = "" var nodesQuery: Binding { Binding { @@ -86,7 +90,7 @@ struct NodeList: View { Image(systemName: "lines.measurement.horizontal") .font(.footnote) .symbolRenderingMode(.hierarchical) - DistanceText(meters: metersAway).font(.footnote) + DistanceText(meters: metersAway).font(.caption) } } } @@ -119,7 +123,7 @@ struct NodeList: View { MeshtasticLogo() ) .onAppear { - self.bleManager.context = context + // self.bleManager.context = context } } detail: { if let node = selection { diff --git a/Meshtastic/Views/Nodes/NodeListSplit.swift b/Meshtastic/Views/Nodes/NodeListSplit.swift new file mode 100644 index 00000000..b181b859 --- /dev/null +++ b/Meshtastic/Views/Nodes/NodeListSplit.swift @@ -0,0 +1,82 @@ +// +// NodeListSplit.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/8/23. +// +import SwiftUI +import CoreLocation + +struct NodeListSplit: View { + + @State private var columnVisibility = NavigationSplitViewVisibility.all + + @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 + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], + animation: .default) + + private var nodes: FetchedResults + + @State private var selection: NodeInfoEntity? // Nothing selected by default. + + var body: some View { + NavigationSplitView(columnVisibility: $columnVisibility) { + + let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + List(nodes, id: \.self, selection: $selection) { node in + + NodeListItem(node: node) + } + .searchable(text: nodesQuery, prompt: "Find a node") + .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) + .listStyle(.plain) + .navigationSplitViewColumnWidth(300) + .navigationBarItems(leading: + MeshtasticLogo() + ) + } content: { + + if let node = selection { + //NodeDetailItem(node: node) + NodeDetail(node: node) + .navigationSplitViewColumnWidth(300) + } else { + Text("select.node") + } + + } detail: { + Text("Content") + } + .navigationSplitViewStyle(.balanced) + +// } detail: { +// VStack { +// Button("Detail Only") { +// columnVisibility = .detailOnly +// } +// +// Button("Content and Detail") { +// columnVisibility = .doubleColumn +// } +// +// Button("Show All") { +// columnVisibility = .all +// } +// } +// } + } +}