Cleanup node list

Added preview for NodeListItem, Added CoreData bindings, Align all icons, Deduplicate code for list items, Fix list view padding for tab bar transparency
This commit is contained in:
Brian Floersch 2025-02-06 00:04:59 -05:00
parent 41a252649a
commit 4d0312623d
10 changed files with 503 additions and 165 deletions

View file

@ -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 = "<group>"; };
BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = "<group>"; };
BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactoryResetNodeIntent.swift; sourceTree = "<group>"; };
D32BA3912D54617800714840 /* NodeInfoEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NodeInfoEntity+CoreDataClass.swift"; sourceTree = "<group>"; };
D32BA3922D54617800714840 /* NodeInfoEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NodeInfoEntity+CoreDataProperties.swift"; sourceTree = "<group>"; };
D32BA3932D54617800714840 /* UserEntity+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserEntity+CoreDataClass.swift"; sourceTree = "<group>"; };
D32BA3942D54617800714840 /* UserEntity+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserEntity+CoreDataProperties.swift"; sourceTree = "<group>"; };
D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = "<group>"; };
D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = "<group>"; };
D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = "<group>"; };
@ -474,7 +477,6 @@
DDC2E16526CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = "<group>"; };
DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = "<group>"; };
DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorConfig.swift; sourceTree = "<group>"; };
DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV16.xcdatamodel; sourceTree = "<group>"; };
DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
@ -663,6 +665,17 @@
path = Custom;
sourceTree = "<group>";
};
D32BA3902D54612800714840 /* CoreData */ = {
isa = PBXGroup;
children = (
D32BA3912D54617800714840 /* NodeInfoEntity+CoreDataClass.swift */,
D32BA3922D54617800714840 /* NodeInfoEntity+CoreDataProperties.swift */,
D32BA3932D54617800714840 /* UserEntity+CoreDataClass.swift */,
D32BA3942D54617800714840 /* UserEntity+CoreDataProperties.swift */,
);
path = CoreData;
sourceTree = "<group>";
};
D9C9839E2B79D0C600BDBE6A /* TextMessageField */ = {
isa = PBXGroup;
children = (
@ -929,6 +942,7 @@
DDC2E15626CE248E0042C5E4 /* Meshtastic */ = {
isa = PBXGroup;
children = (
D32BA3902D54612800714840 /* CoreData */,
BCB6137F2C6728E700485544 /* AppIntents */,
DD1BD0EC2C603C5B008C0C70 /* Measurement */,
25F5D5BC2C3F6D7B008036E3 /* Router */,
@ -1021,7 +1035,6 @@
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */,
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */,
DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */,
DDC3B273283F411B00AC321C /* LastHeardText.swift */,
DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */,
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */,
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */,
@ -1478,7 +1491,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 */,

View file

@ -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 {
}

View file

@ -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<NodeInfoEntity> {
return NSFetchRequest<NodeInfoEntity>(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)
}

View file

@ -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 {
}

View file

@ -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<UserEntity> {
return NSFetchRequest<UserEntity>(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)
}

View file

@ -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

View file

@ -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"))
}
}

View file

@ -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@*/)

View file

@ -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).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)
}
}
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)
}
}

View file

@ -93,7 +93,7 @@ struct NodeList: View {
)
/// Don't show message, trace route, position exchange or delete context menu items for the connected node
if connectedNode.num != node.num {
if (!node.viaMqtt || node.viaMqtt && node.hopsAway == 0) {
if !node.viaMqtt || node.viaMqtt && node.hopsAway == 0 {
Button(action: {
if let url = URL(string: "meshtastic:///messages?userNum=\(node.num)") {
UIApplication.shared.open(url)
@ -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)