diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 765de0df..d1eb78ab 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 */; }; @@ -26,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 */; }; @@ -38,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 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.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 */; }; @@ -135,6 +133,11 @@ 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 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB263E2AABEE20003AFCB7 /* NodeList.swift */; }; + DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26412AABF655003AFCB7 /* NodeListItem.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 */; }; DDDB443D29F6592F00EE2349 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443C29F6592F00EE2349 /* NetworkManager.swift */; }; DDDB444029F79AB000EE2349 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB443F29F79AB000EE2349 /* UserDefaults.swift */; }; @@ -158,7 +161,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 /* NodeInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDEE5E029DA3E1100A8E078 /* NodeInfoView.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 */; }; @@ -214,7 +216,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 = ""; }; @@ -224,7 +225,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 = ""; }; @@ -239,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 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.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 = ""; }; @@ -351,6 +351,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 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = ""; }; + DDDB26412AABF655003AFCB7 /* NodeListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListItem.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 = ""; }; @@ -373,7 +379,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 /* NodeInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoView.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 = ""; }; @@ -458,14 +463,14 @@ DD47E3CA26F0E50300029299 /* Nodes */ = { isa = PBXGroup; children = ( + DDDB26402AABEF7B003AFCB7 /* Helpers */, + DDDB263E2AABEE20003AFCB7 /* NodeList.swift */, DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */, - DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, - DD2E65252767A01F00E45FC5 /* NodeDetail.swift */, - DD47E3CD26F103C600029299 /* NodeList.swift */, DD90860D26F69BAE00DC5189 /* NodeMap.swift */, DD73FD1028750779000852D6 /* PositionLog.swift */, - DD14E72D2A82A614006E39BC /* RemoteHardware.swift */, + DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, + DD2E65252767A01F00E45FC5 /* NodeDetailOld.swift */, ); path = Nodes; sourceTree = ""; @@ -654,6 +659,7 @@ DDDB443E29F79A9400EE2349 /* Extensions */, DDC2E1A526CEB32B0042C5E4 /* Helpers */, DDC2E18826CE24EE0042C5E4 /* Model */, + DDDB263D2AABD34F003AFCB7 /* Navigation */, DDC4D5662754996200A4208E /* Persistence */, DDAF8C5626ED07740058C060 /* Protobufs */, DDC2E18926CE24F70042C5E4 /* Resources */, @@ -738,7 +744,6 @@ DDC2E18D26CE25CB0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( - DDDEE5DF29DA3DA000A8E078 /* Node */, DD5E523D298F5A7D00D21B61 /* Weather */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, @@ -790,6 +795,24 @@ path = Mqtt; sourceTree = ""; }; + DDDB263D2AABD34F003AFCB7 /* Navigation */ = { + isa = PBXGroup; + children = ( + ); + path = Navigation; + sourceTree = ""; + }; + DDDB26402AABEF7B003AFCB7 /* Helpers */ = { + isa = PBXGroup; + children = ( + DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */, + DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */, + DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, + DDDB26472AACD6D1003AFCB7 /* NodeMapControl.swift */, + ); + path = Helpers; + sourceTree = ""; + }; DDDB443E29F79A9400EE2349 /* Extensions */ = { isa = PBXGroup; children = ( @@ -827,14 +850,6 @@ path = Widgets; sourceTree = ""; }; - DDDEE5DF29DA3DA000A8E078 /* Node */ = { - isa = PBXGroup; - children = ( - DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */, - ); - path = Node; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1068,12 +1083,15 @@ 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 */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, + DDDB263F2AABEE20003AFCB7 /* NodeList.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 */, @@ -1097,15 +1115,14 @@ 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 */, DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */, DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */, 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */, - DD47E3CE26F103C600029299 /* NodeList.swift in Sources */, DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */, - DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, @@ -1127,7 +1144,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 */, @@ -1159,6 +1176,7 @@ DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.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 */, @@ -1166,7 +1184,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 */, @@ -1381,7 +1398,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 +1432,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 +1554,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 +1587,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 = ""; @@ -1681,6 +1698,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */, DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */, DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */, DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */, @@ -1699,7 +1717,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 d24f69f1..c041a7dd 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -24,4 +24,16 @@ 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 { + + let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date()) + if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending { + return true + } + return false + } } 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/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/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/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 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 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/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/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/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift deleted file mode 100644 index 362de937..00000000 --- a/Meshtastic/Views/Helpers/Node/NodeInfoView.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/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/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index dcda62a4..6d2551cd 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()) @@ -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 25a161a4..232e1b8f 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 { @@ -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 10fb667e..f402be8a 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()) @@ -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 new file mode 100644 index 00000000..3ee69c79 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -0,0 +1,152 @@ +/* + 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 + @State private var showingShutdownConfirm: Bool = false + @State private var showingRebootConfirm: Bool = false + + @ObservedObject var node: NodeInfoEntity + var columnVisibility = NavigationSplitViewVisibility.all + + var body: some View { + + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) + NavigationStack { + GeometryReader { bounds in + 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 { + 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: "mappin.and.ellipse") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Position Log") + .font(.title3) + } + .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: { + Image(systemName: "sensor") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Detection Sensor Log") + .font(.title3) + } + .disabled(!node.hasDetectionSensorMetrics) + Divider() + } + + + 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() + } + } + } + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + } + } + .padding(.bottom, 2) + } + } +} 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/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift new file mode 100644 index 00000000..3c230e27 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.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/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift new file mode 100644 index 00000000..f2397f26 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -0,0 +1,90 @@ +// +// NodeListItem.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/8/23. +// + +import SwiftUI +import CoreLocation + +struct NodeListItem: View { + + @ObservedObject var node: NodeInfoEntity + var connected: Bool + var connectedNode: Int64 + var modemPreset: Int + + var body: some View { + + NavigationLink(value: node) { + 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 { + Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") + .font(.footnote) + .symbolRenderingMode(.hierarchical) + .foregroundColor(.green) + Text("connected").font(.caption) + } + } + HStack { + 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 && connectedNode != node.num { + 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 { + 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 { + Image(systemName: "fibrechannel") + .font(.footnote) + .symbolRenderingMode(.hierarchical) + Text("Channel: \(node.channel)") + .font(.caption) + } + } + + if !connected { + HStack { + let preset = ModemPresets(rawValue: Int(modemPreset)) + 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/Helpers/NodeMapControl.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift new file mode 100644 index 00000000..75a455b7 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapControl.swift @@ -0,0 +1,164 @@ +// +// NodeMapControl.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 9/9/23. +// +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 + @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 { + + 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 { + VStack { + Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) + .font(.caption) + + 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) + + } + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .padding(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" + } + } + } + } + } + .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 deleted file mode 100644 index 99ffcd04..00000000 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ /dev/null @@ -1,248 +0,0 @@ -/* - 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/NodeDetailOld.swift b/Meshtastic/Views/Nodes/NodeDetailOld.swift new file mode 100644 index 00000000..f0e78ea5 --- /dev/null +++ b/Meshtastic/Views/Nodes/NodeDetailOld.swift @@ -0,0 +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) +// } +// } +//} diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 8cd324e5..7aa17db0 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -1,17 +1,26 @@ // -// NodeList.swift +// NodeListSplit.swift // Meshtastic // -// Copyright(c) Garth Vander Houwen 8/7/21. +// Created by Garth Vander Houwen on 9/8/23. // - -// 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 +enum SelectedDetail { + case positionLog + case nodeMap + case deviceMetricsLog + case environmentMetricsLog + case detectionSensorLog +} + + + struct NodeList: View { + + @State private var columnVisibility = NavigationSplitViewVisibility.all + @State private var selectedNode: NodeInfoEntity? @State private var searchText = "" var nodesQuery: Binding { Binding { @@ -30,102 +39,88 @@ struct NodeList: View { animation: .default) private var nodes: FetchedResults + - @State private var selection: NodeInfoEntity? // Nothing selected by default. var body: some View { - - NavigationSplitView { + 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 - 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: "repeat.circle.fill") - .font(.callout) - .symbolRenderingMode(.hierarchical) - Text("connected").font(.callout) - .foregroundColor(.green) - } - } - 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(.footnote) - } - } - } - if node.channel > 0 { - HStack(alignment: .bottom) { - Image(systemName: "fibrechannel") - .font(.footnote) - .symbolRenderingMode(.hierarchical) - Text("Channel: \(node.channel)") - .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) - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - } - } - } - .padding([.top, .bottom]) - } - } - .listStyle(.plain) + 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() - ) - .onAppear { + 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 { + 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 { + Text("select.node") + } + } detail: { + Text("Select something to view") + } + .navigationSplitViewStyle(.balanced) +// .onChange(of: selectedNode) { _ in +// if selectedNode == nil { +// columnVisibility = .all +// } else { +// columnVisibility = .doubleColumn +// } +// } + .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") + } + +// } detail: { +// VStack { +// Button("Detail Only") { +// columnVisibility = .detailOnly +// } +// +// Button("Content and Detail") { +// columnVisibility = .doubleColumn +// } +// +// Button("Show All") { +// columnVisibility = .all +// } +// } +// } } } diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 1948f6ea..a41f891c 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( @@ -236,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 diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 4cd48761..0ccbd6cd 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -17,15 +17,13 @@ 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.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) let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") if UIDevice.current.userInterfaceIdiom == .pad && !useGrid || UIDevice.current.userInterfaceIdiom == .mac { @@ -162,12 +160,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 + } } } } 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 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 (%@)";