Merge pull request #1089 from gh123man/cleanup-nodelist-view

Improve Node List Layout and Styling
This commit is contained in:
Garth Vander Houwen 2025-02-11 08:15:26 -08:00 committed by GitHub
commit 8a7d765acb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 502 additions and 164 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>"; };
@ -664,6 +666,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 = (
@ -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 */,

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

View file

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