From f84eda21d0ec46e782956788ff128a7f6871120c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 7 Sep 2023 17:45:30 -0700 Subject: [PATCH 01/16] Update project.pbxproj Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 765de0df..e800a969 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1381,7 +1381,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1415,7 +1415,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1537,7 +1537,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1570,7 +1570,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.4; + MARKETING_VERSION = 2.2.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 2cf72f7d3d851beacd6377c570082d9d56d250fd Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 7 Sep 2023 23:38:53 -0700 Subject: [PATCH 02/16] Clean up node details --- Meshtastic.xcodeproj/project.pbxproj | 4 ++++ .../CoreData/NodeInfoEntityExtension.swift | 9 ++++++++ Meshtastic/Views/Helpers/DateTimeText.swift | 10 ++++----- Meshtastic/Views/Helpers/DistanceText.swift | 2 +- Meshtastic/Views/Helpers/LastHeardText.swift | 9 +++++++- .../Views/Helpers/Node/NodeListDetail.swift | 19 ++++++++++++++++ Meshtastic/Views/Nodes/NodeList.swift | 22 ++++++++++--------- en.lproj/Localizable.strings | 2 +- 8 files changed, 59 insertions(+), 18 deletions(-) create mode 100644 Meshtastic/Views/Helpers/Node/NodeListDetail.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 765de0df..697f9463 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ 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 */; }; @@ -323,6 +324,7 @@ 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 = ""; }; @@ -831,6 +833,7 @@ isa = PBXGroup; children = ( DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */, + DDC18AD02AAAE5920083FE1E /* NodeListDetail.swift */, ); path = Node; sourceTree = ""; @@ -1082,6 +1085,7 @@ 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 */, diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index d24f69f1..b57e6ff4 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -24,4 +24,13 @@ extension NodeInfoEntity { let environmentMetrics = telemetries?.filter{ ($0 as AnyObject).metricsType == 1 } return environmentMetrics?.count ?? 0 > 0 } + + var isOnline: Bool { + + let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date()) + if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending { + return true + } + return false + } } diff --git a/Meshtastic/Views/Helpers/DateTimeText.swift b/Meshtastic/Views/Helpers/DateTimeText.swift index 0fcabf76..6b61afd0 100644 --- a/Meshtastic/Views/Helpers/DateTimeText.swift +++ b/Meshtastic/Views/Helpers/DateTimeText.swift @@ -16,14 +16,14 @@ struct DateTimeText: View { var dateTime: Date? let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) - + let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current) + var body: some View { + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a") + if dateTime != nil && dateTime! >= sixMonthsAgo! { - - Text("\(dateTime!, style: .date) \(dateTime!, style: .time)") - + Text(" \(dateTime!.formattedDate(format: dateFormatString))") } else { - Text("unknown.age") } } diff --git a/Meshtastic/Views/Helpers/DistanceText.swift b/Meshtastic/Views/Helpers/DistanceText.swift index 0812e14f..67f9419c 100644 --- a/Meshtastic/Views/Helpers/DistanceText.swift +++ b/Meshtastic/Views/Helpers/DistanceText.swift @@ -16,7 +16,7 @@ struct DistanceText: View { var body: some View { let distanceFormatter = MKDistanceFormatter() - Text("distance")+Text(": \(distanceFormatter.string(fromDistance: Double(meters)))") + Text("\(distanceFormatter.string(fromDistance: Double(meters))) away") } } struct DistanceText_Previews: PreviewProvider { diff --git a/Meshtastic/Views/Helpers/LastHeardText.swift b/Meshtastic/Views/Helpers/LastHeardText.swift index 3a207b6d..97acce81 100644 --- a/Meshtastic/Views/Helpers/LastHeardText.swift +++ b/Meshtastic/Views/Helpers/LastHeardText.swift @@ -8,9 +8,16 @@ import SwiftUI struct LastHeardText: View { var lastHeard: Date? let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) + + static let formatter: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + return formatter + }() + var body: some View { if lastHeard != nil && lastHeard! >= sixMonthsAgo! { - Text("heard")+Text(" \(lastHeard!, style: .relative) ")+Text("ago") + Text("heard")+Text(" \(LastHeardText.formatter.localizedString(for: lastHeard!, relativeTo: Date.now))") } else { Text("unknown.age") } diff --git a/Meshtastic/Views/Helpers/Node/NodeListDetail.swift b/Meshtastic/Views/Helpers/Node/NodeListDetail.swift new file mode 100644 index 00000000..43dd76f9 --- /dev/null +++ b/Meshtastic/Views/Helpers/Node/NodeListDetail.swift @@ -0,0 +1,19 @@ +// +// 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/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 8cd324e5..104412c9 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -61,13 +61,21 @@ struct NodeList: View { .font(.callout) if connected { HStack(alignment: .bottom) { - Image(systemName: "repeat.circle.fill") - .font(.callout) + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .font(.footnote) .symbolRenderingMode(.hierarchical) - Text("connected").font(.callout) .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 @@ -91,13 +99,6 @@ struct NodeList: View { .font(.footnote) } } - HStack(alignment: .bottom) { - Image(systemName: "clock.badge.checkmark.fill") - .font(.caption) - .symbolRenderingMode(.hierarchical) - LastHeardText(lastHeard: node.lastHeard) - .font(.caption) - } 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) @@ -112,6 +113,7 @@ struct NodeList: View { } } .listStyle(.plain) + .navigationSplitViewColumnWidth(300) .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) .navigationBarItems(leading: MeshtasticLogo() diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 97b8db37..f32414d3 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -50,7 +50,7 @@ "config.save.confirm"="After config values save the node will reboot."; "communicating"="Communicating with device. ."; "connected.radio"="Connected Radio"; -"connected"="Connected"; +"connected"="Bluetooth Connected"; "connecting"="Connecting . ."; "contacts"="Contacts"; "contacts %@"="Contacts (%@)"; From 9fccb74f435ac67deeac83c82e0c66c346d8b108 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 8 Sep 2023 18:52:59 -0700 Subject: [PATCH 03/16] Three column working point --- Meshtastic.xcodeproj/project.pbxproj | 32 ++++++- Meshtastic/Extensions/UserDefaults.swift | 1 - Meshtastic/Views/ContentView.swift | 6 ++ .../Views/Helpers/Node/NodeListDetail.swift | 19 ---- .../Views/Messages/ChannelMessageList.swift | 6 +- .../Views/Messages/UserMessageList.swift | 2 +- .../Views/Nodes/Helpers/NodeDetailItem.swift | 30 +++++++ .../Views/Nodes/Helpers/NodeListItem.swift | 89 +++++++++++++++++++ Meshtastic/Views/Nodes/NodeList.swift | 8 +- Meshtastic/Views/Nodes/NodeListSplit.swift | 82 +++++++++++++++++ 10 files changed, 245 insertions(+), 30 deletions(-) delete mode 100644 Meshtastic/Views/Helpers/Node/NodeListDetail.swift create mode 100644 Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift create mode 100644 Meshtastic/Views/Nodes/Helpers/NodeListItem.swift create mode 100644 Meshtastic/Views/Nodes/NodeListSplit.swift 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 +// } +// } +// } + } +} From d9a424388a72efdea3d95b992ec777ccb2b5fc55 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 9 Sep 2023 09:00:34 -0700 Subject: [PATCH 04/16] Working 3 column nodes view --- Meshtastic.xcodeproj/project.pbxproj | 10 +- .../Views/Helpers/LoRaSignalStrength.swift | 1 + .../Views/Nodes/Helpers/NodeDetailItem.swift | 115 ++++++++++-- .../Views/Nodes/Helpers/NodeInfoItem.swift | 174 ++++++++++++++++++ .../Views/Nodes/Helpers/NodeListItem.swift | 49 ++--- Meshtastic/Views/Nodes/NodeDetail.swift | 2 +- Meshtastic/Views/Nodes/NodeListSplit.swift | 9 +- Meshtastic/Views/Nodes/PositionLog.swift | 5 +- Meshtastic/Views/Nodes/RemoteHardware.swift | 8 - 9 files changed, 315 insertions(+), 58 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift delete mode 100644 Meshtastic/Views/Nodes/RemoteHardware.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e2a7efd6..208e0b8d 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; }; DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; }; DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */; }; - DD14E72E2A82A614006E39BC /* RemoteHardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD14E72D2A82A614006E39BC /* RemoteHardware.swift */; }; DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */; }; DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B828CDA93900720036 /* SerialConfigEnums.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; @@ -138,6 +137,7 @@ 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 */; }; + DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.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 */; }; @@ -217,7 +217,6 @@ DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = ""; }; DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = ""; }; DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV15.xcdatamodel; sourceTree = ""; }; - DD14E72D2A82A614006E39BC /* RemoteHardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteHardware.swift; sourceTree = ""; }; DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfigEnums.swift; sourceTree = ""; }; DD1925B828CDA93900720036 /* SerialConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfigEnums.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; @@ -357,6 +356,7 @@ 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 = ""; }; + DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoItem.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 = ""; }; @@ -471,7 +471,6 @@ DD47E3CD26F103C600029299 /* NodeList.swift */, DD90860D26F69BAE00DC5189 /* NodeMap.swift */, DD73FD1028750779000852D6 /* PositionLog.swift */, - DD14E72D2A82A614006E39BC /* RemoteHardware.swift */, 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, DDDB263E2AABEE20003AFCB7 /* NodeListSplit.swift */, ); @@ -809,8 +808,9 @@ DDDB26402AABEF7B003AFCB7 /* Helpers */ = { isa = PBXGroup; children = ( - DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */, + DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */, + DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, ); path = Helpers; sourceTree = ""; @@ -1093,6 +1093,7 @@ DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, DDDB445429F8AD1600EE2349 /* Data.swift in Sources */, + DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */, DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */, DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, @@ -1194,7 +1195,6 @@ DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, - DD14E72E2A82A614006E39BC /* RemoteHardware.swift in Sources */, DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */, diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index c415f8fb..444083ae 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -26,6 +26,7 @@ struct LoRaSignalStrengthMeter: View { .foregroundColor(getRssiColor(rssi: rssi)) .font(.caption2) } + .padding(.bottom, 2) } else { Gauge(value: Double(signalStrength.rawValue), in: 0...3) { } currentValueLabel: { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift index dc22ca2d..84b0a3b6 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift @@ -1,9 +1,7 @@ -// -// NodeDetailItem.swift -// Meshtastic -// -// Created by Garth Vander Houwen on 9/8/23. -// +/* + Abstract: + A view showing the details for a node. + */ import SwiftUI import WeatherKit @@ -11,20 +9,115 @@ import MapKit import CoreLocation struct NodeDetailItem: View { - - var node: NodeInfoEntity + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @Environment(\.colorScheme) var colorScheme: ColorScheme + @AppStorage("meshMapType") private var meshMapType = 0 + @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false + @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false + @State private var selectedMapLayer: MapLayer = .standard + @State var waypointCoordinate: WaypointCoordinate? + @State var editingWaypoint: Int = 0 + @State private var loadedWeather: Bool = false + @State private var showingDetailsPopover = false + @State private var showingForecast = false + @State private var showingShutdownConfirm: Bool = false + @State private var showingRebootConfirm: Bool = false + @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( + mapName: "offlinemap", + tileType: "png", + canReplaceMapContent: true + ) + @ObservedObject 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 - - + /// The current weather condition for the city. + @State private var condition: WeatherCondition? + @State private var temperature: Measurement? + @State private var humidity: Int? + @State private var symbolName: String = "cloud.fill" + + @State private var attributionLink: URL? + @State private var attributionLogo: URL? + var body: some View { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) NavigationStack { - + GeometryReader { bounds in + VStack { + ScrollView { + NodeInfoItem(node: node) + if self.bleManager.connectedPeripheral != nil && node.metadata != nil { + HStack { + if node.metadata?.canShutdown ?? false { + + Button(action: { + showingShutdownConfirm = true + }) { + Label("Power Off", systemImage: "power") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .confirmationDialog( + "are.you.sure", + isPresented: $showingShutdownConfirm + ) { + Button("Shutdown Node?", role: .destructive) { + if !bleManager.sendShutdown(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { + print("Shutdown Failed") + } + } + } + } + + Button(action: { + showingRebootConfirm = true + }) { + Label("reboot", systemImage: "arrow.triangle.2.circlepath") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .confirmationDialog("are.you.sure", + isPresented: $showingRebootConfirm + ) { + Button("reboot.node", role: .destructive) { + if !bleManager.sendReboot(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { + print("Reboot Failed") + } + } + } + } + .padding(5) + Divider() + } + } + } + .edgesIgnoringSafeArea([.leading, .trailing]) + .sheet(item: $waypointCoordinate, content: { wpc in + WaypointFormView(coordinate: wpc) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) + }) + .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) + .navigationBarItems(trailing: + ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + }) + } + .padding(.bottom, 2) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift new file mode 100644 index 00000000..26c3c2fc --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -0,0 +1,174 @@ +// +// NodeInfoItem.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 9/9/23. +// + +import SwiftUI +import CoreLocation +import MapKit + +struct NodeInfoItem: View { + + var node: NodeInfoEntity + + enum SelectedDetail { + case positionLog + case nodeMap + case deviceMetricsLog + case environmentMetricsLog + case detectionSensorLog + } + + var body: some View { + + Divider() + if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + + } else { + + } + + HStack { + + VStack(alignment: .center) { + CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) + } + if node.user != nil { + Divider() + VStack { + Image(node.user!.hwModel ?? "unset".localized) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 75, height: 75) + .cornerRadius(5) + Text(String(node.user!.hwModel ?? "unset".localized)) + .font(.caption2).fixedSize() + } + } + if node.snr != 0 { + Divider() + VStack(alignment: .center) { + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", node.snr))dB") + .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) + .font(.caption2) + Text("RSSI \(node.rssi)dB") + .foregroundColor(getRssiColor(rssi: node.rssi)) + .font(.caption2) + } + } + let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) + if deviceMetrics?.count ?? 0 >= 1 { + Divider() + let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + VStack(alignment: .center) { + BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) + if mostRecent?.voltage ?? 0 > 0 { + + Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V") + .font(.callout) + .foregroundColor(.gray) + .fixedSize() + } + } + } + } + Divider() + VStack(alignment: .center) { + VStack { + HStack { + Image(systemName: "number") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("Node Number:").font(.title2) + } + Text(String(node.num)).font(.title3).foregroundColor(.gray) + } + Divider() + VStack { + HStack { + Image(systemName: "person") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("User Id:").font(.title2) + } + Text(node.user?.userId ?? "?").font(.title3).foregroundColor(.gray) + } + Divider() + } + + VStack { + // List { + if node.hasPositions { + + NavigationLink { + PositionLog(node: node) + .onAppear { + + } + } label: { + + Image(systemName: "building.columns") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Position Log") + .font(.title3) + } + .fixedSize(horizontal: false, vertical: true) + Divider() + } + + if node.hasDeviceMetrics { + + NavigationLink { + DeviceMetricsLog(node: node) + } label: { + + Image(systemName: "flipphone") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Device Metrics Log") + .font(.title3) + } + Divider() + } + if node.hasEnvironmentMetrics { + NavigationLink { + EnvironmentMetricsLog(node: node) + } label: { + + Image(systemName: "chart.xyaxis.line") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Environment Metrics Log") + .font(.title3) + } + Divider() + } + NavigationLink { + DetectionSensorLog(node: node) + } label: { + + Image(systemName: "sensor") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Detection Sensor Log") + .font(.title3) + } + .fixedSize(horizontal: false, vertical: true) + Divider() + // } + // .listStyle(.plain) + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 5574e1dc..d619c379 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -6,18 +6,18 @@ // import SwiftUI - - - +import CoreLocation struct NodeListItem: View { - @StateObject var node: NodeInfoEntity + public var node: NodeInfoEntity + var connected: Bool + var connectedNode: Int64 + var modemPreset: Int 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) { @@ -50,20 +50,20 @@ struct NodeListItem: View { 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.positions?.count ?? 0 > 0 && connectedNode != 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") @@ -74,11 +74,12 @@ struct NodeListItem: View { } } -// 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) -// } -// } + if !connected { + HStack(alignment: .bottom) { + let preset = ModemPresets(rawValue: Int(modemPreset)) + LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true) + } + } } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 99ffcd04..356e9e59 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -44,7 +44,7 @@ struct NodeDetail: View { @State private var attributionLink: URL? @State private var attributionLogo: URL? - + var body: some View { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) diff --git a/Meshtastic/Views/Nodes/NodeListSplit.swift b/Meshtastic/Views/Nodes/NodeListSplit.swift index b181b859..a83667d1 100644 --- a/Meshtastic/Views/Nodes/NodeListSplit.swift +++ b/Meshtastic/Views/Nodes/NodeListSplit.swift @@ -39,21 +39,20 @@ struct NodeListSplit: View { let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) List(nodes, id: \.self, selection: $selection) { node in - NodeListItem(node: node) + NodeListItem(node: node, connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0)) } .searchable(text: nodesQuery, prompt: "Find a node") .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) .listStyle(.plain) - .navigationSplitViewColumnWidth(300) + .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) .navigationBarItems(leading: MeshtasticLogo() ) } content: { if let node = selection { - //NodeDetailItem(node: node) - NodeDetail(node: node) - .navigationSplitViewColumnWidth(300) + NodeDetailItem(node: node) + } else { Text("select.node") } diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 4cd48761..2322a8d2 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -19,11 +19,8 @@ struct PositionLog: View { @State var exportString = "" var node: NodeInfoEntity @State private var isPresentingClearLogConfirm = false - //@State private var sortOrder = [KeyPathComparator(\PositionEntity.latitude)] + @State private var sortOrder = [KeyPathComparator(\PositionEntity.time)] - @State var sortOrder: [KeyPathComparator] = [ - .init(\.latitude, order: SortOrder.forward) - ] var body: some View { NavigationStack { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) diff --git a/Meshtastic/Views/Nodes/RemoteHardware.swift b/Meshtastic/Views/Nodes/RemoteHardware.swift deleted file mode 100644 index 42ddc4d4..00000000 --- a/Meshtastic/Views/Nodes/RemoteHardware.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// RemoteHardware.swift -// Meshtastic -// -// Created by Garth Vander Houwen on 8/8/23. -// - -import Foundation From 2d339037ae0334a1bb567173da6d8c5dc744e4e3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 9 Sep 2023 11:29:08 -0700 Subject: [PATCH 05/16] Fully update position log in 3 column mode --- Meshtastic.xcodeproj/project.pbxproj | 4 ++ .../Views/Nodes/Helpers/NodeDetailItem.swift | 34 +++----------- .../Views/Nodes/Helpers/NodeInfoItem.swift | 46 ++++++++----------- .../Views/Nodes/Helpers/NodeListItem.swift | 2 +- .../Views/Nodes/Helpers/NodeMapControl.swift | 8 ++++ Meshtastic/Views/Nodes/NodeList.swift | 7 ++- Meshtastic/Views/Nodes/NodeListSplit.swift | 24 ++++++++-- Meshtastic/Views/Nodes/PositionLog.swift | 20 ++++++-- 8 files changed, 77 insertions(+), 68 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 208e0b8d..3f6c4300 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -138,6 +138,7 @@ DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26412AABF655003AFCB7 /* NodeListItem.swift */; }; DDDB26442AAC0206003AFCB7 /* NodeDetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */; }; DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */; }; + DDDB26482AACD6D1003AFCB7 /* NodeMapControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26472AACD6D1003AFCB7 /* NodeMapControl.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 */; }; @@ -357,6 +358,7 @@ DDDB26412AABF655003AFCB7 /* NodeListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListItem.swift; sourceTree = ""; }; DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetailItem.swift; sourceTree = ""; }; DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoItem.swift; sourceTree = ""; }; + DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapControl.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 = ""; }; @@ -811,6 +813,7 @@ DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */, DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */, DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, + DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */, ); path = Helpers; sourceTree = ""; @@ -1101,6 +1104,7 @@ DDDB263F2AABEE20003AFCB7 /* NodeListSplit.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, + DDDB26482AACD6D1003AFCB7 /* NodeMapControl.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift index 84b0a3b6..e1f553b8 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift @@ -13,14 +13,6 @@ struct NodeDetailItem: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @Environment(\.colorScheme) var colorScheme: ColorScheme - @AppStorage("meshMapType") private var meshMapType = 0 - @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false - @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false - @State private var selectedMapLayer: MapLayer = .standard - @State var waypointCoordinate: WaypointCoordinate? - @State var editingWaypoint: Int = 0 - @State private var loadedWeather: Bool = false - @State private var showingDetailsPopover = false @State private var showingForecast = false @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false @@ -29,21 +21,7 @@ struct NodeDetailItem: View { tileType: "png", canReplaceMapContent: true ) - @ObservedObject 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 - - /// The current weather condition for the city. - @State private var condition: WeatherCondition? - @State private var temperature: Measurement? - @State private var humidity: Int? - @State private var symbolName: String = "cloud.fill" - - @State private var attributionLink: URL? - @State private var attributionLogo: URL? + var node: NodeInfoEntity var body: some View { @@ -102,12 +80,12 @@ struct NodeDetailItem: View { } } } + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + } .edgesIgnoringSafeArea([.leading, .trailing]) - .sheet(item: $waypointCoordinate, content: { wpc in - WaypointFormView(coordinate: wpc) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) - }) .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) .navigationBarItems(trailing: ZStack { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 26c3c2fc..3d36bcb0 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -12,14 +12,6 @@ import MapKit struct NodeInfoItem: View { var node: NodeInfoEntity - - enum SelectedDetail { - case positionLog - case nodeMap - case deviceMetricsLog - case environmentMetricsLog - case detectionSensorLog - } var body: some View { @@ -105,26 +97,7 @@ struct NodeInfoItem: View { VStack { // List { - if node.hasPositions { - - NavigationLink { - PositionLog(node: node) - .onAppear { - } - } label: { - - Image(systemName: "building.columns") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Position Log") - .font(.title3) - } - .fixedSize(horizontal: false, vertical: true) - Divider() - } - if node.hasDeviceMetrics { NavigationLink { @@ -154,6 +127,25 @@ struct NodeInfoItem: View { } Divider() } + if node.hasPositions { + + NavigationLink { + PositionLog(node: node) + .onAppear { + + } + } label: { + + Image(systemName: "building.columns") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Position Log") + .font(.title3) + } + .fixedSize(horizontal: false, vertical: true) + Divider() + } NavigationLink { DetectionSensorLog(node: node) } label: { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index d619c379..432901d6 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -10,7 +10,7 @@ import CoreLocation struct NodeListItem: View { - public var node: NodeInfoEntity + @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 var modemPreset: Int diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift new file mode 100644 index 00000000..78a4d5c7 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift @@ -0,0 +1,8 @@ +// +// NodeMapControl.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/9/23. +// + +import Foundation diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index e949b7a2..3ab4a478 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -13,9 +13,6 @@ import CoreLocation struct NodeList: View { - init () { - //self.bleManager.context = context - } @State private var searchText = "" var nodesQuery: Binding { Binding { @@ -123,7 +120,9 @@ struct NodeList: View { MeshtasticLogo() ) .onAppear { - // self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } } } detail: { if let node = selection { diff --git a/Meshtastic/Views/Nodes/NodeListSplit.swift b/Meshtastic/Views/Nodes/NodeListSplit.swift index a83667d1..6ac85836 100644 --- a/Meshtastic/Views/Nodes/NodeListSplit.swift +++ b/Meshtastic/Views/Nodes/NodeListSplit.swift @@ -7,9 +7,22 @@ import SwiftUI import CoreLocation +enum SelectedDetail { + case positionLog + case nodeMap + case deviceMetricsLog + case environmentMetricsLog + case detectionSensorLog +} + struct NodeListSplit: View { + // Layout variables @State private var columnVisibility = NavigationSplitViewVisibility.all + @State private var selectedNode: NodeInfoEntity? + @State private var selectedDetail: SelectedDetail? + + @SceneStorage("selectedDetailView") var selectedDetailView: String? @State private var searchText = "" var nodesQuery: Binding { @@ -30,14 +43,14 @@ struct NodeListSplit: View { 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 + List(nodes, id: \.self, selection: $selectedNode) { node in NodeListItem(node: node, connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0)) } @@ -50,7 +63,7 @@ struct NodeListSplit: View { ) } content: { - if let node = selection { + if let node = selectedNode { NodeDetailItem(node: node) } else { @@ -61,6 +74,11 @@ struct NodeListSplit: View { Text("Content") } .navigationSplitViewStyle(.balanced) + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + } // } detail: { // VStack { diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 2322a8d2..2100f955 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -17,12 +17,19 @@ struct PositionLog: View { } @State var isExporting = false @State var exportString = "" - var node: NodeInfoEntity + @ObservedObject var node: NodeInfoEntity @State private var isPresentingClearLogConfirm = false @State private var sortOrder = [KeyPathComparator(\PositionEntity.time)] var body: some View { NavigationStack { + + + if node.hasPositions { + + } else { + Text("Node has no positions.") + } let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") if UIDevice.current.userInterfaceIdiom == .pad && !useGrid || UIDevice.current.userInterfaceIdiom == .mac { @@ -159,12 +166,15 @@ struct PositionLog: View { ) } .navigationTitle("Position Log \(node.positions?.count ?? 0) Points") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + .navigationBarItems( + trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } } } } From dad6654374ab3f15b8819a161656569f9b4bc4db Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 9 Sep 2023 18:43:51 -0700 Subject: [PATCH 06/16] Hook up updates to all node list details views --- Meshtastic.xcodeproj/project.pbxproj | 4 +- .../Views/Nodes/DetectionSensorLog.swift | 2 +- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 2 +- .../Views/Nodes/EnvironmentMetricsLog.swift | 2 +- .../Views/Nodes/Helpers/NodeDetailItem.swift | 72 +++++++++++++++-- .../Views/Nodes/Helpers/NodeInfoItem.swift | 79 +------------------ .../Views/Nodes/Helpers/NodeMapControl.swift | 12 ++- Meshtastic/Views/Nodes/NodeListSplit.swift | 6 +- 8 files changed, 88 insertions(+), 91 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 3f6c4300..ea9b95f4 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -467,11 +467,11 @@ isa = PBXGroup; children = ( DDDB26402AABEF7B003AFCB7 /* Helpers */, - DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */, - DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, DD2E65252767A01F00E45FC5 /* NodeDetail.swift */, DD47E3CD26F103C600029299 /* NodeList.swift */, DD90860D26F69BAE00DC5189 /* NodeMap.swift */, + DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */, + DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, DD73FD1028750779000852D6 /* PositionLog.swift */, 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, DDDB263E2AABEE20003AFCB7 /* NodeListSplit.swift */, diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index dcda62a4..c3604ab8 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -14,7 +14,7 @@ struct DetectionSensorLog: View { @State private var isPresentingClearLogConfirm: Bool = false @State var isExporting = false @State var exportString = "" - var node: NodeInfoEntity + @ObservedObject var node: NodeInfoEntity var body: some View { let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date()) diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 25a161a4..08cd55c6 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -19,7 +19,7 @@ struct DeviceMetricsLog: View { @State private var batteryChartColor: Color = .blue @State private var airtimeChartColor: Color = .orange @State private var channelUtilizationChartColor: Color = .green - var node: NodeInfoEntity + @ObservedObject var node: NodeInfoEntity var body: some View { diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 10fb667e..ccc0ccdb 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -17,7 +17,7 @@ struct EnvironmentMetricsLog: View { @State var isExporting = false @State var exportString = "" - var node: NodeInfoEntity + @ObservedObject var node: NodeInfoEntity var body: some View { let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date()) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift index e1f553b8..5d1b340d 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift @@ -13,15 +13,10 @@ struct NodeDetailItem: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @Environment(\.colorScheme) var colorScheme: ColorScheme - @State private var showingForecast = false @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false - @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( - mapName: "offlinemap", - tileType: "png", - canReplaceMapContent: true - ) - var node: NodeInfoEntity + + @ObservedObject var node: NodeInfoEntity var body: some View { @@ -31,6 +26,69 @@ struct NodeDetailItem: View { VStack { ScrollView { NodeInfoItem(node: node) + VStack { + NavigationLink { + DeviceMetricsLog(node: node) + } label: { + Image(systemName: "flipphone") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Device Metrics Log") + .font(.title3) + } + .disabled(!node.hasDeviceMetrics) + Divider() + NavigationLink { + EnvironmentMetricsLog(node: node) + } label: { + Image(systemName: "chart.xyaxis.line") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Environment Metrics Log") + .font(.title3) + } + .disabled(!node.hasEnvironmentMetrics) + Divider() + NavigationLink { + NodeMapControl(node: node) + } label: { + Image(systemName: "map") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Node Map") + .font(.title3) + } + .disabled(!node.hasPositions) + Divider() + NavigationLink { + PositionLog(node: node) + } label: { + Image(systemName: "building.columns") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Position Log") + .font(.title3) + } + .disabled(!node.hasPositions) + Divider() + NavigationLink { + DetectionSensorLog(node: node) + } label: { + Image(systemName: "sensor") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Detection Sensor Log") + .font(.title3) + } + Divider() + } + + if self.bleManager.connectedPeripheral != nil && node.metadata != nil { HStack { if node.metadata?.canShutdown ?? false { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index 3d36bcb0..3c230e27 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -11,16 +11,11 @@ import MapKit struct NodeInfoItem: View { - var node: NodeInfoEntity + @ObservedObject var node: NodeInfoEntity var body: some View { Divider() - if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { - - } else { - - } HStack { @@ -70,7 +65,7 @@ struct NodeInfoItem: View { } } Divider() - VStack(alignment: .center) { + HStack(alignment: .center) { VStack { HStack { Image(systemName: "number") @@ -92,75 +87,7 @@ struct NodeInfoItem: View { } Text(node.user?.userId ?? "?").font(.title3).foregroundColor(.gray) } - Divider() - } - - VStack { - // List { - - if node.hasDeviceMetrics { - - NavigationLink { - DeviceMetricsLog(node: node) - } label: { - - Image(systemName: "flipphone") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Device Metrics Log") - .font(.title3) - } - Divider() - } - if node.hasEnvironmentMetrics { - NavigationLink { - EnvironmentMetricsLog(node: node) - } label: { - - Image(systemName: "chart.xyaxis.line") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Environment Metrics Log") - .font(.title3) - } - Divider() - } - if node.hasPositions { - - NavigationLink { - PositionLog(node: node) - .onAppear { - - } - } label: { - - Image(systemName: "building.columns") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Position Log") - .font(.title3) - } - .fixedSize(horizontal: false, vertical: true) - Divider() - } - NavigationLink { - DetectionSensorLog(node: node) - } label: { - - Image(systemName: "sensor") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Detection Sensor Log") - .font(.title3) - } - .fixedSize(horizontal: false, vertical: true) - Divider() - // } - // .listStyle(.plain) } + Divider() } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift index 78a4d5c7..68723969 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift @@ -4,5 +4,15 @@ // // Created by Garth Vander Houwen on 9/9/23. // +import SwiftUI +import CoreLocation +import MapKit -import Foundation +struct NodeMapControl: View { + + @ObservedObject var node: NodeInfoEntity + + var body: some View { + Text("I am a map") + } +} diff --git a/Meshtastic/Views/Nodes/NodeListSplit.swift b/Meshtastic/Views/Nodes/NodeListSplit.swift index 6ac85836..8ec52f3b 100644 --- a/Meshtastic/Views/Nodes/NodeListSplit.swift +++ b/Meshtastic/Views/Nodes/NodeListSplit.swift @@ -17,7 +17,6 @@ enum SelectedDetail { struct NodeListSplit: View { - // Layout variables @State private var columnVisibility = NavigationSplitViewVisibility.all @State private var selectedNode: NodeInfoEntity? @State private var selectedDetail: SelectedDetail? @@ -71,9 +70,12 @@ struct NodeListSplit: View { } } detail: { - Text("Content") + Text("Select something to view") } .navigationSplitViewStyle(.balanced) + .onChange(of: selectedNode) { _ in + selectedDetail = nil + } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context From 7ca655535acd5e3db998d23ee723ac54b41eb9f2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 9 Sep 2023 19:10:05 -0700 Subject: [PATCH 07/16] Add map to new 3 column layout, comment out old views --- Meshtastic/Views/ContentView.swift | 8 +- .../Views/Nodes/Helpers/NodeDetailItem.swift | 2 +- .../Views/Nodes/Helpers/NodeMapControl.swift | 93 +++- Meshtastic/Views/Nodes/NodeDetail.swift | 496 +++++++++--------- Meshtastic/Views/Nodes/NodeList.swift | 266 +++++----- 5 files changed, 475 insertions(+), 390 deletions(-) diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 751b7a38..5f2340fe 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -19,16 +19,11 @@ struct ContentView: View { Label("bluetooth", systemImage: "antenna.radiowaves.left.and.right") } .tag(Tab.ble) - NodeList() - .tabItem { - Label("nodes", systemImage: "flipphone") - } - .tag(Tab.nodes) NodeListSplit() .tabItem { Label("nodes", systemImage: "flipphone") } - .tag(Tab.nodes2) + .tag(Tab.nodes) NodeMap() .tabItem { Label("map", systemImage: "map") @@ -56,6 +51,5 @@ enum Tab { case map case ble case nodes - case nodes2 case settings } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift index 5d1b340d..7c4d04f4 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift @@ -66,7 +66,7 @@ struct NodeDetailItem: View { NavigationLink { PositionLog(node: node) } label: { - Image(systemName: "building.columns") + Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .font(.title) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift index 68723969..1a816892 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift @@ -10,9 +10,100 @@ import MapKit struct NodeMapControl: View { + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @Environment(\.colorScheme) var colorScheme: ColorScheme + @AppStorage("meshMapType") private var meshMapType = 0 + @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false + @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false + @State private var selectedMapLayer: MapLayer = .standard + @State var waypointCoordinate: WaypointCoordinate? + @State var editingWaypoint: Int = 0 + @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( + mapName: "offlinemap", + tileType: "png", + canReplaceMapContent: true + ) + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], + predicate: NSPredicate( + format: "expire == nil || expire >= %@", Date() as NSDate + ), animation: .none) + private var waypoints: FetchedResults @ObservedObject var node: NodeInfoEntity var body: some View { - Text("I am a map") + + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) + NavigationStack { + GeometryReader { bounds in + VStack { + if node.hasPositions { + ZStack { + let positionArray = node.positions?.array as? [PositionEntity] ?? [] + let lastTenThousand = Array(positionArray.prefix(10000)) + // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } + ZStack { + MapViewSwiftUI(onLongPress: { coord in + waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) + }, onWaypointEdit: { wpId in + if wpId > 0 { + waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) + } + }, + selectedMapLayer: selectedMapLayer, + positions: lastTenThousand, + waypoints: Array(waypoints), + userTrackingMode: MKUserTrackingMode.none, + showNodeHistory: meshMapShowNodeHistory, + showRouteLines: meshMapShowRouteLines, + customMapOverlay: self.customMapOverlay + ) + VStack(alignment: .leading) { + Spacer() + HStack(alignment: .bottom, spacing: 1) { + Picker("Map Type", selection: $selectedMapLayer) { + ForEach(MapLayer.allCases, id: \.self) { layer in + if layer == MapLayer.offline && UserDefaults.enableOfflineMaps { + Text(layer.localized) + } else if layer != MapLayer.offline { + Text(layer.localized) + } + } + } + .onChange(of: (selectedMapLayer)) { newMapLayer in + UserDefaults.mapLayer = newMapLayer + } + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .pickerStyle(.menu) + .padding(5) + } + } + } + .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) + .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65) + } + } else { + HStack { + } + .padding([.top], 20) + } + } + .edgesIgnoringSafeArea([.leading, .trailing]) + .sheet(item: $waypointCoordinate, content: { wpc in + WaypointFormView(coordinate: wpc) + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.automatic) + }) + .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) + .navigationBarItems(trailing: + ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + }) + } + .padding(.bottom, 2) + } } } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 356e9e59..f0e78ea5 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -1,248 +1,248 @@ -/* - Abstract: - A view showing the details for a node. - */ - -import SwiftUI -import WeatherKit -import MapKit -import CoreLocation - -struct NodeDetail: View { - - @Environment(\.managedObjectContext) var context - @EnvironmentObject var bleManager: BLEManager - @Environment(\.colorScheme) var colorScheme: ColorScheme - @AppStorage("meshMapType") private var meshMapType = 0 - @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false - @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false - @State private var selectedMapLayer: MapLayer = .standard - @State var waypointCoordinate: WaypointCoordinate? - @State var editingWaypoint: Int = 0 - @State private var loadedWeather: Bool = false - @State private var showingDetailsPopover = false - @State private var showingForecast = false - @State private var showingShutdownConfirm: Bool = false - @State private var showingRebootConfirm: Bool = false - @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( - mapName: "offlinemap", - tileType: "png", - canReplaceMapContent: true - ) - 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 - - /// The current weather condition for the city. - @State private var condition: WeatherCondition? - @State private var temperature: Measurement? - @State private var humidity: Int? - @State private var symbolName: String = "cloud.fill" - - @State private var attributionLink: URL? - @State private var attributionLogo: URL? - - var body: some View { - - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) - NavigationStack { - GeometryReader { bounds in - VStack { - if node.hasPositions { - ZStack { - let positionArray = node.positions?.array as? [PositionEntity] ?? [] - let lastTenThousand = Array(positionArray.prefix(10000)) - // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } - ZStack { - MapViewSwiftUI(onLongPress: { coord in - waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) - }, onWaypointEdit: { wpId in - if wpId > 0 { - waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) - } - }, - selectedMapLayer: selectedMapLayer, - positions: lastTenThousand, - waypoints: Array(waypoints), - userTrackingMode: MKUserTrackingMode.none, - showNodeHistory: meshMapShowNodeHistory, - showRouteLines: meshMapShowRouteLines, - customMapOverlay: self.customMapOverlay - ) - VStack(alignment: .leading) { - Spacer() - HStack(alignment: .bottom, spacing: 1) { - Picker("Map Type", selection: $selectedMapLayer) { - ForEach(MapLayer.allCases, id: \.self) { layer in - if layer == MapLayer.offline && UserDefaults.enableOfflineMaps { - Text(layer.localized) - } else if layer != MapLayer.offline { - Text(layer.localized) - } - } - } - .onChange(of: (selectedMapLayer)) { newMapLayer in - UserDefaults.mapLayer = newMapLayer - } - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) - .pickerStyle(.menu) - .padding(5) - VStack { - Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) - .font(.caption) - - Label("\(humidity ?? 0)%", systemImage: "humidity") - .font(.caption2) - } - .padding(10) - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) - .padding(5) - #if targetEnvironment(macCatalyst) - .popover(isPresented: $showingForecast, - arrowEdge: .top) { - Text("Today's Weather Forecast") - .font(.title) - .padding() - let nodeLocation = node.positions?.lastObject as? PositionEntity - NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation?.nodeCoordinate!.latitude ?? LocationHelper.currentLocation.latitude, longitude: nodeLocation?.nodeCoordinate!.longitude ?? LocationHelper.currentLocation.longitude) ) - .frame(height: 250) - } - #else - .sheet(isPresented: $showingForecast) { - Text("Today's Weather Forecast") - .font(.title) - .padding() - let nodeLocation = node.positions?.lastObject as? PositionEntity - NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation?.nodeCoordinate!.latitude ?? LocationHelper.currentLocation.latitude, longitude: nodeLocation?.nodeCoordinate!.longitude ?? LocationHelper.currentLocation.longitude) ).frame(height: 250) - .presentationDetents([.medium]) - .presentationDragIndicator(.automatic) - } - #endif - .gesture( - LongPressGesture(minimumDuration: 0.5) - .onEnded { _ in - showingForecast = true - } - ) - } - } - } - .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) - .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65) - } - } else { - HStack { - } - .padding([.top], 20) - } - ScrollView { - NodeInfoView(node: node) - if self.bleManager.connectedPeripheral != nil && node.metadata != nil { - HStack { - if node.metadata?.canShutdown ?? false { - - Button(action: { - showingShutdownConfirm = true - }) { - Label("Power Off", systemImage: "power") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .confirmationDialog( - "are.you.sure", - isPresented: $showingShutdownConfirm - ) { - Button("Shutdown Node?", role: .destructive) { - if !bleManager.sendShutdown(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { - print("Shutdown Failed") - } - } - } - } - - Button(action: { - showingRebootConfirm = true - }) { - Label("reboot", systemImage: "arrow.triangle.2.circlepath") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .confirmationDialog("are.you.sure", - isPresented: $showingRebootConfirm - ) { - Button("reboot.node", role: .destructive) { - if !bleManager.sendReboot(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { - print("Reboot Failed") - } - } - } - } - .padding(5) - Divider() - } - if node.positions?.count ?? 0 > 0 { - VStack { - AsyncImage(url: attributionLogo) { image in - image - .resizable() - .scaledToFit() - } placeholder: { - ProgressView() - .controlSize(.mini) - } - .frame(height: 15) - - Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!) - } - .font(.footnote) - } - } - } - .edgesIgnoringSafeArea([.leading, .trailing]) - .sheet(item: $waypointCoordinate, content: { wpc in - WaypointFormView(coordinate: wpc) - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) - }) - .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) - .navigationBarItems(trailing: - ZStack { - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) - .task(id: node.num) { - if !loadedWeather { - do { - if node.hasPositions { - let mostRecent = node.positions?.lastObject as? PositionEntity - let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)) - condition = weather.currentWeather.condition - temperature = weather.currentWeather.temperature - humidity = Int(weather.currentWeather.humidity * 100) - symbolName = weather.currentWeather.symbolName - let attribution = try await WeatherService.shared.attribution - attributionLink = attribution.legalPageURL - attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL - loadedWeather = true - } - } catch { - print("Could not gather weather information...", error.localizedDescription) - condition = .clear - symbolName = "cloud.fill" - } - } - } - } - .padding(.bottom, 2) - } - } -} +///* +// Abstract: +// A view showing the details for a node. +// */ +// +//import SwiftUI +//import WeatherKit +//import MapKit +//import CoreLocation +// +//struct NodeDetail: View { +// +// @Environment(\.managedObjectContext) var context +// @EnvironmentObject var bleManager: BLEManager +// @Environment(\.colorScheme) var colorScheme: ColorScheme +// @AppStorage("meshMapType") private var meshMapType = 0 +// @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false +// @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false +// @State private var selectedMapLayer: MapLayer = .standard +// @State var waypointCoordinate: WaypointCoordinate? +// @State var editingWaypoint: Int = 0 +// @State private var loadedWeather: Bool = false +// @State private var showingDetailsPopover = false +// @State private var showingForecast = false +// @State private var showingShutdownConfirm: Bool = false +// @State private var showingRebootConfirm: Bool = false +// @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay( +// mapName: "offlinemap", +// tileType: "png", +// canReplaceMapContent: true +// ) +// 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 +// +// /// The current weather condition for the city. +// @State private var condition: WeatherCondition? +// @State private var temperature: Measurement? +// @State private var humidity: Int? +// @State private var symbolName: String = "cloud.fill" +// +// @State private var attributionLink: URL? +// @State private var attributionLogo: URL? +// +// var body: some View { +// +// let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) +// NavigationStack { +// GeometryReader { bounds in +// VStack { +// if node.hasPositions { +// ZStack { +// let positionArray = node.positions?.array as? [PositionEntity] ?? [] +// let lastTenThousand = Array(positionArray.prefix(10000)) +// // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } +// ZStack { +// MapViewSwiftUI(onLongPress: { coord in +// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0) +// }, onWaypointEdit: { wpId in +// if wpId > 0 { +// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId)) +// } +// }, +// selectedMapLayer: selectedMapLayer, +// positions: lastTenThousand, +// waypoints: Array(waypoints), +// userTrackingMode: MKUserTrackingMode.none, +// showNodeHistory: meshMapShowNodeHistory, +// showRouteLines: meshMapShowRouteLines, +// customMapOverlay: self.customMapOverlay +// ) +// VStack(alignment: .leading) { +// Spacer() +// HStack(alignment: .bottom, spacing: 1) { +// Picker("Map Type", selection: $selectedMapLayer) { +// ForEach(MapLayer.allCases, id: \.self) { layer in +// if layer == MapLayer.offline && UserDefaults.enableOfflineMaps { +// Text(layer.localized) +// } else if layer != MapLayer.offline { +// Text(layer.localized) +// } +// } +// } +// .onChange(of: (selectedMapLayer)) { newMapLayer in +// UserDefaults.mapLayer = newMapLayer +// } +// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) +// .pickerStyle(.menu) +// .padding(5) +// VStack { +// Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) +// .font(.caption) +// +// Label("\(humidity ?? 0)%", systemImage: "humidity") +// .font(.caption2) +// } +// .padding(10) +// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) +// .padding(5) +// #if targetEnvironment(macCatalyst) +// .popover(isPresented: $showingForecast, +// arrowEdge: .top) { +// Text("Today's Weather Forecast") +// .font(.title) +// .padding() +// let nodeLocation = node.positions?.lastObject as? PositionEntity +// NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation?.nodeCoordinate!.latitude ?? LocationHelper.currentLocation.latitude, longitude: nodeLocation?.nodeCoordinate!.longitude ?? LocationHelper.currentLocation.longitude) ) +// .frame(height: 250) +// } +// #else +// .sheet(isPresented: $showingForecast) { +// Text("Today's Weather Forecast") +// .font(.title) +// .padding() +// let nodeLocation = node.positions?.lastObject as? PositionEntity +// NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation?.nodeCoordinate!.latitude ?? LocationHelper.currentLocation.latitude, longitude: nodeLocation?.nodeCoordinate!.longitude ?? LocationHelper.currentLocation.longitude) ).frame(height: 250) +// .presentationDetents([.medium]) +// .presentationDragIndicator(.automatic) +// } +// #endif +// .gesture( +// LongPressGesture(minimumDuration: 0.5) +// .onEnded { _ in +// showingForecast = true +// } +// ) +// } +// } +// } +// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) +// .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65) +// } +// } else { +// HStack { +// } +// .padding([.top], 20) +// } +// ScrollView { +// NodeInfoView(node: node) +// if self.bleManager.connectedPeripheral != nil && node.metadata != nil { +// HStack { +// if node.metadata?.canShutdown ?? false { +// +// Button(action: { +// showingShutdownConfirm = true +// }) { +// Label("Power Off", systemImage: "power") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding() +// .confirmationDialog( +// "are.you.sure", +// isPresented: $showingShutdownConfirm +// ) { +// Button("Shutdown Node?", role: .destructive) { +// if !bleManager.sendShutdown(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { +// print("Shutdown Failed") +// } +// } +// } +// } +// +// Button(action: { +// showingRebootConfirm = true +// }) { +// Label("reboot", systemImage: "arrow.triangle.2.circlepath") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding() +// .confirmationDialog("are.you.sure", +// isPresented: $showingRebootConfirm +// ) { +// Button("reboot.node", role: .destructive) { +// if !bleManager.sendReboot(fromUser: connectedNode!.user!, toUser: node.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { +// print("Reboot Failed") +// } +// } +// } +// } +// .padding(5) +// Divider() +// } +// if node.positions?.count ?? 0 > 0 { +// VStack { +// AsyncImage(url: attributionLogo) { image in +// image +// .resizable() +// .scaledToFit() +// } placeholder: { +// ProgressView() +// .controlSize(.mini) +// } +// .frame(height: 15) +// +// Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!) +// } +// .font(.footnote) +// } +// } +// } +// .edgesIgnoringSafeArea([.leading, .trailing]) +// .sheet(item: $waypointCoordinate, content: { wpc in +// WaypointFormView(coordinate: wpc) +// .presentationDetents([.medium, .large]) +// .presentationDragIndicator(.automatic) +// }) +// .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) +// .navigationBarItems(trailing: +// ZStack { +// ConnectedDevice( +// bluetoothOn: bleManager.isSwitchedOn, +// deviceConnected: bleManager.connectedPeripheral != nil, +// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") +// }) +// .task(id: node.num) { +// if !loadedWeather { +// do { +// if node.hasPositions { +// let mostRecent = node.positions?.lastObject as? PositionEntity +// let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)) +// condition = weather.currentWeather.condition +// temperature = weather.currentWeather.temperature +// humidity = Int(weather.currentWeather.humidity * 100) +// symbolName = weather.currentWeather.symbolName +// let attribution = try await WeatherService.shared.attribution +// attributionLink = attribution.legalPageURL +// attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL +// loadedWeather = true +// } +// } catch { +// print("Could not gather weather information...", error.localizedDescription) +// condition = .clear +// symbolName = "cloud.fill" +// } +// } +// } +// } +// .padding(.bottom, 2) +// } +// } +//} diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 3ab4a478..c47b1909 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -1,136 +1,136 @@ +//// +//// NodeList.swift +//// Meshtastic +//// +//// Copyright(c) Garth Vander Houwen 8/7/21. +//// // -// NodeList.swift -// Meshtastic +//// Abstract: +//// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device. // -// Copyright(c) Garth Vander Houwen 8/7/21. +//import SwiftUI +//import CoreLocation // - -// Abstract: -// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device. - -import SwiftUI -import CoreLocation - -struct NodeList: View { - - @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 { - 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 - if nodes.count == 0 { - Text("no.nodes").font(.title) - } else { - NavigationLink(value: node) { - let connected: Bool = (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]) - } - } - .listStyle(.plain) - .navigationSplitViewColumnWidth(300) - .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) - .navigationBarItems(leading: - MeshtasticLogo() - ) - .onAppear { - if self.bleManager.context == nil { - self.bleManager.context = context - } - } - } detail: { - if let node = selection { - NodeDetail(node: node) - } else { - Text("select.node") - } - } - .searchable(text: nodesQuery, prompt: "Find a node") - } -} +//struct NodeList: View { +// +// @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 { +// 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 +// if nodes.count == 0 { +// Text("no.nodes").font(.title) +// } else { +// NavigationLink(value: node) { +// let connected: Bool = (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]) +// } +// } +// .listStyle(.plain) +// .navigationSplitViewColumnWidth(300) +// .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) +// .navigationBarItems(leading: +// MeshtasticLogo() +// ) +// .onAppear { +// if self.bleManager.context == nil { +// self.bleManager.context = context +// } +// } +// } detail: { +// if let node = selection { +// NodeDetail(node: node) +// } else { +// Text("select.node") +// } +// } +// .searchable(text: nodesQuery, prompt: "Find a node") +// } +//} From 28386ee681c97e4d0682e9d0e3fc009297c1bd93 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 9 Sep 2023 21:23:14 -0700 Subject: [PATCH 08/16] Additional cleanup for nodes tab. Show connected node on every view for a phon, only the detail tab on ipad and mac --- Meshtastic.xcodeproj/project.pbxproj | 40 +- .../CoreData/NodeInfoEntityExtension.swift | 3 + .../CoreData/UserEntityExtension.swift | 5 + .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 3 + .../contents | 366 +++++++++++++ .../Views/Helpers/ConnectedDevice.swift | 46 +- .../Views/Helpers/Node/NodeInfoView.swift | 510 +++++++++--------- ...{NodeDetailItem.swift => NodeDetail.swift} | 29 +- .../{NodeDetail.swift => NodeDetailOld.swift} | 0 Meshtastic/Views/Nodes/NodeList.swift | 233 ++++---- Meshtastic/Views/Nodes/NodeListOld.swift | 136 +++++ Meshtastic/Views/Nodes/NodeListSplit.swift | 101 ---- Meshtastic/Views/Nodes/PositionLog.swift | 6 - 14 files changed, 933 insertions(+), 547 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV18.xcdatamodel/contents rename Meshtastic/Views/Nodes/Helpers/{NodeDetailItem.swift => NodeDetail.swift} (97%) rename Meshtastic/Views/Nodes/{NodeDetail.swift => NodeDetailOld.swift} (100%) create mode 100644 Meshtastic/Views/Nodes/NodeListOld.swift delete mode 100644 Meshtastic/Views/Nodes/NodeListSplit.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index ea9b95f4..e2e69453 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; }; DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; }; DD2DC2C029BCD8AB003B383C /* HardwareModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */; }; - DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; }; + DD2E65262767A01F00E45FC5 /* NodeDetailOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */; }; DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; }; DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; }; DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */; }; @@ -37,7 +37,7 @@ DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41582928585C32009B0E59 /* RangeTestConfig.swift */; }; DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */; }; DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */; }; - DD47E3CE26F103C600029299 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.swift */; }; + DD47E3CE26F103C600029299 /* NodeListOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeListOld.swift */; }; DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; }; DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; }; DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */; }; @@ -134,9 +134,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 */; }; + DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB263E2AABEE20003AFCB7 /* NodeList.swift */; }; DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26412AABF655003AFCB7 /* NodeListItem.swift */; }; - DDDB26442AAC0206003AFCB7 /* NodeDetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */; }; + DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */; }; DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */; }; DDDB26482AACD6D1003AFCB7 /* NodeMapControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */; }; DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443529F6287000EE2349 /* MapButtons.swift */; }; @@ -227,7 +227,7 @@ DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = ""; }; DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = ""; }; DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = ""; }; - DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = ""; }; + DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetailOld.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = ""; }; DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModel.xcdatamodel; sourceTree = ""; }; @@ -242,7 +242,7 @@ DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLESignalStrengthIndicator.swift; sourceTree = ""; }; DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV5.xcdatamodel; sourceTree = ""; }; - DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = ""; }; + DD47E3CD26F103C600029299 /* NodeListOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListOld.swift; sourceTree = ""; }; DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = ""; }; DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = ""; }; @@ -354,11 +354,12 @@ 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 = ""; }; + DDDB263E2AABEE20003AFCB7 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.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 = ""; }; + DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = ""; }; DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoItem.swift; sourceTree = ""; }; DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapControl.swift; sourceTree = ""; }; + DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV18.xcdatamodel; 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 = ""; }; @@ -467,14 +468,14 @@ isa = PBXGroup; children = ( DDDB26402AABEF7B003AFCB7 /* Helpers */, - DD2E65252767A01F00E45FC5 /* NodeDetail.swift */, - DD47E3CD26F103C600029299 /* NodeList.swift */, - DD90860D26F69BAE00DC5189 /* NodeMap.swift */, + DDDB263E2AABEE20003AFCB7 /* NodeList.swift */, + 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */, DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, + DD90860D26F69BAE00DC5189 /* NodeMap.swift */, DD73FD1028750779000852D6 /* PositionLog.swift */, - 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, - DDDB263E2AABEE20003AFCB7 /* NodeListSplit.swift */, + DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */, + DD47E3CD26F103C600029299 /* NodeListOld.swift */, ); path = Nodes; sourceTree = ""; @@ -810,7 +811,7 @@ DDDB26402AABEF7B003AFCB7 /* Helpers */ = { isa = PBXGroup; children = ( - DDDB26432AAC0206003AFCB7 /* NodeDetailItem.swift */, + DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */, DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */, DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */, @@ -1101,7 +1102,7 @@ DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */, DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, - DDDB263F2AABEE20003AFCB7 /* NodeListSplit.swift in Sources */, + DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, DDDB26482AACD6D1003AFCB7 /* NodeMapControl.swift in Sources */, @@ -1135,7 +1136,7 @@ DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */, DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */, 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */, - DD47E3CE26F103C600029299 /* NodeList.swift in Sources */, + DD47E3CE26F103C600029299 /* NodeListOld.swift in Sources */, DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */, DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, @@ -1159,7 +1160,7 @@ DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */, DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, - DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */, + DD2E65262767A01F00E45FC5 /* NodeDetailOld.swift in Sources */, DDDE5A1029AFE69700490C6C /* MeshActivityAttributes.swift in Sources */, DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */, DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */, @@ -1191,7 +1192,7 @@ DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, - DDDB26442AAC0206003AFCB7 /* NodeDetailItem.swift in Sources */, + DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */, DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */, DD77093F2AA1B146007A8BF0 /* UIColor.swift in Sources */, DD5E5205298EE33B00D21B61 /* mesh.pb.swift in Sources */, @@ -1713,6 +1714,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */, DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */, DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */, DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */, @@ -1731,7 +1733,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */; + currentVersion = DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index b57e6ff4..c041a7dd 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -24,6 +24,9 @@ extension NodeInfoEntity { let environmentMetrics = telemetries?.filter{ ($0 as AnyObject).metricsType == 1 } return environmentMetrics?.count ?? 0 > 0 } + var hasDetectionSensorMetrics: Bool { + return user?.sensorMessageList.count ?? 0 > 0 + } var isOnline: Bool { diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 927c0372..ecf4e5e2 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -8,6 +8,7 @@ import Foundation extension UserEntity { + var messageList: [MessageEntity] { self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]() @@ -17,6 +18,10 @@ extension UserEntity { self.value(forKey: "adminMessages") as? [MessageEntity] ?? [MessageEntity]() } + var sensorMessageList: [MessageEntity] { + self.value(forKey: "detectionSensorMessages") as? [MessageEntity] ?? [MessageEntity]() + } + var unreadMessages: Int { let unreadMessages = messageList.filter{ ($0 as AnyObject).read == false } return unreadMessages.count diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 6b72864b..348d9e99 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV17.xcdatamodel + MeshtasticDataModelV18.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV17.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV17.xcdatamodel/contents index db7917c1..d7f42c7b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV17.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV17.xcdatamodel/contents @@ -347,6 +347,9 @@ + + + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV18.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV18.xcdatamodel/contents new file mode 100644 index 00000000..c30e2f4b --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV18.xcdatamodel/contents @@ -0,0 +1,366 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index 9e4d1085..3a3b6d2a 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -10,35 +10,39 @@ struct ConnectedDevice: View { var deviceConnected: Bool var name: String var mqttProxyConnected: Bool = false + var phoneOnly: Bool = false var body: some View { HStack { - if bluetoothOn { - if deviceConnected && mqttProxyConnected { - if mqttProxyConnected { - Image(systemName: "iphone.gen3.radiowaves.left.and.right.circle.fill") + + if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { + if bluetoothOn { + if deviceConnected && mqttProxyConnected { + if mqttProxyConnected { + Image(systemName: "iphone.gen3.radiowaves.left.and.right.circle.fill") + .imageScale(.large) + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + } + } + if deviceConnected { + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") .imageScale(.large) .foregroundColor(.green) .symbolRenderingMode(.hierarchical) - } - } - if deviceConnected { - Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .imageScale(.large) - .foregroundColor(.green) - .symbolRenderingMode(.hierarchical) - Text(name).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) - } else { + Text(name).font(name.isEmoji() ? .title : .callout).foregroundColor(.gray) + } else { - Image(systemName: "antenna.radiowaves.left.and.right.slash") - .imageScale(.medium) - .foregroundColor(.red) - .symbolRenderingMode(.hierarchical) - } - } else { - Text("bluetooth.off").font(.subheadline).foregroundColor(.red) - } + Image(systemName: "antenna.radiowaves.left.and.right.slash") + .imageScale(.medium) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) + } + } else { + Text("bluetooth.off").font(.subheadline).foregroundColor(.red) + } + } } } } diff --git a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift index 362de937..94dfdf4f 100644 --- a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift +++ b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift @@ -1,261 +1,261 @@ +//// +//// NodeInfoView.swift +//// Meshtastic +//// +//// Created by Garth Vander Houwen on 4/2/23. +//// // -// NodeInfoView.swift -// Meshtastic +//// +//// DistanceText.swift +//// Meshtastic +//// +//// Copyright(c) Garth Vander Houwen 8/19/22. +//// // -// Created by Garth Vander Houwen on 4/2/23. +//import SwiftUI +//import CoreLocation +//import MapKit // - +//struct NodeInfoView: View { // -// DistanceText.swift -// Meshtastic +// var node: NodeInfoEntity // -// Copyright(c) Garth Vander Houwen 8/19/22. +// var body: some View { +// let hwModelString = node.user?.hwModel ?? "UNSET" // - -import SwiftUI -import CoreLocation -import MapKit - -struct NodeInfoView: View { - - var node: NodeInfoEntity - - var body: some View { - let hwModelString = node.user?.hwModel ?? "UNSET" - - Divider() - if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { - HStack { - VStack(alignment: .center) { - CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 150) - } - Divider() - VStack { - if node.user != nil { - Image(hwModelString) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) - .cornerRadius(5) - - Text(String(hwModelString)) - .foregroundColor(.gray) - .font(.title).fixedSize() - } - } - Divider() - if node.snr != 0 { - VStack(alignment: .center) { - let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) - LoRaSignalStrengthIndicator(signalStrength: signalStrength) - Text("Signal \(signalStrength.description)").font(.title) - Text("SNR \(String(format: "%.2f", node.snr))dB") - .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) - .font(.title3) - Text("RSSI \(node.rssi)dB") - .foregroundColor(getRssiColor(rssi: node.rssi)) - .font(.title3) - } - Divider() - } - - if node.hasDeviceMetrics { - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - VStack(alignment: .center) { - BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) - if mostRecent?.voltage ?? 0 > 0.0 { - - Text(String(format: "%.2f", mostRecent?.voltage ?? 0.0) + " V") - .font(.title) - .foregroundColor(.gray) - .fixedSize() - } - } - .padding() - } - } - .padding() - - Divider() - HStack(alignment: .center) { - - VStack { - HStack { - Image(systemName: "person") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("user").font(.title)+Text(":").font(.title) - } - Text("!\(String(format: "%02x", node.num))") - .font(.title).foregroundColor(.gray) - } - Divider() - VStack { - HStack { - Image(systemName: "number") - .font(.title2) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("Node Number:").font(.title) - } - Text(String(node.num)).font(.title).foregroundColor(.gray) - } - Divider() - VStack { - HStack { - Image(systemName: "clock.badge.checkmark.fill") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("heard.last").font(.title)+Text(":").font(.title) - - } - DateTimeText(dateTime: node.lastHeard) - .font(.title3) - .foregroundColor(.gray) - } - } - Divider() - - } else { - - HStack { - - VStack(alignment: .center) { - CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) - } - if node.user != nil { - Divider() - VStack { - Image(node.user!.hwModel ?? "unset".localized) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 75, height: 75) - .cornerRadius(5) - Text(String(node.user!.hwModel ?? "unset".localized)) - .font(.caption2).fixedSize() - } - } - if node.snr != 0 { - Divider() - VStack(alignment: .center) { - let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) - LoRaSignalStrengthIndicator(signalStrength: signalStrength) - Text("Signal \(signalStrength.description)").font(.footnote) - Text("SNR \(String(format: "%.2f", node.snr))dB") - .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) - .font(.caption2) - Text("RSSI \(node.rssi)dB") - .foregroundColor(getRssiColor(rssi: node.rssi)) - .font(.caption2) - } - } - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - if deviceMetrics?.count ?? 0 >= 1 { - Divider() - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - VStack(alignment: .center) { - BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) - if mostRecent?.voltage ?? 0 > 0 { - - Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V") - .font(.callout) - .foregroundColor(.gray) - .fixedSize() - } - } - } - } - Divider() - HStack(alignment: .center) { - VStack { - HStack { - Image(systemName: "person") - .font(.title2) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("User Id:").font(.title2) - } - Text(node.user?.userId ?? "?").font(.title3).foregroundColor(.gray) - } - Divider() - VStack { - HStack { - Image(systemName: "number") - .font(.title2) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("Node Number:").font(.title2) - } - Text(String(node.num)).font(.title3).foregroundColor(.gray) - } - } - Divider() - } - - VStack { - - if node.hasPositions{ - - NavigationLink { - PositionLog(node: node) - } label: { - - Image(systemName: "building.columns") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Position Log") - .font(.title3) - } - .fixedSize(horizontal: false, vertical: true) - Divider() - } - - if node.hasDeviceMetrics { - - NavigationLink { - DeviceMetricsLog(node: node) - } label: { - - Image(systemName: "flipphone") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Device Metrics Log") - .font(.title3) - } - Divider() - } - if node.hasEnvironmentMetrics { - NavigationLink { - EnvironmentMetricsLog(node: node) - } label: { - - Image(systemName: "chart.xyaxis.line") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Environment Metrics Log") - .font(.title3) - } - Divider() - } - NavigationLink { - DetectionSensorLog(node: node) - } label: { - - Image(systemName: "sensor") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Detection Sensor Log") - .font(.title3) - } - .fixedSize(horizontal: false, vertical: true) - Divider() - } - } -} +// Divider() +// if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { +// HStack { +// VStack(alignment: .center) { +// CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 150) +// } +// Divider() +// VStack { +// if node.user != nil { +// Image(hwModelString) +// .resizable() +// .aspectRatio(contentMode: .fit) +// .frame(width: 100, height: 100) +// .cornerRadius(5) +// +// Text(String(hwModelString)) +// .foregroundColor(.gray) +// .font(.title).fixedSize() +// } +// } +// Divider() +// if node.snr != 0 { +// VStack(alignment: .center) { +// let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) +// LoRaSignalStrengthIndicator(signalStrength: signalStrength) +// Text("Signal \(signalStrength.description)").font(.title) +// Text("SNR \(String(format: "%.2f", node.snr))dB") +// .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) +// .font(.title3) +// Text("RSSI \(node.rssi)dB") +// .foregroundColor(getRssiColor(rssi: node.rssi)) +// .font(.title3) +// } +// Divider() +// } +// +// if node.hasDeviceMetrics { +// let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) +// let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity +// VStack(alignment: .center) { +// BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) +// if mostRecent?.voltage ?? 0 > 0.0 { +// +// Text(String(format: "%.2f", mostRecent?.voltage ?? 0.0) + " V") +// .font(.title) +// .foregroundColor(.gray) +// .fixedSize() +// } +// } +// .padding() +// } +// } +// .padding() +// +// Divider() +// HStack(alignment: .center) { +// +// VStack { +// HStack { +// Image(systemName: "person") +// .font(.title) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("user").font(.title)+Text(":").font(.title) +// } +// Text("!\(String(format: "%02x", node.num))") +// .font(.title).foregroundColor(.gray) +// } +// Divider() +// VStack { +// HStack { +// Image(systemName: "number") +// .font(.title2) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("Node Number:").font(.title) +// } +// Text(String(node.num)).font(.title).foregroundColor(.gray) +// } +// Divider() +// VStack { +// HStack { +// Image(systemName: "clock.badge.checkmark.fill") +// .font(.title) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("heard.last").font(.title)+Text(":").font(.title) +// +// } +// DateTimeText(dateTime: node.lastHeard) +// .font(.title3) +// .foregroundColor(.gray) +// } +// } +// Divider() +// +// } else { +// +// HStack { +// +// VStack(alignment: .center) { +// CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) +// } +// if node.user != nil { +// Divider() +// VStack { +// Image(node.user!.hwModel ?? "unset".localized) +// .resizable() +// .aspectRatio(contentMode: .fit) +// .frame(width: 75, height: 75) +// .cornerRadius(5) +// Text(String(node.user!.hwModel ?? "unset".localized)) +// .font(.caption2).fixedSize() +// } +// } +// if node.snr != 0 { +// Divider() +// VStack(alignment: .center) { +// let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) +// LoRaSignalStrengthIndicator(signalStrength: signalStrength) +// Text("Signal \(signalStrength.description)").font(.footnote) +// Text("SNR \(String(format: "%.2f", node.snr))dB") +// .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) +// .font(.caption2) +// Text("RSSI \(node.rssi)dB") +// .foregroundColor(getRssiColor(rssi: node.rssi)) +// .font(.caption2) +// } +// } +// let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) +// if deviceMetrics?.count ?? 0 >= 1 { +// Divider() +// let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity +// VStack(alignment: .center) { +// BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) +// if mostRecent?.voltage ?? 0 > 0 { +// +// Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V") +// .font(.callout) +// .foregroundColor(.gray) +// .fixedSize() +// } +// } +// } +// } +// Divider() +// HStack(alignment: .center) { +// VStack { +// HStack { +// Image(systemName: "person") +// .font(.title2) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("User Id:").font(.title2) +// } +// Text(node.user?.userId ?? "?").font(.title3).foregroundColor(.gray) +// } +// Divider() +// VStack { +// HStack { +// Image(systemName: "number") +// .font(.title2) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("Node Number:").font(.title2) +// } +// Text(String(node.num)).font(.title3).foregroundColor(.gray) +// } +// } +// Divider() +// } +// +// VStack { +// +// if node.hasPositions{ +// +// NavigationLink { +// PositionLog(node: node) +// } label: { +// +// Image(systemName: "building.columns") +// .symbolRenderingMode(.hierarchical) +// .font(.title) +// +// Text("Position Log") +// .font(.title3) +// } +// .fixedSize(horizontal: false, vertical: true) +// Divider() +// } +// +// if node.hasDeviceMetrics { +// +// NavigationLink { +// DeviceMetricsLog(node: node) +// } label: { +// +// Image(systemName: "flipphone") +// .symbolRenderingMode(.hierarchical) +// .font(.title) +// +// Text("Device Metrics Log") +// .font(.title3) +// } +// Divider() +// } +// if node.hasEnvironmentMetrics { +// NavigationLink { +// EnvironmentMetricsLog(node: node) +// } label: { +// +// Image(systemName: "chart.xyaxis.line") +// .symbolRenderingMode(.hierarchical) +// .font(.title) +// +// Text("Environment Metrics Log") +// .font(.title3) +// } +// Divider() +// } +// NavigationLink { +// DetectionSensorLog(node: node) +// } label: { +// +// Image(systemName: "sensor") +// .symbolRenderingMode(.hierarchical) +// .font(.title) +// +// Text("Detection Sensor Log") +// .font(.title3) +// } +// .fixedSize(horizontal: false, vertical: true) +// Divider() +// } +// } +//} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift similarity index 97% rename from Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift rename to Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 7c4d04f4..fee6fdbe 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetailItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -8,7 +8,7 @@ import WeatherKit import MapKit import CoreLocation -struct NodeDetailItem: View { +struct NodeDetail: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -39,18 +39,6 @@ struct NodeDetailItem: View { } .disabled(!node.hasDeviceMetrics) Divider() - NavigationLink { - EnvironmentMetricsLog(node: node) - } label: { - Image(systemName: "chart.xyaxis.line") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Environment Metrics Log") - .font(.title3) - } - .disabled(!node.hasEnvironmentMetrics) - Divider() NavigationLink { NodeMapControl(node: node) } label: { @@ -75,6 +63,18 @@ struct NodeDetailItem: View { } .disabled(!node.hasPositions) Divider() + NavigationLink { + EnvironmentMetricsLog(node: node) + } label: { + Image(systemName: "chart.xyaxis.line") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Environment Metrics Log") + .font(.title3) + } + .disabled(!node.hasEnvironmentMetrics) + Divider() NavigationLink { DetectionSensorLog(node: node) } label: { @@ -85,6 +85,7 @@ struct NodeDetailItem: View { Text("Detection Sensor Log") .font(.title3) } + .disabled(!node.hasDetectionSensorMetrics) Divider() } @@ -150,7 +151,7 @@ struct NodeDetailItem: View { ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) }) } .padding(.bottom, 2) diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetailOld.swift similarity index 100% rename from Meshtastic/Views/Nodes/NodeDetail.swift rename to Meshtastic/Views/Nodes/NodeDetailOld.swift diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index c47b1909..08b6ff7c 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -1,136 +1,109 @@ -//// -//// NodeList.swift -//// Meshtastic -//// -//// Copyright(c) Garth Vander Houwen 8/7/21. -//// // -//// Abstract: -//// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device. +// NodeListSplit.swift +// Meshtastic // -//import SwiftUI -//import CoreLocation +// Created by Garth Vander Houwen on 9/8/23. // -//struct NodeList: View { -// -// @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 { -// 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 -// if nodes.count == 0 { -// Text("no.nodes").font(.title) -// } else { -// NavigationLink(value: node) { -// let connected: Bool = (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]) +import SwiftUI +import CoreLocation + +enum SelectedDetail { + case positionLog + case nodeMap + case deviceMetricsLog + case environmentMetricsLog + case detectionSensorLog +} + + + +struct NodeListSplit: View { + + @State private var columnVisibility = NavigationSplitViewVisibility.all + @State private var selectedNode: NodeInfoEntity? + @State private var selectedDetail: SelectedDetail? + + @SceneStorage("selectedDetailView") var selectedDetailView: String? + + @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 + + + + 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: $selectedNode) { node in + + NodeListItem(node: node, connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0)) + } + .searchable(text: nodesQuery, prompt: "Find a node") + .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) + .listStyle(.plain) + .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) + .navigationBarItems(leading: + MeshtasticLogo(), + trailing: + ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) + }) + } content: { + + if let node = selectedNode { + NodeDetail(node: node) + + } else { + Text("select.node") + } + + } detail: { + Text("Select something to view") + } + .navigationSplitViewStyle(.balanced) + .onChange(of: selectedNode) { _ in + selectedDetail = nil + } + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + } + +// } detail: { +// VStack { +// Button("Detail Only") { +// columnVisibility = .detailOnly // } -// } -// .listStyle(.plain) -// .navigationSplitViewColumnWidth(300) -// .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) -// .navigationBarItems(leading: -// MeshtasticLogo() -// ) -// .onAppear { -// if self.bleManager.context == nil { -// self.bleManager.context = context +// +// Button("Content and Detail") { +// columnVisibility = .doubleColumn +// } +// +// Button("Show All") { +// columnVisibility = .all // } // } -// } detail: { -// if let node = selection { -// NodeDetail(node: node) -// } else { -// Text("select.node") -// } -// } -// .searchable(text: nodesQuery, prompt: "Find a node") -// } -//} +// } + } +} diff --git a/Meshtastic/Views/Nodes/NodeListOld.swift b/Meshtastic/Views/Nodes/NodeListOld.swift new file mode 100644 index 00000000..c47b1909 --- /dev/null +++ b/Meshtastic/Views/Nodes/NodeListOld.swift @@ -0,0 +1,136 @@ +//// +//// NodeList.swift +//// Meshtastic +//// +//// Copyright(c) Garth Vander Houwen 8/7/21. +//// +// +//// Abstract: +//// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device. +// +//import SwiftUI +//import CoreLocation +// +//struct NodeList: View { +// +// @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 { +// 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 +// if nodes.count == 0 { +// Text("no.nodes").font(.title) +// } else { +// NavigationLink(value: node) { +// let connected: Bool = (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]) +// } +// } +// .listStyle(.plain) +// .navigationSplitViewColumnWidth(300) +// .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) +// .navigationBarItems(leading: +// MeshtasticLogo() +// ) +// .onAppear { +// if self.bleManager.context == nil { +// self.bleManager.context = context +// } +// } +// } detail: { +// if let node = selection { +// NodeDetail(node: node) +// } else { +// Text("select.node") +// } +// } +// .searchable(text: nodesQuery, prompt: "Find a node") +// } +//} diff --git a/Meshtastic/Views/Nodes/NodeListSplit.swift b/Meshtastic/Views/Nodes/NodeListSplit.swift deleted file mode 100644 index 8ec52f3b..00000000 --- a/Meshtastic/Views/Nodes/NodeListSplit.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// NodeListSplit.swift -// Meshtastic -// -// Created by Garth Vander Houwen on 9/8/23. -// -import SwiftUI -import CoreLocation - -enum SelectedDetail { - case positionLog - case nodeMap - case deviceMetricsLog - case environmentMetricsLog - case detectionSensorLog -} - -struct NodeListSplit: View { - - @State private var columnVisibility = NavigationSplitViewVisibility.all - @State private var selectedNode: NodeInfoEntity? - @State private var selectedDetail: SelectedDetail? - - @SceneStorage("selectedDetailView") var selectedDetailView: String? - - @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 - - - - 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: $selectedNode) { node in - - NodeListItem(node: node, connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0)) - } - .searchable(text: nodesQuery, prompt: "Find a node") - .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) - .listStyle(.plain) - .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) - .navigationBarItems(leading: - MeshtasticLogo() - ) - } content: { - - if let node = selectedNode { - NodeDetailItem(node: node) - - } else { - Text("select.node") - } - - } detail: { - Text("Select something to view") - } - .navigationSplitViewStyle(.balanced) - .onChange(of: selectedNode) { _ in - selectedDetail = nil - } - .onAppear { - if self.bleManager.context == nil { - self.bleManager.context = context - } - } - -// } detail: { -// VStack { -// Button("Detail Only") { -// columnVisibility = .detailOnly -// } -// -// Button("Content and Detail") { -// columnVisibility = .doubleColumn -// } -// -// Button("Show All") { -// columnVisibility = .all -// } -// } -// } - } -} diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 2100f955..0ccbd6cd 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -24,12 +24,6 @@ struct PositionLog: View { var body: some View { NavigationStack { - - if node.hasPositions { - - } else { - Text("Node has no positions.") - } let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") if UIDevice.current.userInterfaceIdiom == .pad && !useGrid || UIDevice.current.userInterfaceIdiom == .mac { From 4efc73bf9a3adca9d6b9b75e2592eaa98cf8866c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Sep 2023 07:32:09 -0700 Subject: [PATCH 09/16] Make battery gauge update properly --- Meshtastic/Views/Helpers/BatteryGauge.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index a52de95d..701d7a06 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -9,7 +9,7 @@ import SwiftUI import Charts struct BatteryGauge: View { - @State var batteryLevel = 0.0 + var batteryLevel = 0.0 private let minValue = 0.0 private let maxValue = 100.00 From cafa394ee1c2e74dd789cd395c0f680a601e4efd Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Sep 2023 14:22:30 -0700 Subject: [PATCH 10/16] Move deleted file clean up node list a bit --- Meshtastic.xcodeproj/project.pbxproj | 20 ++++++------------- .../Views/Nodes/Helpers/NodeListItem.swift | 12 +++++------ .../NodeInfoViewOld.swift} | 0 3 files changed, 12 insertions(+), 20 deletions(-) rename Meshtastic/Views/{Helpers/Node/NodeInfoView.swift => Nodes/NodeInfoViewOld.swift} (100%) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e2e69453..b4c4f63b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -162,7 +162,7 @@ DDDE5A1129AFE69700490C6C /* MeshActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */; }; DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; - DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */; }; + DDDEE5E129DA3E1100A8E078 /* NodeInfoViewOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDEE5E029DA3E1100A8E078 /* NodeInfoViewOld.swift */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; @@ -382,7 +382,7 @@ DDDE5A0429AF163E00490C6C /* WidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetsExtension.entitlements; sourceTree = ""; }; DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshActivityAttributes.swift; sourceTree = ""; }; DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoView.swift; sourceTree = ""; }; + DDDEE5E029DA3E1100A8E078 /* NodeInfoViewOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoViewOld.swift; sourceTree = ""; }; DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; @@ -469,11 +469,12 @@ children = ( DDDB26402AABEF7B003AFCB7 /* Helpers */, DDDB263E2AABEE20003AFCB7 /* NodeList.swift */, - 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */, - DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, DD90860D26F69BAE00DC5189 /* NodeMap.swift */, DD73FD1028750779000852D6 /* PositionLog.swift */, + DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, + 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, + DDDEE5E029DA3E1100A8E078 /* NodeInfoViewOld.swift */, DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */, DD47E3CD26F103C600029299 /* NodeListOld.swift */, ); @@ -749,7 +750,6 @@ DDC2E18D26CE25CB0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( - DDDEE5DF29DA3DA000A8E078 /* Node */, DD5E523D298F5A7D00D21B61 /* Weather */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, @@ -856,14 +856,6 @@ path = Widgets; sourceTree = ""; }; - DDDEE5DF29DA3DA000A8E078 /* Node */ = { - isa = PBXGroup; - children = ( - DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */, - ); - path = Node; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1138,7 +1130,7 @@ 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */, DD47E3CE26F103C600029299 /* NodeListOld.swift in Sources */, DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */, - DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */, + DDDEE5E129DA3E1100A8E078 /* NodeInfoViewOld.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 432901d6..f2397f26 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -34,7 +34,7 @@ struct NodeListItem: View { .fontWeight(.medium) .font(.callout) if connected { - HStack(alignment: .bottom) { + HStack { Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") .font(.footnote) .symbolRenderingMode(.hierarchical) @@ -42,7 +42,7 @@ struct NodeListItem: View { Text("connected").font(.caption) } } - HStack(alignment: .bottom) { + HStack { Image(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill") .font(.footnote) .symbolRenderingMode(.hierarchical) @@ -51,7 +51,7 @@ struct NodeListItem: View { .font(.caption) } if node.positions?.count ?? 0 > 0 && connectedNode != node.num { - HStack(alignment: .bottom) { + HStack { 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 { @@ -65,17 +65,17 @@ struct NodeListItem: View { } } if node.channel > 0 { - HStack(alignment: .bottom) { + HStack { Image(systemName: "fibrechannel") .font(.footnote) .symbolRenderingMode(.hierarchical) Text("Channel: \(node.channel)") - .font(.footnote) + .font(.caption) } } if !connected { - HStack(alignment: .bottom) { + HStack { let preset = ModemPresets(rawValue: Int(modemPreset)) LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true) } diff --git a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Nodes/NodeInfoViewOld.swift similarity index 100% rename from Meshtastic/Views/Helpers/Node/NodeInfoView.swift rename to Meshtastic/Views/Nodes/NodeInfoViewOld.swift From 0dac9bc6a5c0ff3d80fcbc164e6ae0028ef83eeb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Sep 2023 14:59:51 -0700 Subject: [PATCH 11/16] YOLO the mesh map --- Meshtastic/Views/Nodes/NodeMap.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 1948f6ea..e6edd97a 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -26,8 +26,10 @@ struct NodeMap: View { @State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels let fromDate: NSDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! as NSDate +// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], +// predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], - predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) + predicate: NSPredicate(format: "nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) private var positions: FetchedResults @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], predicate: NSPredicate( From b80b9e65b303d1fcdd9321d089c2c2a27024dc7d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Sep 2023 15:41:03 -0700 Subject: [PATCH 12/16] Add small weather widget back to node map --- Meshtastic.xcodeproj/project.pbxproj | 4 - Meshtastic/Views/ContentView.swift | 2 +- Meshtastic/Views/Nodes/Helpers/NodeInfo.swift | 93 +++++++ .../Views/Nodes/Helpers/NodeMapControl.swift | 54 ++++ Meshtastic/Views/Nodes/NodeInfoViewOld.swift | 261 ------------------ Meshtastic/Views/Nodes/NodeList.swift | 2 +- 6 files changed, 149 insertions(+), 267 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/NodeInfo.swift delete mode 100644 Meshtastic/Views/Nodes/NodeInfoViewOld.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b4c4f63b..2049f791 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -162,7 +162,6 @@ DDDE5A1129AFE69700490C6C /* MeshActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */; }; DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; - DDDEE5E129DA3E1100A8E078 /* NodeInfoViewOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDEE5E029DA3E1100A8E078 /* NodeInfoViewOld.swift */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; @@ -382,7 +381,6 @@ DDDE5A0429AF163E00490C6C /* WidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetsExtension.entitlements; sourceTree = ""; }; DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshActivityAttributes.swift; sourceTree = ""; }; DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - DDDEE5E029DA3E1100A8E078 /* NodeInfoViewOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoViewOld.swift; sourceTree = ""; }; DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; @@ -474,7 +472,6 @@ DD73FD1028750779000852D6 /* PositionLog.swift */, DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, - DDDEE5E029DA3E1100A8E078 /* NodeInfoViewOld.swift */, DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */, DD47E3CD26F103C600029299 /* NodeListOld.swift */, ); @@ -1130,7 +1127,6 @@ 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */, DD47E3CE26F103C600029299 /* NodeListOld.swift in Sources */, DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */, - DDDEE5E129DA3E1100A8E078 /* NodeInfoViewOld.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 5f2340fe..35b2a9ea 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -19,7 +19,7 @@ struct ContentView: View { Label("bluetooth", systemImage: "antenna.radiowaves.left.and.right") } .tag(Tab.ble) - NodeListSplit() + NodeList() .tabItem { Label("nodes", systemImage: "flipphone") } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift new file mode 100644 index 00000000..3c230e27 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfo.swift @@ -0,0 +1,93 @@ +// +// NodeInfoItem.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 9/9/23. +// + +import SwiftUI +import CoreLocation +import MapKit + +struct NodeInfoItem: View { + + @ObservedObject var node: NodeInfoEntity + + var body: some View { + + Divider() + + HStack { + + VStack(alignment: .center) { + CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) + } + if node.user != nil { + Divider() + VStack { + Image(node.user!.hwModel ?? "unset".localized) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 75, height: 75) + .cornerRadius(5) + Text(String(node.user!.hwModel ?? "unset".localized)) + .font(.caption2).fixedSize() + } + } + if node.snr != 0 { + Divider() + VStack(alignment: .center) { + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", node.snr))dB") + .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) + .font(.caption2) + Text("RSSI \(node.rssi)dB") + .foregroundColor(getRssiColor(rssi: node.rssi)) + .font(.caption2) + } + } + let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) + if deviceMetrics?.count ?? 0 >= 1 { + Divider() + let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + VStack(alignment: .center) { + BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) + if mostRecent?.voltage ?? 0 > 0 { + + Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V") + .font(.callout) + .foregroundColor(.gray) + .fixedSize() + } + } + } + } + Divider() + HStack(alignment: .center) { + VStack { + HStack { + Image(systemName: "number") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("Node Number:").font(.title2) + } + Text(String(node.num)).font(.title3).foregroundColor(.gray) + } + Divider() + VStack { + HStack { + Image(systemName: "person") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("User Id:").font(.title2) + } + Text(node.user?.userId ?? "?").font(.title3).foregroundColor(.gray) + } + } + Divider() + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift index 1a816892..de1e8eff 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift @@ -7,11 +7,21 @@ import SwiftUI import CoreLocation import MapKit +import WeatherKit struct NodeMapControl: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + /// Weather + /// The current weather condition for the city. + @State private var condition: WeatherCondition? + @State private var temperature: Measurement? + @State private var humidity: Int? + @State private var symbolName: String = "cloud.fill" + @State private var attributionLink: URL? + @State private var attributionLogo: URL? + @Environment(\.colorScheme) var colorScheme: ColorScheme @AppStorage("meshMapType") private var meshMapType = 0 @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false @@ -76,6 +86,50 @@ struct NodeMapControl: View { .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .pickerStyle(.menu) .padding(5) + VStack { + Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) + .font(.caption) + + Label("\(humidity ?? 0)%", systemImage: "humidity") + .font(.caption2) + } + .padding(10) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .padding(5) + VStack { + AsyncImage(url: attributionLogo) { image in + image + .resizable() + .scaledToFit() + } placeholder: { + ProgressView() + .controlSize(.mini) + } + .frame(height: 15) + + Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!) + } + .font(.footnote) + .padding(.bottom, 5) + .task { + do { + if node.hasPositions { + let mostRecent = node.positions?.lastObject as? PositionEntity + let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)) + condition = weather.currentWeather.condition + temperature = weather.currentWeather.temperature + humidity = Int(weather.currentWeather.humidity * 100) + symbolName = weather.currentWeather.symbolName + let attribution = try await WeatherService.shared.attribution + attributionLink = attribution.legalPageURL + attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL + } + } catch { + print("Could not gather weather information...", error.localizedDescription) + condition = .clear + symbolName = "cloud.fill" + } + } } } } diff --git a/Meshtastic/Views/Nodes/NodeInfoViewOld.swift b/Meshtastic/Views/Nodes/NodeInfoViewOld.swift deleted file mode 100644 index 94dfdf4f..00000000 --- a/Meshtastic/Views/Nodes/NodeInfoViewOld.swift +++ /dev/null @@ -1,261 +0,0 @@ -//// -//// NodeInfoView.swift -//// Meshtastic -//// -//// Created by Garth Vander Houwen on 4/2/23. -//// -// -//// -//// DistanceText.swift -//// Meshtastic -//// -//// Copyright(c) Garth Vander Houwen 8/19/22. -//// -// -//import SwiftUI -//import CoreLocation -//import MapKit -// -//struct NodeInfoView: View { -// -// var node: NodeInfoEntity -// -// var body: some View { -// let hwModelString = node.user?.hwModel ?? "UNSET" -// -// Divider() -// if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { -// HStack { -// VStack(alignment: .center) { -// CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 150) -// } -// Divider() -// VStack { -// if node.user != nil { -// Image(hwModelString) -// .resizable() -// .aspectRatio(contentMode: .fit) -// .frame(width: 100, height: 100) -// .cornerRadius(5) -// -// Text(String(hwModelString)) -// .foregroundColor(.gray) -// .font(.title).fixedSize() -// } -// } -// Divider() -// if node.snr != 0 { -// VStack(alignment: .center) { -// let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) -// LoRaSignalStrengthIndicator(signalStrength: signalStrength) -// Text("Signal \(signalStrength.description)").font(.title) -// Text("SNR \(String(format: "%.2f", node.snr))dB") -// .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) -// .font(.title3) -// Text("RSSI \(node.rssi)dB") -// .foregroundColor(getRssiColor(rssi: node.rssi)) -// .font(.title3) -// } -// Divider() -// } -// -// if node.hasDeviceMetrics { -// let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) -// let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity -// VStack(alignment: .center) { -// BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) -// if mostRecent?.voltage ?? 0 > 0.0 { -// -// Text(String(format: "%.2f", mostRecent?.voltage ?? 0.0) + " V") -// .font(.title) -// .foregroundColor(.gray) -// .fixedSize() -// } -// } -// .padding() -// } -// } -// .padding() -// -// Divider() -// HStack(alignment: .center) { -// -// VStack { -// HStack { -// Image(systemName: "person") -// .font(.title) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("user").font(.title)+Text(":").font(.title) -// } -// Text("!\(String(format: "%02x", node.num))") -// .font(.title).foregroundColor(.gray) -// } -// Divider() -// VStack { -// HStack { -// Image(systemName: "number") -// .font(.title2) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("Node Number:").font(.title) -// } -// Text(String(node.num)).font(.title).foregroundColor(.gray) -// } -// Divider() -// VStack { -// HStack { -// Image(systemName: "clock.badge.checkmark.fill") -// .font(.title) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("heard.last").font(.title)+Text(":").font(.title) -// -// } -// DateTimeText(dateTime: node.lastHeard) -// .font(.title3) -// .foregroundColor(.gray) -// } -// } -// Divider() -// -// } else { -// -// HStack { -// -// VStack(alignment: .center) { -// CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65) -// } -// if node.user != nil { -// Divider() -// VStack { -// Image(node.user!.hwModel ?? "unset".localized) -// .resizable() -// .aspectRatio(contentMode: .fit) -// .frame(width: 75, height: 75) -// .cornerRadius(5) -// Text(String(node.user!.hwModel ?? "unset".localized)) -// .font(.caption2).fixedSize() -// } -// } -// if node.snr != 0 { -// Divider() -// VStack(alignment: .center) { -// let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) -// LoRaSignalStrengthIndicator(signalStrength: signalStrength) -// Text("Signal \(signalStrength.description)").font(.footnote) -// Text("SNR \(String(format: "%.2f", node.snr))dB") -// .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) -// .font(.caption2) -// Text("RSSI \(node.rssi)dB") -// .foregroundColor(getRssiColor(rssi: node.rssi)) -// .font(.caption2) -// } -// } -// let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) -// if deviceMetrics?.count ?? 0 >= 1 { -// Divider() -// let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity -// VStack(alignment: .center) { -// BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) -// if mostRecent?.voltage ?? 0 > 0 { -// -// Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V") -// .font(.callout) -// .foregroundColor(.gray) -// .fixedSize() -// } -// } -// } -// } -// Divider() -// HStack(alignment: .center) { -// VStack { -// HStack { -// Image(systemName: "person") -// .font(.title2) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("User Id:").font(.title2) -// } -// Text(node.user?.userId ?? "?").font(.title3).foregroundColor(.gray) -// } -// Divider() -// VStack { -// HStack { -// Image(systemName: "number") -// .font(.title2) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("Node Number:").font(.title2) -// } -// Text(String(node.num)).font(.title3).foregroundColor(.gray) -// } -// } -// Divider() -// } -// -// VStack { -// -// if node.hasPositions{ -// -// NavigationLink { -// PositionLog(node: node) -// } label: { -// -// Image(systemName: "building.columns") -// .symbolRenderingMode(.hierarchical) -// .font(.title) -// -// Text("Position Log") -// .font(.title3) -// } -// .fixedSize(horizontal: false, vertical: true) -// Divider() -// } -// -// if node.hasDeviceMetrics { -// -// NavigationLink { -// DeviceMetricsLog(node: node) -// } label: { -// -// Image(systemName: "flipphone") -// .symbolRenderingMode(.hierarchical) -// .font(.title) -// -// Text("Device Metrics Log") -// .font(.title3) -// } -// Divider() -// } -// if node.hasEnvironmentMetrics { -// NavigationLink { -// EnvironmentMetricsLog(node: node) -// } label: { -// -// Image(systemName: "chart.xyaxis.line") -// .symbolRenderingMode(.hierarchical) -// .font(.title) -// -// Text("Environment Metrics Log") -// .font(.title3) -// } -// Divider() -// } -// NavigationLink { -// DetectionSensorLog(node: node) -// } label: { -// -// Image(systemName: "sensor") -// .symbolRenderingMode(.hierarchical) -// .font(.title) -// -// Text("Detection Sensor Log") -// .font(.title3) -// } -// .fixedSize(horizontal: false, vertical: true) -// Divider() -// } -// } -//} diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 08b6ff7c..9a971ac0 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -17,7 +17,7 @@ enum SelectedDetail { -struct NodeListSplit: View { +struct NodeList: View { @State private var columnVisibility = NavigationSplitViewVisibility.all @State private var selectedNode: NodeInfoEntity? From e8d4a7085713e96ab99262940f11ff5450aa53cb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Sep 2023 17:42:53 -0700 Subject: [PATCH 13/16] Fix up weather widget --- .../Views/Nodes/Helpers/NodeDetail.swift | 12 +++--- .../Views/Nodes/Helpers/NodeMapControl.swift | 41 ++++++++++--------- Meshtastic/Views/Nodes/NodeList.swift | 23 +++++++---- 3 files changed, 42 insertions(+), 34 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index fee6fdbe..2637e81c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -17,6 +17,7 @@ struct NodeDetail: View { @State private var showingRebootConfirm: Bool = false @ObservedObject var node: NodeInfoEntity + var columnVisibility = NavigationSplitViewVisibility.all var body: some View { @@ -146,12 +147,13 @@ struct NodeDetail: View { } .edgesIgnoringSafeArea([.leading, .trailing]) .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) - .navigationBarItems(trailing: + .navigationBarItems( + trailing: ZStack { - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) }) } .padding(.bottom, 2) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift index de1e8eff..75a455b7 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift @@ -87,30 +87,31 @@ struct NodeMapControl: View { .pickerStyle(.menu) .padding(5) VStack { - Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) - .font(.caption) + VStack { + Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) + .font(.caption) + + Label("\(humidity ?? 0)%", systemImage: "humidity") + .font(.caption2) - Label("\(humidity ?? 0)%", systemImage: "humidity") - .font(.caption2) + AsyncImage(url: attributionLogo) { image in + image + .resizable() + .scaledToFit() + } placeholder: { + ProgressView() + .controlSize(.mini) + } + .frame(height: 10) + + Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!) + .font(.caption2) + } + .padding(5) + } - .padding(10) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) .padding(5) - VStack { - AsyncImage(url: attributionLogo) { image in - image - .resizable() - .scaledToFit() - } placeholder: { - ProgressView() - .controlSize(.mini) - } - .frame(height: 15) - - Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!) - } - .font(.footnote) - .padding(.bottom, 5) .task { do { if node.hasPositions { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 9a971ac0..2d916934 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -21,10 +21,6 @@ struct NodeList: View { @State private var columnVisibility = NavigationSplitViewVisibility.all @State private var selectedNode: NodeInfoEntity? - @State private var selectedDetail: SelectedDetail? - - @SceneStorage("selectedDetailView") var selectedDetailView: String? - @State private var searchText = "" var nodesQuery: Binding { Binding { @@ -69,20 +65,29 @@ struct NodeList: View { name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) }) } content: { - if let node = selectedNode { - NodeDetail(node: node) - + NodeDetail(node: node, columnVisibility: columnVisibility) + VStack { + Button { + columnVisibility = .detailOnly + } label: { + Image(systemName: "rectangle") + } + } + .padding(.bottom, 5) } else { Text("select.node") } - } detail: { Text("Select something to view") } .navigationSplitViewStyle(.balanced) .onChange(of: selectedNode) { _ in - selectedDetail = nil + if selectedNode == nil { + columnVisibility = .all + } else { + columnVisibility = .doubleColumn + } } .onAppear { if self.bleManager.context == nil { From b613d0427ea2cdd4acf387b61b64cf97709c3143 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Sep 2023 18:00:33 -0700 Subject: [PATCH 14/16] Detail only button on node details view --- .../Views/Nodes/Helpers/NodeDetail.swift | 14 +------ Meshtastic/Views/Nodes/NodeList.swift | 40 ++++++++++++------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 2637e81c..1dd43ba4 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -22,7 +22,7 @@ struct NodeDetail: View { var body: some View { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) - NavigationStack { +// NavigationStack { GeometryReader { bounds in VStack { ScrollView { @@ -145,18 +145,8 @@ struct NodeDetail: View { self.bleManager.context = context } } - .edgesIgnoringSafeArea([.leading, .trailing]) - .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) - .navigationBarItems( - trailing: - ZStack { - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) - }) } .padding(.bottom, 2) - } +// } } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 2d916934..7aa17db0 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -66,13 +66,25 @@ struct NodeList: View { }) } content: { if let node = selectedNode { - NodeDetail(node: node, columnVisibility: columnVisibility) - VStack { - Button { - columnVisibility = .detailOnly - } label: { - Image(systemName: "rectangle") - } + NavigationStack { + NodeDetail(node: node, columnVisibility: columnVisibility) + .edgesIgnoringSafeArea([.leading, .trailing]) + .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) + .navigationBarItems( + trailing: + ZStack { + if (UIDevice.current.userInterfaceIdiom != .phone) { + Button { + columnVisibility = .detailOnly + } label: { + Image(systemName: "rectangle") + } + } + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) + }) } .padding(.bottom, 5) } else { @@ -82,13 +94,13 @@ struct NodeList: View { Text("Select something to view") } .navigationSplitViewStyle(.balanced) - .onChange(of: selectedNode) { _ in - if selectedNode == nil { - columnVisibility = .all - } else { - columnVisibility = .doubleColumn - } - } +// .onChange(of: selectedNode) { _ in +// if selectedNode == nil { +// columnVisibility = .all +// } else { +// columnVisibility = .doubleColumn +// } +// } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context From a2a4c65c30226c284af690f65a8d8d9b94b6944c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Sep 2023 18:50:11 -0700 Subject: [PATCH 15/16] Cleanup --- Meshtastic.xcodeproj/project.pbxproj | 4 - .../Views/Nodes/DetectionSensorLog.swift | 4 +- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 4 +- .../Views/Nodes/EnvironmentMetricsLog.swift | 4 +- .../Views/Nodes/Helpers/NodeDetail.swift | 4 +- Meshtastic/Views/Nodes/NodeListOld.swift | 136 ------------------ Meshtastic/Views/Nodes/NodeMap.swift | 4 +- 7 files changed, 14 insertions(+), 146 deletions(-) delete mode 100644 Meshtastic/Views/Nodes/NodeListOld.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 2049f791..d1eb78ab 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -37,7 +37,6 @@ DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41582928585C32009B0E59 /* RangeTestConfig.swift */; }; DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */; }; DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */; }; - DD47E3CE26F103C600029299 /* NodeListOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeListOld.swift */; }; DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; }; DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; }; DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */; }; @@ -241,7 +240,6 @@ DD41A61E29AE7E8F003C5A37 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLESignalStrengthIndicator.swift; sourceTree = ""; }; DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV5.xcdatamodel; sourceTree = ""; }; - DD47E3CD26F103C600029299 /* NodeListOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListOld.swift; sourceTree = ""; }; DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = ""; }; DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = ""; }; @@ -473,7 +471,6 @@ DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */, - DD47E3CD26F103C600029299 /* NodeListOld.swift */, ); path = Nodes; sourceTree = ""; @@ -1125,7 +1122,6 @@ DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */, DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */, 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */, - DD47E3CE26F103C600029299 /* NodeListOld.swift in Sources */, DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index c3604ab8..6d2551cd 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -124,7 +124,9 @@ struct DetectionSensorLog: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } } .fileExporter( isPresented: $isExporting, diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 08cd55c6..232e1b8f 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -211,7 +211,9 @@ struct DeviceMetricsLog: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } } .fileExporter( isPresented: $isExporting, diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index ccc0ccdb..f402be8a 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -193,7 +193,9 @@ struct EnvironmentMetricsLog: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } } .fileExporter( isPresented: $isExporting, diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 1dd43ba4..3ee69c79 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -22,7 +22,7 @@ struct NodeDetail: View { var body: some View { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) -// NavigationStack { + NavigationStack { GeometryReader { bounds in VStack { ScrollView { @@ -147,6 +147,6 @@ struct NodeDetail: View { } } .padding(.bottom, 2) -// } + } } } diff --git a/Meshtastic/Views/Nodes/NodeListOld.swift b/Meshtastic/Views/Nodes/NodeListOld.swift deleted file mode 100644 index c47b1909..00000000 --- a/Meshtastic/Views/Nodes/NodeListOld.swift +++ /dev/null @@ -1,136 +0,0 @@ -//// -//// NodeList.swift -//// Meshtastic -//// -//// Copyright(c) Garth Vander Houwen 8/7/21. -//// -// -//// Abstract: -//// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device. -// -//import SwiftUI -//import CoreLocation -// -//struct NodeList: View { -// -// @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 { -// 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 -// if nodes.count == 0 { -// Text("no.nodes").font(.title) -// } else { -// NavigationLink(value: node) { -// let connected: Bool = (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]) -// } -// } -// .listStyle(.plain) -// .navigationSplitViewColumnWidth(300) -// .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) -// .navigationBarItems(leading: -// MeshtasticLogo() -// ) -// .onAppear { -// if self.bleManager.context == nil { -// self.bleManager.context = context -// } -// } -// } detail: { -// if let node = selection { -// NodeDetail(node: node) -// } else { -// Text("select.node") -// } -// } -// .searchable(text: nodesQuery, prompt: "Find a node") -// } -//} diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index e6edd97a..a41f891c 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -238,7 +238,9 @@ struct NodeMap: View { }) .onAppear(perform: { UIApplication.shared.isIdleTimerDisabled = true - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } }) .onDisappear(perform: { UIApplication.shared.isIdleTimerDisabled = false From 30300536f67d7f9f5ef141ef1080ff1cabf904d9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 13 Sep 2023 05:08:01 -0700 Subject: [PATCH 16/16] Update protos --- Meshtastic/Protobufs/meshtastic/config.pb.swift | 5 +++-- Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 5 ++--- .../Protobufs/meshtastic/module_config.pb.swift | 16 +++++++++++----- .../Protobufs/meshtastic/telemetry.pb.swift | 6 +----- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index f327d250..eeee9214 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -375,8 +375,9 @@ struct Config { /// /// Bit field of boolean configuration options, indicating which optional - /// fields to include when assembling POSITION messages - /// Longitude and latitude are always included (also time if GPS-synced) + /// fields to include when assembling POSITION messages. + /// Longitude, latitude, altitude, speed, heading, and DOP + /// are always included (also time if GPS-synced) /// NOTE: the more fields are included, the larger the message will be - /// leading to longer airtime and a higher risk of packet loss enum PositionFlags: SwiftProtobuf.Enum { diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 9467436f..c4d94c49 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -583,9 +583,8 @@ struct Position { /// /// This is usually not sent over the mesh (to save space), but it is sent - /// from the phone so that the local device can set its RTC If it is sent over - /// the mesh (because there are devices on the mesh without GPS), it will only - /// be sent by devices which has a hardware GPS clock. + /// from the phone so that the local device can set its time if it is sent over + /// the mesh (because there are devices on the mesh without GPS or RTC). /// seconds since 1970 var time: UInt32 { get {return _storage._time} diff --git a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift index 5fae17a2..a1e60223 100644 --- a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift @@ -1020,6 +1020,7 @@ struct ModuleConfig { init() {} } + /// ///Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. ///Initially created for the RAK14001 RGB LED module. struct AmbientLightingConfig { @@ -1027,19 +1028,24 @@ struct ModuleConfig { // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - ///Sets LED to on or off. + /// + /// Sets LED to on or off. var ledState: Bool = false - ///Sets the overall current for the LED, firmware side range for the RAK14001 is 1-31, but users should be given a range of 0-100% + /// + /// Sets the current for the LED output. Default is 10. var current: UInt32 = 0 - /// Red level + /// + /// Sets the red LED level. Values are 0-255. var red: UInt32 = 0 - ///Sets the green level of the LED, firmware side values are 0-255, but users should be given a range of 0-100% + /// + /// Sets the green LED level. Values are 0-255. var green: UInt32 = 0 - ///Sets the blue level of the LED, firmware side values are 0-255, but users should be given a range of 0-100% + /// + /// Sets the blue LED level. Values are 0-255. var blue: UInt32 = 0 var unknownFields = SwiftProtobuf.UnknownStorage() diff --git a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift index 4294c83b..dc365499 100644 --- a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift @@ -284,11 +284,7 @@ struct Telemetry { // methods supported on all messages. /// - /// This is usually not sent over the mesh (to save space), but it is sent - /// from the phone so that the local device can set its RTC If it is sent over - /// the mesh (because there are devices on the mesh without GPS), it will only - /// be sent by devices which has a hardware GPS clock (IE Mobile Phone). - /// seconds since 1970 + /// Seconds since 1970 - or 0 for unknown/unset var time: UInt32 = 0 var variant: Telemetry.OneOf_Variant? = nil