diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 081de5d6..5c8bdeb9 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -180,7 +180,6 @@ DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; }; DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */; }; DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; }; - DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC3B273283F411B00AC321C /* LastHeardText.swift */; }; DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */; }; DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; }; DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; }; @@ -305,6 +304,10 @@ BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = ""; }; BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = ""; }; BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactoryResetNodeIntent.swift; sourceTree = ""; }; + D32BA3912D54617800714840 /* NodeInfoEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NodeInfoEntity+CoreDataClass.swift"; sourceTree = ""; }; + D32BA3922D54617800714840 /* NodeInfoEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NodeInfoEntity+CoreDataProperties.swift"; sourceTree = ""; }; + D32BA3932D54617800714840 /* UserEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserEntity+CoreDataClass.swift"; sourceTree = ""; }; + D32BA3942D54617800714840 /* UserEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserEntity+CoreDataProperties.swift"; sourceTree = ""; }; D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = ""; }; D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = ""; }; D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; @@ -474,7 +477,6 @@ DDC2E16526CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = ""; }; - DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = ""; }; DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorConfig.swift; sourceTree = ""; }; DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV16.xcdatamodel; sourceTree = ""; }; DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; @@ -664,6 +666,17 @@ path = Custom; sourceTree = ""; }; + D32BA3902D54612800714840 /* CoreData */ = { + isa = PBXGroup; + children = ( + D32BA3912D54617800714840 /* NodeInfoEntity+CoreDataClass.swift */, + D32BA3922D54617800714840 /* NodeInfoEntity+CoreDataProperties.swift */, + D32BA3932D54617800714840 /* UserEntity+CoreDataClass.swift */, + D32BA3942D54617800714840 /* UserEntity+CoreDataProperties.swift */, + ); + path = CoreData; + sourceTree = ""; + }; D9C9839E2B79D0C600BDBE6A /* TextMessageField */ = { isa = PBXGroup; children = ( @@ -931,6 +944,7 @@ DDC2E15626CE248E0042C5E4 /* Meshtastic */ = { isa = PBXGroup; children = ( + D32BA3902D54612800714840 /* CoreData */, BCB6137F2C6728E700485544 /* AppIntents */, DD1BD0EC2C603C5B008C0C70 /* Measurement */, 25F5D5BC2C3F6D7B008036E3 /* Router */, @@ -1023,7 +1037,6 @@ DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */, DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */, - DDC3B273283F411B00AC321C /* LastHeardText.swift */, DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */, DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */, DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */, @@ -1488,7 +1501,6 @@ 231B3F212D087A4C0069A07D /* MetricTableColumn.swift in Sources */, 231B3F222D087A4C0069A07D /* MetricsColumnList.swift in Sources */, DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */, - DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, DDDE5A1029AFE69700490C6C /* MeshActivityAttributes.swift in Sources */, DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */, 251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */, diff --git a/Meshtastic/CoreData/NodeInfoEntity+CoreDataClass.swift b/Meshtastic/CoreData/NodeInfoEntity+CoreDataClass.swift new file mode 100644 index 00000000..37c14e81 --- /dev/null +++ b/Meshtastic/CoreData/NodeInfoEntity+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// NodeInfoEntity+CoreDataClass.swift +// +// +// Created by Brian Floersch on 2/5/25. +// +// + +import Foundation +import CoreData + +@objc(NodeInfoEntity) +public class NodeInfoEntity: NSManagedObject { + +} diff --git a/Meshtastic/CoreData/NodeInfoEntity+CoreDataProperties.swift b/Meshtastic/CoreData/NodeInfoEntity+CoreDataProperties.swift new file mode 100644 index 00000000..4df81fe2 --- /dev/null +++ b/Meshtastic/CoreData/NodeInfoEntity+CoreDataProperties.swift @@ -0,0 +1,200 @@ +// +// NodeInfoEntity+CoreDataProperties.swift +// +// +// Created by Brian Floersch on 2/5/25. +// +// + +import Foundation +import CoreData + +extension NodeInfoEntity { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "NodeInfoEntity") + } + + @NSManaged public var bleName: String? + @NSManaged public var channel: Int32 + @NSManaged public var favorite: Bool + @NSManaged public var firstHeard: Date? + @NSManaged public var hopsAway: Int32 + @NSManaged public var id: Int64 + @NSManaged public var ignored: Bool + @NSManaged public var lastHeard: Date? + @NSManaged public var num: Int64 + @NSManaged public var peripheralId: String? + @NSManaged public var rssi: Int32 + @NSManaged public var sessionExpiration: Date? + @NSManaged public var sessionPasskey: Data? + @NSManaged public var snr: Float + @NSManaged public var viaMqtt: Bool + @NSManaged public var ambientLightingConfig: AmbientLightingConfigEntity? + @NSManaged public var bluetoothConfig: BluetoothConfigEntity? + @NSManaged public var cannedMessageConfig: CannedMessageConfigEntity? + @NSManaged public var detectionSensorConfig: DetectionSensorConfigEntity? + @NSManaged public var deviceConfig: DeviceConfigEntity? + @NSManaged public var displayConfig: DisplayConfigEntity? + @NSManaged public var externalNotificationConfig: ExternalNotificationConfigEntity? + @NSManaged public var loRaConfig: LoRaConfigEntity? + @NSManaged public var metadata: DeviceMetadataEntity? + @NSManaged public var mqttConfig: MQTTConfigEntity? + @NSManaged public var myInfo: MyInfoEntity? + @NSManaged public var networkConfig: NetworkConfigEntity? + @NSManaged public var pax: NSOrderedSet? + @NSManaged public var paxCounterConfig: PaxCounterConfigEntity? + @NSManaged public var positionConfig: PositionConfigEntity? + @NSManaged public var positions: NSOrderedSet? + @NSManaged public var powerConfig: PowerConfigEntity? + @NSManaged public var rangeTestConfig: RangeTestConfigEntity? + @NSManaged public var rtttlConfig: RTTTLConfigEntity? + @NSManaged public var securityConfig: SecurityConfigEntity? + @NSManaged public var serialConfig: SerialConfigEntity? + @NSManaged public var storeForwardConfig: StoreForwardConfigEntity? + @NSManaged public var telemetries: NSOrderedSet? + @NSManaged public var telemetryConfig: TelemetryConfigEntity? + @NSManaged public var traceRoutes: NSOrderedSet? + @NSManaged public var user: UserEntity? + +} + +// MARK: Generated accessors for pax +extension NodeInfoEntity { + + @objc(insertObject:inPaxAtIndex:) + @NSManaged public func insertIntoPax(_ value: PaxCounterEntity, at idx: Int) + + @objc(removeObjectFromPaxAtIndex:) + @NSManaged public func removeFromPax(at idx: Int) + + @objc(insertPax:atIndexes:) + @NSManaged public func insertIntoPax(_ values: [PaxCounterEntity], at indexes: NSIndexSet) + + @objc(removePaxAtIndexes:) + @NSManaged public func removeFromPax(at indexes: NSIndexSet) + + @objc(replaceObjectInPaxAtIndex:withObject:) + @NSManaged public func replacePax(at idx: Int, with value: PaxCounterEntity) + + @objc(replacePaxAtIndexes:withPax:) + @NSManaged public func replacePax(at indexes: NSIndexSet, with values: [PaxCounterEntity]) + + @objc(addPaxObject:) + @NSManaged public func addToPax(_ value: PaxCounterEntity) + + @objc(removePaxObject:) + @NSManaged public func removeFromPax(_ value: PaxCounterEntity) + + @objc(addPax:) + @NSManaged public func addToPax(_ values: NSOrderedSet) + + @objc(removePax:) + @NSManaged public func removeFromPax(_ values: NSOrderedSet) + +} + +// MARK: Generated accessors for positions +extension NodeInfoEntity { + + @objc(insertObject:inPositionsAtIndex:) + @NSManaged public func insertIntoPositions(_ value: PositionEntity, at idx: Int) + + @objc(removeObjectFromPositionsAtIndex:) + @NSManaged public func removeFromPositions(at idx: Int) + + @objc(insertPositions:atIndexes:) + @NSManaged public func insertIntoPositions(_ values: [PositionEntity], at indexes: NSIndexSet) + + @objc(removePositionsAtIndexes:) + @NSManaged public func removeFromPositions(at indexes: NSIndexSet) + + @objc(replaceObjectInPositionsAtIndex:withObject:) + @NSManaged public func replacePositions(at idx: Int, with value: PositionEntity) + + @objc(replacePositionsAtIndexes:withPositions:) + @NSManaged public func replacePositions(at indexes: NSIndexSet, with values: [PositionEntity]) + + @objc(addPositionsObject:) + @NSManaged public func addToPositions(_ value: PositionEntity) + + @objc(removePositionsObject:) + @NSManaged public func removeFromPositions(_ value: PositionEntity) + + @objc(addPositions:) + @NSManaged public func addToPositions(_ values: NSOrderedSet) + + @objc(removePositions:) + @NSManaged public func removeFromPositions(_ values: NSOrderedSet) + +} + +// MARK: Generated accessors for telemetries +extension NodeInfoEntity { + + @objc(insertObject:inTelemetriesAtIndex:) + @NSManaged public func insertIntoTelemetries(_ value: TelemetryEntity, at idx: Int) + + @objc(removeObjectFromTelemetriesAtIndex:) + @NSManaged public func removeFromTelemetries(at idx: Int) + + @objc(insertTelemetries:atIndexes:) + @NSManaged public func insertIntoTelemetries(_ values: [TelemetryEntity], at indexes: NSIndexSet) + + @objc(removeTelemetriesAtIndexes:) + @NSManaged public func removeFromTelemetries(at indexes: NSIndexSet) + + @objc(replaceObjectInTelemetriesAtIndex:withObject:) + @NSManaged public func replaceTelemetries(at idx: Int, with value: TelemetryEntity) + + @objc(replaceTelemetriesAtIndexes:withTelemetries:) + @NSManaged public func replaceTelemetries(at indexes: NSIndexSet, with values: [TelemetryEntity]) + + @objc(addTelemetriesObject:) + @NSManaged public func addToTelemetries(_ value: TelemetryEntity) + + @objc(removeTelemetriesObject:) + @NSManaged public func removeFromTelemetries(_ value: TelemetryEntity) + + @objc(addTelemetries:) + @NSManaged public func addToTelemetries(_ values: NSOrderedSet) + + @objc(removeTelemetries:) + @NSManaged public func removeFromTelemetries(_ values: NSOrderedSet) + +} + +// MARK: Generated accessors for traceRoutes +extension NodeInfoEntity { + + @objc(insertObject:inTraceRoutesAtIndex:) + @NSManaged public func insertIntoTraceRoutes(_ value: TraceRouteEntity, at idx: Int) + + @objc(removeObjectFromTraceRoutesAtIndex:) + @NSManaged public func removeFromTraceRoutes(at idx: Int) + + @objc(insertTraceRoutes:atIndexes:) + @NSManaged public func insertIntoTraceRoutes(_ values: [TraceRouteEntity], at indexes: NSIndexSet) + + @objc(removeTraceRoutesAtIndexes:) + @NSManaged public func removeFromTraceRoutes(at indexes: NSIndexSet) + + @objc(replaceObjectInTraceRoutesAtIndex:withObject:) + @NSManaged public func replaceTraceRoutes(at idx: Int, with value: TraceRouteEntity) + + @objc(replaceTraceRoutesAtIndexes:withTraceRoutes:) + @NSManaged public func replaceTraceRoutes(at indexes: NSIndexSet, with values: [TraceRouteEntity]) + + @objc(addTraceRoutesObject:) + @NSManaged public func addToTraceRoutes(_ value: TraceRouteEntity) + + @objc(removeTraceRoutesObject:) + @NSManaged public func removeFromTraceRoutes(_ value: TraceRouteEntity) + + @objc(addTraceRoutes:) + @NSManaged public func addToTraceRoutes(_ values: NSOrderedSet) + + @objc(removeTraceRoutes:) + @NSManaged public func removeFromTraceRoutes(_ values: NSOrderedSet) + +} diff --git a/Meshtastic/CoreData/UserEntity+CoreDataClass.swift b/Meshtastic/CoreData/UserEntity+CoreDataClass.swift new file mode 100644 index 00000000..cd207492 --- /dev/null +++ b/Meshtastic/CoreData/UserEntity+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// UserEntity+CoreDataClass.swift +// +// +// Created by Brian Floersch on 2/5/25. +// +// + +import Foundation +import CoreData + +@objc(UserEntity) +public class UserEntity: NSManagedObject { + +} diff --git a/Meshtastic/CoreData/UserEntity+CoreDataProperties.swift b/Meshtastic/CoreData/UserEntity+CoreDataProperties.swift new file mode 100644 index 00000000..753bba68 --- /dev/null +++ b/Meshtastic/CoreData/UserEntity+CoreDataProperties.swift @@ -0,0 +1,108 @@ +// +// UserEntity+CoreDataProperties.swift +// +// +// Created by Brian Floersch on 2/5/25. +// +// + +import Foundation +import CoreData + +extension UserEntity { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "UserEntity") + } + + @NSManaged public var hwDisplayName: String? + @NSManaged public var hwModel: String? + @NSManaged public var hwModelId: Int32 + @NSManaged public var isLicensed: Bool + @NSManaged public var keyMatch: Bool + @NSManaged public var lastMessage: Date? + @NSManaged public var longName: String? + @NSManaged public var mute: Bool + @NSManaged public var newPublicKey: Data? + @NSManaged public var num: Int64 + @NSManaged public var numString: String? + @NSManaged public var pkiEncrypted: Bool + @NSManaged public var publicKey: Data? + @NSManaged public var role: Int32 + @NSManaged public var shortName: String? + @NSManaged public var userId: String? + @NSManaged public var receivedMessages: NSOrderedSet? + @NSManaged public var sentMessages: NSOrderedSet? + @NSManaged public var userNode: NodeInfoEntity? + +} + +// MARK: Generated accessors for receivedMessages +extension UserEntity { + + @objc(insertObject:inReceivedMessagesAtIndex:) + @NSManaged public func insertIntoReceivedMessages(_ value: MessageEntity, at idx: Int) + + @objc(removeObjectFromReceivedMessagesAtIndex:) + @NSManaged public func removeFromReceivedMessages(at idx: Int) + + @objc(insertReceivedMessages:atIndexes:) + @NSManaged public func insertIntoReceivedMessages(_ values: [MessageEntity], at indexes: NSIndexSet) + + @objc(removeReceivedMessagesAtIndexes:) + @NSManaged public func removeFromReceivedMessages(at indexes: NSIndexSet) + + @objc(replaceObjectInReceivedMessagesAtIndex:withObject:) + @NSManaged public func replaceReceivedMessages(at idx: Int, with value: MessageEntity) + + @objc(replaceReceivedMessagesAtIndexes:withReceivedMessages:) + @NSManaged public func replaceReceivedMessages(at indexes: NSIndexSet, with values: [MessageEntity]) + + @objc(addReceivedMessagesObject:) + @NSManaged public func addToReceivedMessages(_ value: MessageEntity) + + @objc(removeReceivedMessagesObject:) + @NSManaged public func removeFromReceivedMessages(_ value: MessageEntity) + + @objc(addReceivedMessages:) + @NSManaged public func addToReceivedMessages(_ values: NSOrderedSet) + + @objc(removeReceivedMessages:) + @NSManaged public func removeFromReceivedMessages(_ values: NSOrderedSet) + +} + +// MARK: Generated accessors for sentMessages +extension UserEntity { + + @objc(insertObject:inSentMessagesAtIndex:) + @NSManaged public func insertIntoSentMessages(_ value: MessageEntity, at idx: Int) + + @objc(removeObjectFromSentMessagesAtIndex:) + @NSManaged public func removeFromSentMessages(at idx: Int) + + @objc(insertSentMessages:atIndexes:) + @NSManaged public func insertIntoSentMessages(_ values: [MessageEntity], at indexes: NSIndexSet) + + @objc(removeSentMessagesAtIndexes:) + @NSManaged public func removeFromSentMessages(at indexes: NSIndexSet) + + @objc(replaceObjectInSentMessagesAtIndex:withObject:) + @NSManaged public func replaceSentMessages(at idx: Int, with value: MessageEntity) + + @objc(replaceSentMessagesAtIndexes:withSentMessages:) + @NSManaged public func replaceSentMessages(at indexes: NSIndexSet, with values: [MessageEntity]) + + @objc(addSentMessagesObject:) + @NSManaged public func addToSentMessages(_ value: MessageEntity) + + @objc(removeSentMessagesObject:) + @NSManaged public func removeFromSentMessages(_ value: MessageEntity) + + @objc(addSentMessages:) + @NSManaged public func addToSentMessages(_ values: NSOrderedSet) + + @objc(removeSentMessages:) + @NSManaged public func removeFromSentMessages(_ values: NSOrderedSet) + +} diff --git a/Meshtastic/Extensions/Date.swift b/Meshtastic/Extensions/Date.swift index 7fc72b33..0736fc63 100644 --- a/Meshtastic/Extensions/Date.swift +++ b/Meshtastic/Extensions/Date.swift @@ -9,6 +9,14 @@ import Foundation extension Date { + var lastHeard: String { + if timeIntervalSince1970 > 0 { + formatted() + } else { + "unknown" + } + } + func formattedDate(format: String) -> String { let dateformat = DateFormatter() dateformat.dateFormat = format diff --git a/Meshtastic/Views/Helpers/LastHeardText.swift b/Meshtastic/Views/Helpers/LastHeardText.swift deleted file mode 100644 index 5981d9c2..00000000 --- a/Meshtastic/Views/Helpers/LastHeardText.swift +++ /dev/null @@ -1,28 +0,0 @@ -import SwiftUI -// -// LastHeardText.swift -// Meshtastic Apple -// -// Created by Garth Vander Houwen on 5/25/22. -// -struct LastHeardText: View { - var lastHeard: Date? - - var body: some View { - if let lastHeard, lastHeard.timeIntervalSince1970 > 0 { - Text(lastHeard.formatted()) - } else { - Text("unknown") - } - } -} -struct LastHeardText_Previews: PreviewProvider { - static var previews: some View { - LastHeardText(lastHeard: Date()) - .previewLayout(.fixed(width: 300, height: 100)) - .environment(\.locale, .init(identifier: "en")) - LastHeardText(lastHeard: Date()) - .previewLayout(.fixed(width: 300, height: 100)) - .environment(\.locale, .init(identifier: "de")) - } -} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index fb019e0b..f68354c0 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -55,7 +55,7 @@ struct PositionPopover: View { if idiom != .phone { Text("heard".localized + ":") } - LastHeardText(lastHeard: position.time) + Text(position.time?.lastHeard ?? "unknown") .foregroundColor(.primary) .font(idiom == .phone ? .callout : .body) .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 788a95bf..5cae352d 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -15,12 +15,43 @@ struct NodeListItem: View { var connectedNode: Int64 var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast - var body: some View { + var userKeyStatus: (String, Color) { + var image = "lock.open.fill" + var color = Color.yellow + if node.user?.pkiEncrypted ?? false { + if !(node.user?.keyMatch ?? false) { + /// Public Key on the User and the Public Key on the Last Message don't match + image = "key.slash" + color = .red + } else { + image = "lock.fill" + color = .green + } + } + return (image, color) + } + var locationData: (PositionEntity, CLLocation)? { + guard let lastPostion = node.positions?.lastObject as? PositionEntity else { + return nil + } + guard let currentLocation = LocationsHandler.shared.locationsArray.last else { + return nil + } + + let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude) + + if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude { + return (lastPostion, myCoord) + } + return nil + } + + var body: some View { NavigationLink(value: node) { LazyVStack(alignment: .leading) { HStack { - VStack(alignment: .leading) { + VStack(alignment: .center) { CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70) .padding(.trailing, 5) if node.latestDeviceMetrics != nil { @@ -30,23 +61,11 @@ struct NodeListItem: View { } VStack(alignment: .leading) { HStack { - if node.user?.pkiEncrypted ?? false { - if !(node.user?.keyMatch ?? false) { - /// Public Key on the User and the Public Key on the Last Message don't match - Image(systemName: "key.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } - } else { - Image(systemName: "lock.open.fill") - .foregroundColor(.yellow) - } - Text(node.user?.longName ?? "unknown".localized) - .font(.headline) - .fontWeight(.regular) - .allowsTightening(true) + let (image, color) = userKeyStatus + IconAndText(systemName: image, + imageColor: color, + text: node.user?.longName ?? "unknown".localized, + textColor: .primary) if node.favorite { Spacer() Image(systemName: "star.fill") @@ -54,149 +73,82 @@ struct NodeListItem: View { } } if connected { - HStack { - Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .font(.callout) - .symbolRenderingMode(.hierarchical) - .foregroundColor(.green) - .frame(width: 30) - Text("connected") - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.gray) - } - } - HStack { - Image(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill") - .font(.callout) - .symbolRenderingMode(.hierarchical) - .foregroundColor(node.isOnline ? .green : .orange) - .frame(width: 30) - LastHeardText(lastHeard: node.lastHeard) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.gray) - } - HStack { - let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) - Image(systemName: role?.systemName ?? "figure") - .font(.callout) - .symbolRenderingMode(.hierarchical) - .frame(width: 30) - Text("Role: \(role?.name ?? "unknown".localized)") - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.gray) - + IconAndText(systemName: "antenna.radiowaves.left.and.right.circle.fill", + imageColor: .green, + text: "connected".localized) } + IconAndText(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill", + imageColor: node.isOnline ? .green : .orange, + text: node.lastHeard?.lastHeard ?? "unknown") + let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) + IconAndText(systemName: role?.systemName ?? "figure", + text: "Role: \(role?.name ?? "unknown".localized)") if node.isStoreForwardRouter { - HStack { - Image(systemName: "envelope.arrow.triangle.branch") - .font(.callout) - .symbolRenderingMode(.multicolor) - .frame(width: 30) - Text("storeforward".localized) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.secondary) - } + IconAndText(systemName: "envelope.arrow.triangle.branch", + renderingMode: .multicolor, + text: "storeforward".localized) } if node.positions?.count ?? 0 > 0 && connectedNode != node.num { HStack { - if let lastPostion = node.positions?.lastObject as? PositionEntity { - if let currentLocation = LocationsHandler.shared.locationsArray.last { - let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude) - if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.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(.callout) - .symbolRenderingMode(.multicolor) - .frame(width: 30) - DistanceText(meters: metersAway) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.gray) - let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) - let headingDegrees = Measurement(value: trueBearing, unit: UnitAngle.degrees) - Image(systemName: "location.north") - .font(.callout) - .symbolRenderingMode(.multicolor) - .clipShape(Circle()) - .rotationEffect(Angle(degrees: headingDegrees.value)) - let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees) - Text("\(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))") - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - .foregroundColor(.gray) - } - } + if let (lastPostion, myCoord) = locationData { + let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) + let metersAway = nodeCoord.distance(from: myCoord) + Image(systemName: "lines.measurement.horizontal") + .font(.callout) + .symbolRenderingMode(.multicolor) + .frame(width: 30) + DistanceText(meters: metersAway) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(.gray) + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) + let headingDegrees = Measurement(value: trueBearing, unit: UnitAngle.degrees).reciprocal() + Image(systemName: "location.north") + .font(.callout) + .symbolRenderingMode(.multicolor) + .clipShape(Circle()) + .rotationEffect(Angle(degrees: headingDegrees.value)) + let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees).reciprocal() + Text("\(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))") + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(.gray) } } } HStack { if node.channel > 0 { - HStack { - Image(systemName: "\(node.channel).circle.fill") - .font(.title2) - .frame(width: 30) - Text("Channel") - .foregroundColor(.secondary) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) - } + IconAndText(systemName: "\(node.channel).circle.fill", text: "Channel") } if node.viaMqtt && connectedNode != node.num { - Image(systemName: "dot.radiowaves.up.forward") - .symbolRenderingMode(.multicolor) - .font(.callout) - .frame(width: 30) - Text("MQTT") - .foregroundColor(.gray) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + IconAndText(systemName: "dot.radiowaves.up.forward", + renderingMode: .multicolor, + text: "MQTT") } } if node.hasPositions || node.hasEnvironmentMetrics || node.hasDetectionSensorMetrics || node.hasTraceRoutes { HStack { - Image(systemName: "scroll") - .symbolRenderingMode(.hierarchical) - .font(.callout) - Text("Logs:") - .foregroundColor(.gray) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption2) - .allowsTightening(true) + IconAndText(systemName: "scroll", text: "Logs:") if node.hasDeviceMetrics { - Image(systemName: "flipphone") - .symbolRenderingMode(.hierarchical) - .font(.callout) + DefaultIcon(systemName: "flipphone") } if node.hasPositions { - Image(systemName: "mappin.and.ellipse") - .symbolRenderingMode(.hierarchical) - .font(.callout) - + DefaultIcon(systemName: "mappin.and.ellipse") } if node.hasEnvironmentMetrics { - Image(systemName: "cloud.sun.rain") - .symbolRenderingMode(.hierarchical) - .font(.callout) - + DefaultIcon(systemName: "cloud.sun.rain") } if node.hasDetectionSensorMetrics { - Image(systemName: "sensor") - .symbolRenderingMode(.hierarchical) - .font(.callout) + DefaultIcon(systemName: "sensor") } if node.hasTraceRoutes { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - .font(.callout) + DefaultIcon(systemName: "signpost.right.and.left") } } } if node.hopsAway > 0 { HStack { - Image(systemName: "hare") - .font(.callout) - .symbolRenderingMode(.multicolor) - Text("Hops Away:") - .foregroundColor(.secondary) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + IconAndText(systemName: "hare", text: "Hops Away:") Image(systemName: "\(node.hopsAway).square") .font(.title2) } @@ -215,3 +167,60 @@ struct NodeListItem: View { .padding(.bottom, 4) } } + +struct DefaultIcon: View { + let systemName: String + + var body: some View { + Image(systemName: systemName) + .symbolRenderingMode(.hierarchical) + .font(.callout) + } +} + +struct IconAndText: View { + let systemName: String + var imageColor: Color? + var renderingMode: SymbolRenderingMode = .hierarchical + let text: String + var textColor: Color = .gray + + @ViewBuilder + var image: some View { + if let color = imageColor { + Image(systemName: systemName) + .foregroundColor(color) + } else { + Image(systemName: systemName) + } + } + + var body: some View { + HStack { + image + .font(.callout) + .symbolRenderingMode(renderingMode) + .frame(width: 30) + Text(text) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .foregroundColor(textColor) + .allowsTightening(true) + } + } +} + +#Preview { + VStack(alignment: .leading) { + IconAndText(systemName: "antenna.radiowaves.left.and.right.circle.fill", text: "foo") + IconAndText(systemName: "antenna.radiowaves.left.and.right.circle", text: "bar") + NodeListItem(node: { + let context = PersistenceController.preview.container.viewContext + let nodeInfo = NodeInfoEntity(context: context) + let user = UserEntity(context: context) + user.longName = "Test User" + user.shortName = "TU" + nodeInfo.user = user + return nodeInfo + }(), connected: true, connectedNode: 0, modemPreset: .longFast) + } +} diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 3ba74de8..6d007c56 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -200,7 +200,6 @@ struct NodeList: View { .controlSize(.regular) .padding(5) } - .padding(.bottom, 5) .searchable(text: $searchText, placement: .automatic, prompt: "Find a node") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately)